From a03e816886f4241309a6e77ba7df1ad922da1fc0 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Sat, 31 Oct 2026 01:32:04 -0400 Subject: nx2: let the host page out texture pool backing first Switch 2 sparse blocks and GPU-only regions get guest memory when a game writes them (Mario Kart World copies 2-4 GB of compressed textures into its sparse texture pools) and the texture cache reads it again only when it builds an image there. Every 30 s, mark that backing cold: its pages leave the working set (VirtualUnlock on Windows, MADV_PAGEOUT on Linux) so the OS compresses or pages them out before anything else when RAM is short. The contents stay. Mario Kart World: 8.5 GB of RAM in use at five minutes instead of 9.0 GB on a machine with RAM to spare, frame times unchanged. --- src/common/host_memory.cpp | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) (limited to 'src/common/host_memory.cpp') diff --git a/src/common/host_memory.cpp b/src/common/host_memory.cpp index 75a9da80e9..c0e7160dd4 100644 --- a/src/common/host_memory.cpp +++ b/src/common/host_memory.cpp @@ -790,6 +790,27 @@ void HostMemory::Protect(size_t virtual_offset, size_t length, MemoryPermission #endif } +void HostMemory::HintCold(size_t physical_offset, size_t length, + std::optional virtual_offset) { + if (length == 0 || backing_base == nullptr) { + return; + } + const auto hint = [length](u8* address) { +#ifdef _WIN32 + // Unlocking pages that are not locked takes them out of the working set. + VirtualUnlock(address, length); +#elif defined(__linux__) && defined(MADV_PAGEOUT) + madvise(address, length, MADV_PAGEOUT); +#else + static_cast(address); +#endif + }; + hint(backing_base + physical_offset); + if (virtual_offset && virtual_base != nullptr && *virtual_offset + length <= virtual_size) { + hint(virtual_base + *virtual_offset); + } +} + void HostMemory::ClearBackingRegion(size_t physical_offset, size_t length, u32 fill_value) { std::memset(backing_base + physical_offset, fill_value, length); } -- cgit v1.2.3 From e2c512916c6f5042d030fcf1708043233f8df08e Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Sun, 1 Nov 2026 00:30:52 -0400 Subject: texture_cache: mark guest memory textures were uploaded from cold Once a texture is uploaded the host GPU has its own copy, and the guest rarely reads that memory again, but it stayed in the emulator's working set. Every 600 frames the guest ranges of images uploaded since (64 KiB or more, not GPU-written or sparse) are marked cold through HostMemory::HintCold, backing and CPU view, from a low-priority worker, so the OS pages them out before anything else when it needs RAM. Their contents stay; using them brings them back. Mario Kart World's title screen at ~10 minutes: game memory in RAM 4.8-5.5 -> 3.6 GB, the emulator's real RAM 10.6-11.9 -> 9.4 GB, frames over 100 ms not worse. (cherry picked from commit ea36b2c5ca352f8c8cdcc829a2d3bde9bdc06ece) --- src/common/host_memory.cpp | 2 +- src/common/host_memory.h | 3 +- src/core/device_memory_manager.h | 7 ++++ src/core/device_memory_manager.inc | 44 +++++++++++++++++++++++ src/video_core/texture_cache/texture_cache.h | 14 ++++++++ src/video_core/texture_cache/texture_cache_base.h | 2 ++ 6 files changed, 70 insertions(+), 2 deletions(-) (limited to 'src/common/host_memory.cpp') diff --git a/src/common/host_memory.cpp b/src/common/host_memory.cpp index c0e7160dd4..2c45dfd3a5 100644 --- a/src/common/host_memory.cpp +++ b/src/common/host_memory.cpp @@ -791,7 +791,7 @@ void HostMemory::Protect(size_t virtual_offset, size_t length, MemoryPermission } void HostMemory::HintCold(size_t physical_offset, size_t length, - std::optional virtual_offset) { + std::optional virtual_offset) const { if (length == 0 || backing_base == nullptr) { return; } diff --git a/src/common/host_memory.h b/src/common/host_memory.h index fe0938103c..4d8d15cf58 100644 --- a/src/common/host_memory.h +++ b/src/common/host_memory.h @@ -57,7 +57,8 @@ public: /// Memory written once and read again rarely: its pages leave the working set (backing and, /// when given, its view at virtual_offset), so the OS compresses or pages them out first when /// it needs RAM. The contents stay; using the pages brings them back. - void HintCold(size_t physical_offset, size_t length, std::optional virtual_offset); + void HintCold(size_t physical_offset, size_t length, + std::optional virtual_offset) const; [[nodiscard]] u8* BackingBasePointer() noexcept { return backing_base; diff --git a/src/core/device_memory_manager.h b/src/core/device_memory_manager.h index 9c034f01a1..05a8136169 100644 --- a/src/core/device_memory_manager.h +++ b/src/core/device_memory_manager.h @@ -18,6 +18,7 @@ #include "common/common_types.h" #include "common/range_mutex.h" #include "common/scratch_buffer.h" +#include "common/thread_worker.h" #include "common/sparse_large_vector.h" namespace Core { @@ -53,6 +54,10 @@ public: void BindInterface(DeviceInterface* device_inter); + /// Marks the memory behind [address, address + size) cold (Common::HostMemory::HintCold), + /// from a low-priority thread: data the host GPU has its own copy of, e.g. uploaded textures. + void HintCold(DAddr address, size_t size); + DAddr Allocate(size_t size); void AllocateFixed(DAddr start, size_t size); void Free(DAddr start, size_t size); @@ -194,7 +199,9 @@ private: std::unique_ptr> impl; const uintptr_t physical_base; + const DeviceMemory* device_memory_ptr; DeviceInterface* device_inter; + std::unique_ptr cold_hint_worker; struct TrackedEntry { VAddr cpu_backing_address; diff --git a/src/core/device_memory_manager.inc b/src/core/device_memory_manager.inc index c45bc71656..77ab95e07e 100644 --- a/src/core/device_memory_manager.inc +++ b/src/core/device_memory_manager.inc @@ -171,6 +171,7 @@ struct DeviceMemoryManagerAllocator { template DeviceMemoryManager::DeviceMemoryManager(const DeviceMemory& device_memory_) : physical_base{uintptr_t(device_memory_.buffer.BackingBasePointer())} + , device_memory_ptr{&device_memory_} , device_inter{nullptr} , compressed_device_addr(1ULL << ((Settings::values.memory_layout_mode.GetValue() == Settings::MemoryLayout::Memory_4Gb ? physical_min_bits : physical_max_bits) - Memory::YUZU_PAGEBITS)) , tracked_entries(device_as_size >> Memory::YUZU_PAGEBITS) @@ -182,6 +183,49 @@ DeviceMemoryManager::DeviceMemoryManager(const DeviceMemory& device_memo template DeviceMemoryManager::~DeviceMemoryManager() = default; +template +void DeviceMemoryManager::HintCold(DAddr address, size_t size) { + struct Run { + u64 physical_offset; + VAddr virtual_address; ///< 0: no CPU mapping known + u64 size; + }; + std::vector runs; + const size_t page_end = Common::DivCeil(address + size, Memory::YUZU_PAGESIZE); + for (size_t page = address >> Memory::YUZU_PAGEBITS; page < page_end; ++page) { + const u32 compressed = tracked_entries[page].compressed_physical_ptr; + if (compressed == 0) { + continue; + } + const u64 physical = u64{compressed - 1} << Memory::YUZU_PAGEBITS; + const VAddr vaddr = ExtractCPUBacking(page).second; + if (!runs.empty()) { + Run& last = runs.back(); + const bool contiguous_view = + last.virtual_address == 0 ? vaddr == 0 : last.virtual_address + last.size == vaddr; + if (last.physical_offset + last.size == physical && contiguous_view) { + last.size += Memory::YUZU_PAGESIZE; + continue; + } + } + runs.push_back({physical, vaddr, Memory::YUZU_PAGESIZE}); + } + if (runs.empty()) { + return; + } + if (!cold_hint_worker) { + cold_hint_worker = std::make_unique(1, "DeviceMemory:ColdHint"); + } + cold_hint_worker->QueueWork([this, runs = std::move(runs)] { + for (const Run& run : runs) { + device_memory_ptr->buffer.HintCold( + run.physical_offset, run.size, + run.virtual_address != 0 ? std::optional{run.virtual_address} + : std::nullopt); + } + }); +} + template void DeviceMemoryManager::BindInterface(DeviceInterface* device_inter_) { device_inter = device_inter_; diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index 63abffed0f..13637aa265 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -315,6 +315,12 @@ void TextureCache

::TickFrame() { sentenced_image_view.Tick(); TickAsyncDecode(); TickAsyncUnswizzle(); + if (frame_tick % 600 == 0 && !uploaded_ranges.empty()) { + for (const auto& [address, size] : uploaded_ranges) { + device_memory.HintCold(address, size); + } + uploaded_ranges.clear(); + } runtime.TickFrame(); ++frame_tick; @@ -1352,6 +1358,14 @@ void TextureCache

::RefreshContents(Image& image, ImageId image_id) { auto staging = runtime.UploadStagingBuffer(MapSizeBytes(image)); UploadImageContents(image, staging); runtime.InsertUploadMemoryBarrier(); + // The host GPU has its own copy now and the guest rarely reads this memory again: TickFrame + // marks it cold, so the OS pages it out before anything else when it needs RAM (title 1 + // streams several GB of textures through it). + constexpr u64 ColdHintMinBytes = 64_KiB; + if (image.guest_size_bytes >= ColdHintMinBytes && + False(image.flags & (ImageFlagBits::GpuModified | ImageFlagBits::Sparse))) { + uploaded_ranges.emplace_back(image.cpu_addr, image.guest_size_bytes); + } } template diff --git a/src/video_core/texture_cache/texture_cache_base.h b/src/video_core/texture_cache/texture_cache_base.h index 11e95f98b3..26d989f2da 100644 --- a/src/video_core/texture_cache/texture_cache_base.h +++ b/src/video_core/texture_cache/texture_cache_base.h @@ -558,6 +558,8 @@ private: Common::ThreadWorker texture_decode_worker{1, "TextureDecoder", {}, Common::ThreadPlacement::Efficiency}; std::vector> async_decodes; + /// Guest memory images were uploaded from since the last cold hint (device address, size). + std::vector> uploaded_ranges; std::deque unswizzle_queue; u8 current_unswizzle_frame; -- cgit v1.2.3