aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTakoroka <137927095+Takoroka@users.noreply.github.com>2026-11-01 13:25:46 -0500
committerTakoroka <137927095+Takoroka@users.noreply.github.com>2026-11-01 13:25:46 -0500
commit9ee8d6f6a6900537eccf67e4c3a9bb16248dc6ab (patch)
tree66082e3914f210cfdcb144c67e98a3aa16b5b5b4
parentf695266b80ba1e8a4a4f2f8421d65ed224068c7a (diff)
parent86968cb5ce3195cb131eeb0ef68d2d8a4860f7df (diff)
Merge remote-tracking branch 'origin/fix/cold-hint-uploaded-memory' into test/linux-all-prs
-rw-r--r--src/common/host_memory.cpp21
-rw-r--r--src/common/host_memory.h6
-rw-r--r--src/core/device_memory_manager.h7
-rw-r--r--src/core/device_memory_manager.inc44
-rw-r--r--src/core/hle/service/nx2_gsv.h40
-rw-r--r--src/video_core/buffer_cache/buffer_cache.h11
-rw-r--r--src/video_core/buffer_cache/buffer_cache_base.h2
-rw-r--r--src/video_core/texture_cache/texture_cache.h14
-rw-r--r--src/video_core/texture_cache/texture_cache_base.h2
9 files changed, 147 insertions, 0 deletions
diff --git a/src/common/host_memory.cpp b/src/common/host_memory.cpp
index 75a9da80e9..2c45dfd3a5 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<size_t> virtual_offset) const {
+ 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<void>(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);
}
diff --git a/src/common/host_memory.h b/src/common/host_memory.h
index 8747a1850f..4d8d15cf58 100644
--- a/src/common/host_memory.h
+++ b/src/common/host_memory.h
@@ -54,6 +54,12 @@ public:
void ClearBackingRegion(size_t physical_offset, size_t length, u32 fill_value);
+ /// 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<size_t> 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 27ff342213..d275bdbf2d 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);
@@ -197,7 +202,9 @@ private:
std::unique_ptr<DeviceMemoryManagerAllocator<Traits>> impl;
const uintptr_t physical_base;
+ const DeviceMemory* device_memory_ptr;
DeviceInterface* device_inter;
+ std::unique_ptr<Common::ThreadWorker> 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 83faa1b3f3..3523e3cf1c 100644
--- a/src/core/device_memory_manager.inc
+++ b/src/core/device_memory_manager.inc
@@ -171,6 +171,7 @@ struct DeviceMemoryManagerAllocator {
template <typename Traits>
DeviceMemoryManager<Traits>::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)
@@ -183,6 +184,49 @@ template <typename Traits>
DeviceMemoryManager<Traits>::~DeviceMemoryManager() = default;
template <typename Traits>
+void DeviceMemoryManager<Traits>::HintCold(DAddr address, size_t size) {
+ struct Run {
+ u64 physical_offset;
+ VAddr virtual_address; ///< 0: no CPU mapping known
+ u64 size;
+ };
+ std::vector<Run> 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<Common::ThreadWorker>(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<size_t>{run.virtual_address}
+ : std::nullopt);
+ }
+ });
+}
+
+template <typename Traits>
void DeviceMemoryManager<Traits>::BindInterface(DeviceInterface* device_inter_) {
device_inter = device_inter_;
}
diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h
index 628bdeda5b..90959057f1 100644
--- a/src/core/hle/service/nx2_gsv.h
+++ b/src/core/hle/service/nx2_gsv.h
@@ -1246,6 +1246,46 @@ private:
std::array<Kernel::KEvent*, NumIds> m_events{};
std::map<u32, GpuMemory> m_memory;
u32 m_next_memory_handle{1};
+
+ // Every 30 s: the backing of sparse blocks and GPU-only regions is marked cold. The game
+ // writes it once (texture uploads by 2D blits: title 1 fills 2-4 GB of texture pools) and it
+ // is read again only when the texture cache builds an image over it, so the OS may compress or
+ // page it out before anything else. Last member: stops before the regions are destroyed.
+ void HintColdBacking() {
+ struct Range {
+ u64 physical_offset;
+ u64 virtual_address;
+ u64 size;
+ };
+ std::vector<Range> ranges;
+ {
+ std::scoped_lock lk{m_region_mutex};
+ for (const auto& [gpu_address, region] : m_gpu_only_regions) {
+ u64 virtual_address = region.cpu_address;
+ for (const auto& block : *region.pages) {
+ ranges.push_back({GetInteger(block.GetAddress()) - Core::DramMemoryMap::Base,
+ virtual_address, block.GetSize()});
+ virtual_address += block.GetSize();
+ }
+ }
+ }
+ auto& buffer = system.DeviceMemory().buffer;
+ for (const Range& range : ranges) {
+ buffer.HintCold(range.physical_offset, range.size, range.virtual_address);
+ }
+ }
+ std::jthread m_cold_hint_thread{[this](std::stop_token stop) {
+ Common::SetCurrentThreadName("nx2:ColdHint");
+ Common::SetCurrentThreadPriority(Common::ThreadPriority::Low);
+ while (!stop.stop_requested()) {
+ for (int i = 0; i < 30 && !stop.stop_requested(); ++i) {
+ std::this_thread::sleep_for(std::chrono::seconds{1});
+ }
+ if (!stop.stop_requested()) {
+ HintColdBacking();
+ }
+ }
+ }};
};
// Native memory and images handed out by the Switch 2 graphics server (gsv 111/114/423/429/315),
diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h
index 53874e75cc..70d79c3535 100644
--- a/src/video_core/buffer_cache/buffer_cache.h
+++ b/src/video_core/buffer_cache/buffer_cache.h
@@ -100,6 +100,12 @@ void BufferCache<P>::TickFrame() {
return;
}
runtime.TickFrame(slot_buffers);
+ if (frame_tick % 600 == 0 && !uploaded_buffer_ranges.empty()) {
+ for (const auto& [address, size] : uploaded_buffer_ranges) {
+ device_memory.HintCold(address, size);
+ }
+ uploaded_buffer_ranges.clear();
+ }
if (static u64 frames{}; ++frames % 600 == 0) {
u64 total{};
size_t count{};
@@ -1928,6 +1934,11 @@ void BufferCache<P>::MappedUploadMemory([[maybe_unused]] Buffer& buffer,
u8* const src_pointer = staging_pointer.data() + copy.src_offset;
const DAddr device_addr = buffer.CpuAddr() + copy.dst_offset;
device_memory.ReadBlockUnsafe(device_addr, src_pointer, copy.size);
+ // Large uploads are mostly geometry the host GPU now has its own copy of: TickFrame
+ // marks their guest memory cold (see TextureCache::RefreshContents).
+ if (copy.size >= 256_KiB) {
+ uploaded_buffer_ranges.emplace_back(device_addr, copy.size);
+ }
// Apply the staging offset
copy.src_offset += upload_staging.offset;
}
diff --git a/src/video_core/buffer_cache/buffer_cache_base.h b/src/video_core/buffer_cache/buffer_cache_base.h
index d8a61b7913..47f342932d 100644
--- a/src/video_core/buffer_cache/buffer_cache_base.h
+++ b/src/video_core/buffer_cache/buffer_cache_base.h
@@ -580,6 +580,8 @@ private:
size_t immediate_buffer_capacity = 0;
Common::ScratchBuffer<u8> immediate_buffer_alloc;
+ /// Guest memory large uploads came from since the last cold hint (device address, size).
+ std::vector<std::pair<DAddr, u64>> uploaded_buffer_ranges;
struct LRUItemParams {
using ObjectType = BufferId;
diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h
index 33ca051482..77485a282e 100644
--- a/src/video_core/texture_cache/texture_cache.h
+++ b/src/video_core/texture_cache/texture_cache.h
@@ -320,6 +320,12 @@ void TextureCache<P>::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;
@@ -1365,6 +1371,14 @@ void TextureCache<P>::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 <class P>
diff --git a/src/video_core/texture_cache/texture_cache_base.h b/src/video_core/texture_cache/texture_cache_base.h
index 141e5b76d5..12240287ff 100644
--- a/src/video_core/texture_cache/texture_cache_base.h
+++ b/src/video_core/texture_cache/texture_cache_base.h
@@ -578,6 +578,8 @@ private:
Common::ThreadWorker texture_decode_worker{1, "TextureDecoder", {},
Common::ThreadPlacement::Efficiency};
std::vector<std::unique_ptr<AsyncDecodeContext>> async_decodes;
+ /// Guest memory images were uploaded from since the last cold hint (device address, size).
+ std::vector<std::pair<DAddr, u64>> uploaded_ranges;
std::deque<PendingUnswizzle> unswizzle_queue;
u8 current_unswizzle_frame;