From 1c2ed2a072ba24a3d21a25dfa6cfcce1bf1ae905 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Thu, 29 Oct 2026 04:40:26 -0400 Subject: video_core: skip empty fences; one lock per cache per fence; single-pass overlap subtract Switch 2 titles signal a fence for each compute dispatch's release (5,000-7,000 a second in title 1). With fast GPU fences the callback runs at once, so a fence with no downloads, queries or queued operations does nothing: those (~20%) are no longer made (one in 64 still is, for the operations fences carry). Each cache's lock is now taken once per fence (the flush check took both together first). The overlap range set's Subtract restarted its search after every erase (quadratic); the fencing thread runs it for every downloaded range while holding the buffer cache. The GPU profiler counts fences, skipped fences and buffer download bytes. --- src/video_core/buffer_cache/buffer_cache.h | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src/video_core/buffer_cache') diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h index ad2e200438..e2de0eede1 100644 --- a/src/video_core/buffer_cache/buffer_cache.h +++ b/src/video_core/buffer_cache/buffer_cache.h @@ -17,6 +17,7 @@ #include "video_core/buffer_cache/buffer_cache_base.h" #include "video_core/guest_memory.h" #include "video_core/host1x/gpu_device_memory_manager.h" +#include "video_core/nx2_profile.h" #include "video_core/texture_cache/util.h" namespace VideoCommon { @@ -666,6 +667,8 @@ void BufferCache

::CommitAsyncFlushesHigh() { async_buffers.emplace_back(std::optional{}); return; } + Nx2Profile::Add(Nx2Profile::Counter::DownloadBytes, total_size_bytes); + Nx2Profile::Add(Nx2Profile::Counter::DownloadCopies, downloads.size()); auto download_staging = runtime.DownloadStagingBuffer(total_size_bytes, true); boost::container::small_vector normalized_copies; runtime.PreCopyBarrier(); -- cgit v1.2.3 From b905580202f0da5ca0a6268b176b4591e347fd8d Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Thu, 29 Oct 2026 05:46:59 -0400 Subject: video_core: compact the macro memo arena; download profiling counters Calls stored again with another shape leave their old record in the memo's arena; title 1 filled its 16 MiB every ~2 s and clearing it made the next ~20,000 calls interpret again. The live records are now copied to a new arena (a clear only when they alone fill three quarters of it). The memo statistics count hits on the last call's slot and compactions. With EDEN_NX2_GPU_PROFILE the GPU thread also reports how much downloaded buffer data lands on pages the native MME has DMA-read. --- src/video_core/buffer_cache/buffer_cache.h | 13 +++++++++++++ src/video_core/macro.cpp | 17 +++++++++++++---- src/video_core/macro_memo.h | 30 +++++++++++++++++++++++++++--- src/video_core/nx2_profile.h | 19 ++++++++++++++++--- 4 files changed, 69 insertions(+), 10 deletions(-) (limited to 'src/video_core/buffer_cache') diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h index e2de0eede1..b909971e42 100644 --- a/src/video_core/buffer_cache/buffer_cache.h +++ b/src/video_core/buffer_cache/buffer_cache.h @@ -679,6 +679,19 @@ void BufferCache

::CommitAsyncFlushesHigh() { Buffer& buffer = slot_buffers[buffer_id]; second_copy.src_offset = static_cast(buffer.CpuAddr()) + copy.src_offset; const DAddr orig_device_addr = static_cast(second_copy.src_offset); + if (Nx2Profile::Enabled()) { + u64 on_dma_pages = 0; + const auto& pages = Nx2Profile::DmaReadPages(); + for (DAddr page = orig_device_addr >> 12; page <= (orig_device_addr + copy.size - 1) >> 12; + ++page) { + if (pages.contains(page)) { + const DAddr begin = (std::max)(orig_device_addr, page << 12); + const DAddr end = (std::min)(orig_device_addr + copy.size, (page + 1) << 12); + on_dma_pages += end - begin; + } + } + Nx2Profile::Add(Nx2Profile::Counter::DownloadBytesOnDmaPages, on_dma_pages); + } async_downloads.Add(orig_device_addr, copy.size); buffer.MarkUsage(copy.src_offset, copy.size); runtime.CopyBuffer(download_staging.buffer, buffer, copies, false); diff --git a/src/video_core/macro.cpp b/src/video_core/macro.cpp index 3422cb16fa..50e32ce999 100644 --- a/src/video_core/macro.cpp +++ b/src/video_core/macro.cpp @@ -1420,6 +1420,14 @@ void MacroEngine::Execute(Core::System& system, Engines::Maxwell3D& maxwell3d, u // from a culling dispatch) and still be only in host buffers: flush them first, // or the macro draws with an older frame's arguments (objects pop in and out). const size_t read_bytes = size_t{std::min(value, 0x1000)} * 4; + if (VideoCommon::Nx2Profile::Enabled()) { + VideoCommon::Nx2Profile::Add(VideoCommon::Nx2Profile::Counter::DmaReadBytes, read_bytes); + for (GPUVAddr page = source & ~GPUVAddr{0xfff}; page < source + read_bytes; page += 0x1000) { + if (const auto device = memory.GpuToCpuAddress(page)) { + VideoCommon::Nx2Profile::DmaReadPages().insert(*device >> 12); + } + } + } if (memory.IsMemoryDirty(source, read_bytes, VideoCommon::CacheType::BufferCache)) { memory.FlushRegion(source, read_bytes, VideoCommon::CacheType::BufferCache); static u64 flushes{}; @@ -1462,10 +1470,11 @@ void MacroEngine::Execute(Core::System& system, Engines::Maxwell3D& maxwell3d, u memo_stats && (++native_calls & 0xffffff) == 0) { const auto& stats = native_memo.GetStatistics(); LOG_INFO(HW_GPU, - "Macro memo: {} hits, {} misses, {} guard failures, {} stores ({} in " - "place), {} clears; {} entries, {} KiB", - stats.hits, stats.misses, stats.guard_failures, stats.stores, - stats.overwrites, stats.clears, native_memo.Size(), + "Macro memo: {} hits ({} on the last call's slot), {} misses, {} guard " + "failures, {} stores ({} in place), {} compactions, {} clears; {} entries, " + "{} KiB", + stats.hits, stats.slot_hits, stats.misses, stats.guard_failures, stats.stores, + stats.overwrites, stats.compactions, stats.clears, native_memo.Size(), native_memo.ArenaWords() * 4 / 1024); } native_memo.Synchronize(native_code_generation); diff --git a/src/video_core/macro_memo.h b/src/video_core/macro_memo.h index 0e994fc78a..7fe914b4f2 100644 --- a/src/video_core/macro_memo.h +++ b/src/video_core/macro_memo.h @@ -41,7 +41,8 @@ public: }; struct Statistics { - std::uint64_t hits{}, misses{}, guard_failures{}, stores{}, overwrites{}, clears{}; + std::uint64_t hits{}, slot_hits{}, misses{}, guard_failures{}, stores{}, overwrites{}, + compactions{}, clears{}; }; BasicMacroMemo() { @@ -57,6 +58,7 @@ public: if (last.first == start && last.second != NoRecord && Match(last.second, start, parameters, data_ram, effects)) { ++statistics.hits; + ++statistics.slot_hits; return true; } const auto it = entries.find(Hash(start, parameters)); @@ -127,8 +129,7 @@ public: } } if (arena.size() >= MaxArenaWords) { - Clear(); - ++statistics.clears; + Compact(); } const auto offset = static_cast(arena.size()); arena.push_back(start); @@ -186,8 +187,31 @@ private: last_by_start.fill({0, NoRecord}); } + /// Copies the records still in the map to a new arena. A call stored again with another + /// shape leaves its old record behind: title 1 filled the arena with them every ~2 s, and + /// clearing it made the next ~20,000 calls interpret again. Clears only when the live + /// records alone would fill most of it. + void Compact() { + compacted.clear(); + for (auto& entry : entries) { + const std::uint32_t* record = arena.data() + entry.second; + const std::size_t words = + HeaderWords + record[1] + std::size_t{2} * (record[2] + record[3] + record[4]); + entry.second = static_cast(compacted.size()); + compacted.insert(compacted.end(), record, record + words); + } + arena.swap(compacted); + last_by_start.fill({0, NoRecord}); + ++statistics.compactions; + if (arena.size() >= MaxArenaWords / 4 * 3) { + Clear(); + ++statistics.clears; + } + } + Map entries; std::vector arena; + std::vector compacted; ///< Compact's new arena (kept for its capacity) /// The last record found or stored per macro start (direct mapped). std::array, 256> last_by_start{}; std::uint64_t generation{}; diff --git a/src/video_core/nx2_profile.h b/src/video_core/nx2_profile.h index 088bfc5fe7..e4609cf77d 100644 --- a/src/video_core/nx2_profile.h +++ b/src/video_core/nx2_profile.h @@ -7,6 +7,7 @@ #include #include #include +#include #include "common/common_types.h" #include "common/logging.h" @@ -74,7 +75,7 @@ inline u64 Now() { struct ThreadData { std::array(Zone::Count)> exclusive_ns{}; std::array(Zone::Count)> count{}; - std::array counters{}; ///< See Counter + std::array counters{}; ///< See Counter u64 child_ns{}; ///< Time of the zones nested in the current one u64 window_start{}; }; @@ -118,8 +119,17 @@ private: enum class Counter : u32 { DownloadBytes, ///< Buffer data copied back from the host GPU at fences DownloadCopies, ///< Buffer ranges copied back + DmaReadBytes, ///< Guest memory the native MME read with DMA + DownloadBytesOnDmaPages, ///< Of DownloadBytes: on pages the MME has DMA-read }; +/// Device pages (4 KiB) the native MME has DMA-read since start (GPU thread only; filled only +/// while profiling): which buffer downloads anything on the GPU thread reads back. +inline std::unordered_set& DmaReadPages() { + static std::unordered_set pages; + return pages; +} + inline void Add(Counter counter, u64 amount) { if (Enabled()) { t_data.counters[static_cast(counter)] += amount; @@ -158,9 +168,12 @@ inline void Report() { const double calls = static_cast(data.count[i]) * 1e9 / window; line += fmt::format(" | {} {:.1f} ms ({:.0f}/s)", zone_names[i], ms, calls); } - line += fmt::format(" | buffer downloads {:.0f} KiB in {:.0f} copies", + line += fmt::format(" | buffer downloads {:.0f} KiB in {:.0f} copies ({:.0f} KiB on pages " + "MME DMA reads) | MME DMA reads {:.0f} KiB", static_cast(data.counters[0]) * 1e9 / 1024.0 / window, - static_cast(data.counters[1]) * 1e9 / window); + static_cast(data.counters[1]) * 1e9 / window, + static_cast(data.counters[3]) * 1e9 / 1024.0 / window, + static_cast(data.counters[2]) * 1e9 / 1024.0 / window); LOG_INFO(HW_GPU, "GPU thread per second:{}", line); data = {}; data.window_start = now; -- cgit v1.2.3 From be449da3db512f790c71d60a58e027a5898a0446 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Thu, 29 Oct 2026 06:22:54 -0400 Subject: buffer_cache: no buffer downloads at every fence for Switch 2 titles Eden copied every GPU-written buffer range back to guest memory at the next fence. Title 1 signals a fence per compute dispatch, so that was 250-330 MB/s in 13,000-20,000 ranges a second, with the fencing thread holding the buffer cache while it wrote them. Measured: the native MME's DMA reads never touched those pages (they read query words written on the CPU side), and with reactive flushing the guest CPU read GPU-written memory at most 64 times in 200 s. Without them, for Switch 2 titles (native tiling): fences ~1.1 us instead of ~5, draw setup 6.1 us instead of 6.4-6.7, macro calls 160 ns instead of 167-170, GPU-thread lock spinning 1.8% (was 5.5%). Regions stay marked GPU written and are still flushed on demand. EDEN_NX2_BUFFER_DOWNLOADS=0/1 forces either way. Reactive flushing reads of GPU-written memory are now counted in the log. --- src/video_core/buffer_cache/buffer_cache.h | 8 ++++++-- src/video_core/buffer_cache/buffer_cache_base.h | 19 +++++++++++++++++++ src/video_core/gpu.cpp | 7 +++++++ 3 files changed, 32 insertions(+), 2 deletions(-) (limited to 'src/video_core/buffer_cache') diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h index b909971e42..0a6b53b33b 100644 --- a/src/video_core/buffer_cache/buffer_cache.h +++ b/src/video_core/buffer_cache/buffer_cache.h @@ -252,7 +252,9 @@ bool BufferCache

::DMACopy(GPUVAddr src_address, GPUVAddr dest_address, u64 am const DAddr diff = base_address - *cpu_src_address; const DAddr new_base_address = *cpu_dest_address + diff; tmp_intervals.push_back({new_base_address, size}); - uncommitted_gpu_modified_ranges.Add(new_base_address, size); + if (AsyncBufferDownloads()) { + uncommitted_gpu_modified_ranges.Add(new_base_address, size); + } }; gpu_modified_ranges.ForEachInRange(*cpu_src_address, amount, mirror); // This subtraction in this order is important for overlapping copies. @@ -1574,7 +1576,9 @@ void BufferCache

::MarkWrittenBuffer(BufferId buffer_id, DAddr device_addr, u3 } memory_tracker.MarkRegionAsGpuModified(device_addr, size); gpu_modified_ranges.Add(device_addr, size); - uncommitted_gpu_modified_ranges.Add(device_addr, size); + if (AsyncBufferDownloads()) { + uncommitted_gpu_modified_ranges.Add(device_addr, size); + } } template diff --git a/src/video_core/buffer_cache/buffer_cache_base.h b/src/video_core/buffer_cache/buffer_cache_base.h index 57011752cc..6d6d873b75 100644 --- a/src/video_core/buffer_cache/buffer_cache_base.h +++ b/src/video_core/buffer_cache/buffer_cache_base.h @@ -7,6 +7,7 @@ #pragma once #include +#include #include #include #include @@ -514,6 +515,24 @@ private: MemoryTracker memory_tracker; Common::RangeSet uncommitted_gpu_modified_ranges; + + /// Whether GPU-written buffer ranges are copied back to guest memory at the next fence; + /// otherwise only when something flushes them (as upstream yuzu below High GPU accuracy). + /// Not for Switch 2 titles: title 1 had 250-330 MB/s copied back (a fence per compute + /// dispatch, the fencing thread holding the buffer cache while it wrote them), while its CPU + /// read GPU-written memory at most 64 times in 200 s (counted with reactive flushing) and + /// the native MME's DMA reads never touched those pages. Without them a fence costs ~1 us + /// instead of ~5 and draw setup ~7% less. EDEN_NX2_BUFFER_DOWNLOADS=0/1 forces it. + bool AsyncBufferDownloads() const { + static const int forced = [] { + const char* value = std::getenv("EDEN_NX2_BUFFER_DOWNLOADS"); + return value == nullptr ? -1 : (value[0] == '0' ? 0 : 1); + }(); + if (forced >= 0) { + return forced == 1; + } + return gpu_memory == nullptr || !gpu_memory->UsesNativeTiling(); + } Common::RangeSet gpu_modified_ranges; std::deque> committed_gpu_modified_ranges; diff --git a/src/video_core/gpu.cpp b/src/video_core/gpu.cpp index 6441b9511a..53f8f063f7 100644 --- a/src/video_core/gpu.cpp +++ b/src/video_core/gpu.cpp @@ -221,6 +221,13 @@ struct GPU::Impl { return raster_area; } raster_area.preemtive = true; + // A guest CPU read of GPU-written memory (reactive flushing): count them, they decide + // whether buffer data must be copied back at every fence (EDEN_NX2_BUFFER_DOWNLOADS). + if (static std::atomic reads{}; (reads.fetch_add(1, std::memory_order_relaxed) & 63) == 0) { + LOG_INFO(HW_GPU, "CPU read of GPU-written memory [{:#x}, {:#x}) ({} so far)", + raster_area.start_address, raster_area.end_address, + reads.load(std::memory_order_relaxed)); + } const u64 fence = RequestSyncOperation([this, &raster_area]() { renderer->ReadRasterizer()->FlushRegion(raster_area.start_address, raster_area.end_address - raster_area.start_address); }); -- cgit v1.2.3 From 6b7185676d59976bf02f6666368032067ff344bc Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:05:03 -0400 Subject: common: pass-through hash for integer and pointer keys; RangeSet::Intersects Common::unordered_map/set defaulted to std::hash, a byte-wise FNV-1a on MSVC for integers and pointers, which boost's flat containers then mix again: the GPU thread's page maps (shader and query cache invalidation, per inline upload) spent ~1% in it. Integer and pointer keys now go through as they are and boost mixes them. BufferCache::IsRegionGpuModified visited every GPU-written range inside the query just to answer yes or no; without per-fence downloads those ranges stay fragmented and it was ~4% of title 1's GPU thread (inline uploads). RangeSet::Intersects answers with one lookup. Inline uploads: 10.5% -> 7.5% of the GPU thread. --- src/common/container/unordered_map.h | 2 +- src/common/container/unordered_set.h | 2 +- src/common/container_hash.h | 22 ++++++++++++++++++++++ src/common/range_sets.h | 3 +++ src/common/range_sets.inc | 12 ++++++++++++ src/video_core/buffer_cache/buffer_cache.h | 6 +++--- 6 files changed, 42 insertions(+), 5 deletions(-) (limited to 'src/video_core/buffer_cache') diff --git a/src/common/container/unordered_map.h b/src/common/container/unordered_map.h index 7d9e49fd80..c4d4a72663 100644 --- a/src/common/container/unordered_map.h +++ b/src/common/container/unordered_map.h @@ -9,7 +9,7 @@ namespace Common { -template , class Pred = std::equal_to, +template , class Pred = std::equal_to, class Allocator = std::allocator>> using unordered_map = boost::unordered::unordered_flat_map; diff --git a/src/common/container/unordered_set.h b/src/common/container/unordered_set.h index 54d71406a1..690c33286f 100644 --- a/src/common/container/unordered_set.h +++ b/src/common/container/unordered_set.h @@ -9,7 +9,7 @@ namespace Common { -template , class Pred = std::equal_to, +template , class Pred = std::equal_to, class Allocator = std::allocator> using unordered_set = boost::unordered::unordered_flat_set; diff --git a/src/common/container_hash.h b/src/common/container_hash.h index d7477f7e4f..f06f69ad8b 100644 --- a/src/common/container_hash.h +++ b/src/common/container_hash.h @@ -19,6 +19,28 @@ namespace Common { +/// The default hash of Common::unordered_map/set. std::hash of an integer or a pointer is a +/// byte-wise FNV-1a on MSVC, and boost's flat containers mix a hash that does not say it +/// avalanches anyway: pass those keys through (the GPU thread's page maps looked keys up +/// tens of thousands of times a second with it). Other keys keep std::hash. +template +struct FastHash : std::hash {}; + +template + requires std::is_integral_v +struct FastHash { + std::size_t operator()(Key key) const noexcept { + return static_cast(key); + } +}; + +template +struct FastHash { + std::size_t operator()(T* pointer) const noexcept { + return reinterpret_cast(pointer); + } +}; + namespace detail { template diff --git a/src/common/range_sets.h b/src/common/range_sets.h index f8fcee4837..e5cb53b058 100644 --- a/src/common/range_sets.h +++ b/src/common/range_sets.h @@ -26,6 +26,9 @@ public: void Clear(); bool Empty() const; + /// Whether any range overlaps [base_address, base_address + size): one lookup. + bool Intersects(AddressType base_address, size_t size) const; + template void ForEach(Func&& func) const; diff --git a/src/common/range_sets.inc b/src/common/range_sets.inc index 57b07d05f6..aeda7bc351 100644 --- a/src/common/range_sets.inc +++ b/src/common/range_sets.inc @@ -220,6 +220,18 @@ bool RangeSet::Empty() const { return m_impl->m_ranges_set.empty(); } +template +bool RangeSet::Intersects(AddressType base_address, size_t size) const { + const auto& set = m_impl->m_ranges_set; + if (set.empty() || size == 0) { + return false; + } + const AddressType end_address = base_address + static_cast(size); + // The first range not entirely below the query; it overlaps when it starts before its end. + const auto it = set.lower_bound(typename RangeSetImpl::IntervalType{base_address, end_address}); + return it != set.end() && it->lower() < end_address; +} + template template void RangeSet::ForEach(Func&& func) const { diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h index 0a6b53b33b..c6fc5989b7 100644 --- a/src/video_core/buffer_cache/buffer_cache.h +++ b/src/video_core/buffer_cache/buffer_cache.h @@ -746,9 +746,9 @@ void BufferCache

::PopAsyncBuffers() { template bool BufferCache

::IsRegionGpuModified(DAddr addr, size_t size) { - bool is_dirty = false; - gpu_modified_ranges.ForEachInRange(addr, size, [&](DAddr, DAddr) { is_dirty = true; }); - return is_dirty; + // One lookup instead of visiting every range inside (~4% of the GPU thread for title 1's + // inline uploads once GPU-written ranges are no longer cleared by downloads). + return gpu_modified_ranges.Intersects(addr, size); } template -- cgit v1.2.3 From 4d7558e25bb0a59d90153bc8dfdca4b4e1fc2f48 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:26:48 -0400 Subject: buffer_cache: one GPU wait for all buffers the collector evicts The buffer collector downloaded each evicted buffer's GPU-written data with its own wait for the GPU. Without per-fence downloads (d530bc4) most of title 1's evicted buffers are GPU-written, and on an 8 GB GPU near its budget it evicts some every frame. Their ranges now go to one staging buffer, copied with one wait. --- src/video_core/buffer_cache/buffer_cache.h | 69 ++++++++++++++++++++++--- src/video_core/buffer_cache/buffer_cache_base.h | 3 ++ 2 files changed, 66 insertions(+), 6 deletions(-) (limited to 'src/video_core/buffer_cache') diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h index c6fc5989b7..39a528ddeb 100644 --- a/src/video_core/buffer_cache/buffer_cache.h +++ b/src/video_core/buffer_cache/buffer_cache.h @@ -62,17 +62,28 @@ void BufferCache

::RunGarbageCollector() { const bool aggressive_gc = total_used_memory >= critical_memory; const u64 ticks_to_destroy = aggressive_gc ? 60 : 120; int num_iterations = aggressive_gc ? 64 : 32; - const auto clean_up = [this, &num_iterations](BufferId buffer_id) { + // Their GPU-written data is downloaded first, all with one wait for the GPU: one wait per + // buffer stalled the GPU thread every frame on title 1 (an 8 GB GPU near its budget, no + // per-fence downloads keeping the buffers clean), ~2.5% of its time. + boost::container::small_vector evicted; + lru_cache.ForEachItemBelow(frame_tick - ticks_to_destroy, [&](BufferId buffer_id) { if (num_iterations == 0) { return true; } --num_iterations; - auto& buffer = slot_buffers[buffer_id]; - DownloadBufferMemory(buffer); - DeleteBuffer(buffer_id); + evicted.push_back(buffer_id); return false; - }; - lru_cache.ForEachItemBelow(frame_tick - ticks_to_destroy, clean_up); + }); + if constexpr (USE_MEMORY_MAPS) { + DownloadBuffersMemory(evicted); + } else { + for (const BufferId buffer_id : evicted) { + DownloadBufferMemory(slot_buffers[buffer_id]); + } + } + for (const BufferId buffer_id : evicted) { + DeleteBuffer(buffer_id); + } } template @@ -1951,6 +1962,52 @@ void BufferCache

::DownloadBufferMemory(Buffer& buffer) { DownloadBufferMemory(buffer, buffer.CpuAddr(), buffer.SizeBytes()); } +template +void BufferCache

::DownloadBuffersMemory(std::span buffer_ids) { + struct Download { + BufferId buffer_id; + BufferCopy copy; + }; + std::vector downloads; + u64 total_size_bytes = 0; + for (const BufferId buffer_id : buffer_ids) { + Buffer& buffer = slot_buffers[buffer_id]; + const DAddr buffer_addr = buffer.CpuAddr(); + memory_tracker.ForEachDownloadRangeAndClear( + buffer_addr, buffer.SizeBytes(), [&](u64 device_addr_out, u64 range_size) { + gpu_modified_ranges.ForEachInRange(device_addr_out, range_size, + [&](DAddr start, DAddr end) { + downloads.push_back({buffer_id, BufferCopy{ + .src_offset = start - buffer_addr, + .dst_offset = total_size_bytes, + .size = end - start, + }}); + total_size_bytes += Common::AlignUp(end - start, 64); + }); + ClearDownload(device_addr_out, range_size); + gpu_modified_ranges.Subtract(device_addr_out, range_size); + }); + } + if (total_size_bytes == 0) { + return; + } + auto download_staging = runtime.DownloadStagingBuffer(total_size_bytes); + for (Download& download : downloads) { + download.copy.dst_offset += download_staging.offset; + Buffer& buffer = slot_buffers[download.buffer_id]; + buffer.MarkUsage(download.copy.src_offset, download.copy.size); + runtime.CopyBuffer(download_staging.buffer, buffer, std::span{&download.copy, 1}, true); + } + runtime.Finish(); + const u8* const mapped_memory = download_staging.mapped_span.data(); + for (const Download& download : downloads) { + const DAddr device_addr = slot_buffers[download.buffer_id].CpuAddr() + download.copy.src_offset; + device_memory.WriteBlockUnsafe( + device_addr, mapped_memory + (download.copy.dst_offset - download_staging.offset), + download.copy.size); + } +} + template void BufferCache

::DownloadBufferMemory(Buffer& buffer, DAddr device_addr, u64 size) { boost::container::small_vector copies; diff --git a/src/video_core/buffer_cache/buffer_cache_base.h b/src/video_core/buffer_cache/buffer_cache_base.h index 6d6d873b75..25d35e03ec 100644 --- a/src/video_core/buffer_cache/buffer_cache_base.h +++ b/src/video_core/buffer_cache/buffer_cache_base.h @@ -468,6 +468,9 @@ private: void DownloadBufferMemory(Buffer& buffer_id, DAddr device_addr, u64 size); + /// Downloads the GPU-written data of several buffers with one wait for the GPU. + void DownloadBuffersMemory(std::span buffer_ids); + void DeleteBuffer(BufferId buffer_id, bool do_not_mark = false); [[nodiscard]] Binding StorageBufferBinding(GPUVAddr ssbo_addr, u32 cbuf_index, -- cgit v1.2.3 From 35e55ed818a705847bc2cb676fc35d315b683e55 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Thu, 29 Oct 2026 08:38:09 -0400 Subject: buffer_cache: collect on the buffers' own size for Switch 2 titles The buffer collector compared the whole device's memory use with thresholds from the device size, so with title 1's textures filling an 8 GB GPU (~6.8 GB used, buffers ~200 MiB of it) it ran aggressively every frame: up to 64 buffers evicted and recreated later, the GPU-written ones downloaded first with a wait for the GPU (~5% of the GPU thread). For Switch 2 titles it now runs when the buffers themselves pass 1 GiB (aggressive past 2 GiB); below the aggressive mode it also keeps buffers holding GPU-written data, which it would have to download. Other titles keep the device-wide thresholds. Buffer downloads by the collector: 5.4% -> 0%; the cache settles around 540 MiB. --- src/video_core/buffer_cache/buffer_cache.h | 26 +++++++++++++++++++++---- src/video_core/buffer_cache/buffer_cache_base.h | 3 ++- 2 files changed, 24 insertions(+), 5 deletions(-) (limited to 'src/video_core/buffer_cache') diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h index 39a528ddeb..42d7ba066d 100644 --- a/src/video_core/buffer_cache/buffer_cache.h +++ b/src/video_core/buffer_cache/buffer_cache.h @@ -58,8 +58,7 @@ template BufferCache

::~BufferCache() = default; template -void BufferCache

::RunGarbageCollector() { - const bool aggressive_gc = total_used_memory >= critical_memory; +void BufferCache

::RunGarbageCollector(bool aggressive_gc) { const u64 ticks_to_destroy = aggressive_gc ? 60 : 120; int num_iterations = aggressive_gc ? 64 : 32; // Their GPU-written data is downloaded first, all with one wait for the GPU: one wait per @@ -71,6 +70,14 @@ void BufferCache

::RunGarbageCollector() { return true; } --num_iterations; + // Below the critical threshold, buffers holding GPU-written data stay: evicting one + // means downloading it first. The thresholds compare the whole device's use (mostly + // textures) with fixed headroom, so on an 8 GB GPU the collector ran every frame once a + // title 1 course was loaded, and the downloads were ~2.7% of the GPU thread. + const Buffer& buffer = slot_buffers[buffer_id]; + if (!aggressive_gc && gpu_modified_ranges.Intersects(buffer.CpuAddr(), buffer.SizeBytes())) { + return false; + } evicted.push_back(buffer_id); return false; }); @@ -129,8 +136,17 @@ void BufferCache

::TickFrame() { if (runtime.CanReportMemoryUsage()) { total_used_memory = runtime.GetDeviceMemoryUsage(); } - if (total_used_memory >= minimum_memory) { - RunGarbageCollector(); + if (gpu_memory != nullptr && gpu_memory->UsesNativeTiling()) { + // Switch 2 titles fill the device with textures (their collector watches the budget); + // title 1's buffers were ~200 MiB of an 8 GB GPU's ~6.8 GB in use, yet this collector + // ran aggressively every frame on the device-wide figure, evicting up to 64 buffers (and + // downloading the GPU-written ones, ~5% of the GPU thread) that were recreated later. + // Collect when the buffers themselves are large. + if (buffer_bytes >= 1_GiB) { + RunGarbageCollector(buffer_bytes >= 2_GiB); + } + } else if (total_used_memory >= minimum_memory) { + RunGarbageCollector(total_used_memory >= critical_memory); } ++frame_tick; delayed_destruction_ring.Tick(); @@ -1770,9 +1786,11 @@ void BufferCache

::ChangeRegister(BufferId buffer_id) { Buffer& buffer = slot_buffers[buffer_id]; const auto size = buffer.SizeBytes(); if (insert) { + buffer_bytes += Common::AlignUp(size, 1024); total_used_memory += Common::AlignUp(size, 1024); buffer.setLRUID(lru_cache.Insert(buffer_id, frame_tick)); } else { + buffer_bytes -= Common::AlignUp(size, 1024); total_used_memory -= Common::AlignUp(size, 1024); lru_cache.Free(buffer.getLRUID()); } diff --git a/src/video_core/buffer_cache/buffer_cache_base.h b/src/video_core/buffer_cache/buffer_cache_base.h index 25d35e03ec..45a1836341 100644 --- a/src/video_core/buffer_cache/buffer_cache_base.h +++ b/src/video_core/buffer_cache/buffer_cache_base.h @@ -378,7 +378,7 @@ private: ((device_addr + size) & ~Core::DEVICE_PAGEMASK); } - void RunGarbageCollector(); + void RunGarbageCollector(bool aggressive_gc); void BindHostIndexBuffer(); @@ -560,6 +560,7 @@ private: std::vector compute_segments; u64 frame_tick = 0; u64 total_used_memory = 0; + u64 buffer_bytes = 0; ///< The buffers' own size (total_used_memory may be the device's) u64 minimum_memory = 0; u64 critical_memory = 0; BufferId inline_buffer_id; -- cgit v1.2.3