From 3e2056da36ff14ce671ef5ce8b55980aaf440c3c Mon Sep 17 00:00:00 2001 From: Brandon Bunnelle Date: Fri, 6 Nov 2026 07:50:39 -0500 Subject: [PATCH 1/5] =?UTF-8?q?buffer=5Fcache,=20maxwell=5F3d:=20OoT=20GPU?= =?UTF-8?q?-thread=20experiments=20=E2=80=94=20readback=20throttle=20+=20m?= =?UTF-8?q?acro=20stack=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EDEN_NX2_PREFLUSH_PER_FRAME=1 (default off): commit Preflushable buffer readbacks once per frame instead of at every compute fence. Ocarina of Time in forest scenes was copied back at 1.9 GiB/s in 135k copies/s (a fence per compute dispatch x marked ranges), while its guest CPU read GPU-written memory ~571 times a second. Throttle variants tested: plain per-frame (washed out the tonemap), size-thresholded (half-fresh snapshot, partial washout), region-uniform (gamma oscillating between two states), trap-armed (constant gamma flicker). All broke the image path's reader, whose freshness contract is tighter than one frame; the eager scheme is FPS-neutral (the fencing thread is 5% busy) so correctness wins. Code kept for a documented negative result and future work. EDEN_NX2_MACRO_STACK_HIGH=1 (default off): take ProcessMacro's stack fast path in accurate GPU mode as well. The title makes ~900k single-run macro calls a second; the vector/segment/RefreshParameters plumbing exists for the re-fetch-at-use semantic, which an arrival-time snapshot satisfies for every macro it runs. Measured on Ryzen 7 9800X3D / RTX 3080, Accurate mode, 1x, FSR replacement: EDEN_NX2_GPU_PROFILE's zone clock (WallClock::GetTimeNS, ~150k reads/s) costs 27% of the GPU thread on Linux — the heavy vista is latency-bound (GPU thread 60%, cores 30%, ~30 fps), with memory-manager page walks ~15%, lock overhead ~8%, method dispatch ~10% of the thread as the next levers. Co-Authored-By: Claude Code --- src/video_core/buffer_cache/buffer_cache.h | 17 ++++- .../buffer_cache/buffer_cache_base.h | 67 ++++++++++++++++++- src/video_core/engines/maxwell_3d.cpp | 12 +++- 3 files changed, 92 insertions(+), 4 deletions(-) diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h index e73d1cbd6c..497ed74a11 100644 --- a/src/video_core/buffer_cache/buffer_cache.h +++ b/src/video_core/buffer_cache/buffer_cache.h @@ -106,6 +106,14 @@ void BufferCache

::TickFrame() { if (!channel_state) { return; } + // PreflushPerFrame(): the frame boundary is where this frame's Preflushable writes become + // the next fence's downloads (one commit per frame, not one per compute fence). + if (PreflushPerFrame() && !pending_preflush_adds.Empty()) { + pending_preflush_adds.ForEach([&](DAddr start, DAddr end) { + uncommitted_gpu_modified_ranges.Add(start, end - start); + }); + pending_preflush_adds.Clear(); + } runtime.TickFrame(slot_buffers); native_indirect_uniforms_this_frame.clear(); if (frame_tick % 600 == 0 && !uploaded_buffer_ranges.empty()) { @@ -263,8 +271,13 @@ std::optional BufferCache

::GetFlushArea(DA area->start_address = device_addr_start_aligned; area->end_address = device_addr_end_aligned; // Preflushable data is downloaded at fences, but only where it is marked (QueueGpuWrite): the - // rest of the range still needs this flush. - if (memory_tracker.IsRegionPreflushable(device_addr, size) && + // rest of the range still needs this flush. With LazyPreflushDownloads nothing downloads at + // fences, so this fast path must not claim the data is already back: every read of a + // GPU-modified range flushes (preemtive false) through OnCPURead's synchronous flush. + // A range still pending its frame drain (PreflushPerFrame) is likewise not back yet. + if (!LazyPreflushDownloads() && memory_tracker.IsRegionPreflushable(device_addr, size) && + !pending_preflush_adds.Intersects(device_addr_start_aligned, + device_addr_end_aligned - device_addr_start_aligned) && !HasGpuWritesNotDownloadedAtFence(device_addr_start_aligned, device_addr_end_aligned - device_addr_start_aligned)) { area->preemtive = true; diff --git a/src/video_core/buffer_cache/buffer_cache_base.h b/src/video_core/buffer_cache/buffer_cache_base.h index 33ada38210..a28d60e3a7 100644 --- a/src/video_core/buffer_cache/buffer_cache_base.h +++ b/src/video_core/buffer_cache/buffer_cache_base.h @@ -605,6 +605,22 @@ class BufferCache : public VideoCommon::ChannelSetupCachesUsesNativeTiling(); } + /// EDEN_NX2_LAZY_PREFLUSH=1: ranges the guest CPU has read once (Preflushable) are not + /// re-downloaded at every fence either. Every fence that follows a GPU write to such a range + /// copies it back whether anything reads it again or not; Ocarina of Time read GPU-written + /// memory ~3 times a second (single pages, via OnCPURead) while its Preflushable ranges + /// were copied back at 1.9 GiB/s in 135k copies/s (forest scenes, GPU thread saturated). + /// Instead each GPU write arms the guest read traps over the whole range (the generation + /// bump alone invalidates the cores' rasterizer read areas), and a read flushes once, + /// synchronously, through OnCPURead — the path a first read already takes. + bool LazyPreflushDownloads() const { + static const bool lazy = [] { + const char* value = std::getenv("EDEN_NX2_LAZY_PREFLUSH"); + return value != nullptr && value[0] == '1'; + }(); + return lazy; + } + /// Whether a GPU write to this range is copied back at the next fence. Ranges the guest CPU /// has read (Preflushable, set by the first reactive flush) always are: later reads skip the /// flush and rely on these downloads, e.g. collision meshes skinned by compute shaders. @@ -636,11 +652,23 @@ class BufferCache : public VideoCommon::ChannelSetupCaches cursor) { ArmReadTraps(cursor, lower - cursor); } - uncommitted_gpu_modified_ranges.Add(lower, upper - lower); + // Throttle by the size of the whole marked region, not the written part: a + // region's pages must all move with the same timing. Readers treat a mark as + // one structure (a doorbell page plus its payload — throttling only the large + // writes gave the guest CPU a half-fresh snapshot and washed out the screen); + // small regions stay fence-fresh, large ones frame-fresh as a whole. A + // throttled write also arms its read traps: a read that lands before the frame + // drain force-flushes instead of trusting host memory whose age oscillated + // frame to frame (the screen's gamma flipped between two states). + if (PreflushPerFrame() && range_size > PreflushPerFrameMinBytes()) { + pending_preflush_adds.Add(lower, upper - lower); + ArmReadTraps(lower, upper - lower); + } else { + uncommitted_gpu_modified_ranges.Add(lower, upper - lower); + } cursor = upper; }); if (cursor < end) { @@ -660,6 +701,28 @@ class BufferCache : public VideoCommon::ChannelSetupCaches: with PreflushPerFrame, ranges at most this large + /// still download at every fence (freshness-critical parameters); larger ones once per + /// frame. Default one page. + static u64 PreflushPerFrameMinBytes() { + static const u64 min_bytes = [] { + const char* value = std::getenv("EDEN_NX2_PREFLUSH_MIN_BYTES"); + const long parsed = value == nullptr ? 4096 : std::atol(value); + return parsed <= 0 ? u64{4096} : static_cast(parsed); + }(); + return min_bytes; + } + /// GPU writes that are not downloaded at fences: trap the guest CPU's next read of their /// pages, which flushes them once and marks them Preflushable (then DownloadsAtFence). void ArmReadTraps(DAddr device_addr, u64 size) { @@ -679,6 +742,8 @@ class BufferCache : public VideoCommon::ChannelSetupCaches read_trapped_ranges; Common::RangeSet gpu_modified_ranges; std::deque> committed_gpu_modified_ranges; + /// With PreflushPerFrame(): Preflushable ranges GPU-written since the last TickFrame. + Common::RangeSet pending_preflush_adds; // Async Buffers std::deque> async_buffers; diff --git a/src/video_core/engines/maxwell_3d.cpp b/src/video_core/engines/maxwell_3d.cpp index 133987d214..aff2508f59 100644 --- a/src/video_core/engines/maxwell_3d.cpp +++ b/src/video_core/engines/maxwell_3d.cpp @@ -228,9 +228,19 @@ void Maxwell3D::ProcessMacro(Core::System& system, u32 method, const u32* base_s // (and the macro memo stores the parameters after it ran). Only accurate GPU mode rewrites // the parameters in place (RefreshParameters), so it keeps the vector. // EDEN_NX2_MACRO_COPY=1: always use the vector (for comparisons). + // EDEN_NX2_MACRO_STACK_HIGH=1: the stack path in accurate mode too — the parameter copy + // snapshots the words at arrival, so nothing can rewrite them under the macro and the + // per-call vector/segment bookkeeping (and RefreshParameters re-reads) are not needed. + // Ocarina of Time makes ~900k single-run macro calls a second; the vector path costs + // ~100-200 ns of plumbing per call on the GPU thread for a semantic (re-fetch at use) + // that a snapshot satisfies for every macro the title runs. static const bool always_copy = std::getenv("EDEN_NX2_MACRO_COPY") != nullptr; + static const bool stack_in_high = [] { + const char* value = std::getenv("EDEN_NX2_MACRO_STACK_HIGH"); + return value != nullptr && value[0] == '1'; + }(); if (is_last_call && macro_params.empty() && amount <= 64 && !always_copy && - !Settings::IsGPULevelHigh()) { + (!Settings::IsGPULevelHigh() || stack_in_high)) { std::array parameters; std::copy_n(base_start, amount, parameters.begin()); ConsumeSink(system); From f007ef93d7199e9310b15502d34502270e24e516 Mon Sep 17 00:00:00 2001 From: Brandon Bunnelle Date: Fri, 6 Nov 2026 08:35:19 -0500 Subject: [PATCH 2/5] memory_manager: 1024-entry MaxContinuousRange cache + EDEN_NX2_MEMSTATS Ocarina of Time binds more distinct (address, size) pairs than the 64-entry direct-mapped cache held: a vista run thrashed 22% of ~85k queries/s with a stable page table (785 serial changes, all at boot), each miss re-walking up to 8 MiB. 1024 entries cut the steady-state miss rate to ~6% (~3.3x fewer walks, ~2.5% of the GPU thread). EDEN_NX2_MEMSTATS=1 logs per-thread queries/hits/misses/serial changes every 500k queries. Co-Authored-By: Claude Code --- src/video_core/memory_manager.cpp | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/video_core/memory_manager.cpp b/src/video_core/memory_manager.cpp index 11115710e7..5a9c5c66f0 100644 --- a/src/video_core/memory_manager.cpp +++ b/src/video_core/memory_manager.cpp @@ -594,13 +594,38 @@ size_t MemoryManager::MaxContinuousRange(GPUVAddr gpu_addr, size_t size) const { u64 serial; size_t result; }; - thread_local std::array cache{}; + // 1024 entries: Ocarina of Time binds more distinct (address, size) pairs than 64 held — + // a vista run thrashed 22% of ~85k queries/s with a stable page table (785 serial changes, + // all at boot; measured with EDEN_NX2_MEMSTATS), each miss re-walking up to 8 MiB. + thread_local std::array cache{}; const u64 serial = map_serial.load(std::memory_order_acquire); - Entry& entry = cache[((gpu_addr ^ size) * 0x9E3779B97F4A7C15ULL) >> 58]; + Entry& entry = cache[((gpu_addr ^ size) * 0x9E3779B97F4A7C15ULL) >> 54]; + // EDEN_NX2_MEMSTATS=1: per-thread hit/miss and page-table-change counts, logged every + // 500k queries (finds whether the miss cost of this cache is thrash or remaps). + thread_local u64 stat_queries{}, stat_hits{}, stat_misses{}, stat_serials{}, stat_last_serial{}; + const bool stats = [] { + static const bool on = std::getenv("EDEN_NX2_MEMSTATS") != nullptr; + return on; + }(); + if (stats) [[unlikely]] { + ++stat_queries; + stat_serials += serial != stat_last_serial; + stat_last_serial = serial; + } if (entry.owner == unique_identifier + 1 && entry.gpu_addr == gpu_addr && entry.size == size && entry.serial == serial) { + if (stats) [[unlikely]] { + if (++stat_hits % 500000 == 0) { + LOG_INFO(HW_GPU, + "MaxContinuousRange: {} queries ({} hit, {} miss, {} serial changes)", + stat_queries, stat_hits, stat_misses, stat_serials); + } + } return entry.result; } + if (stats) [[unlikely]] { + ++stat_misses; + } const size_t result = MaxContinuousRangeUncached(gpu_addr, size); entry = {unique_identifier + 1, gpu_addr, size, serial, result}; return result; From 2970578f3418d7955613990c707fd39d49effb6e Mon Sep 17 00:00:00 2001 From: Brandon Bunnelle Date: Fri, 6 Nov 2026 12:36:50 -0500 Subject: [PATCH 3/5] docstrings for the touched functions + env-gate lambdas become named helpers Satisfies the docstring-coverage pre-merge check (was 33%): documents BufferCache::TickFrame, BufferCache::GetFlushArea, Maxwell3D::ProcessMacro and MemoryManager::MaxContinuousRange, and replaces the EDEN_NX2_LAZY_PREFLUSH / EDEN_NX2_PREFLUSH_PER_FRAME / EDEN_NX2_PREFLUSH_MIN_BYTES / EDEN_NX2_MEMSTATS initialization lambdas with named, documented helpers (identical semantics, read once as before). Syntax-checked against the real build flags. Co-Authored-By: Claude Code --- src/video_core/buffer_cache/buffer_cache.h | 6 ++++ .../buffer_cache/buffer_cache_base.h | 36 ++++++++++++------- src/video_core/engines/maxwell_3d.cpp | 4 +++ src/video_core/memory_manager.cpp | 15 +++++--- 4 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h index 497ed74a11..0099b924ff 100644 --- a/src/video_core/buffer_cache/buffer_cache.h +++ b/src/video_core/buffer_cache/buffer_cache.h @@ -100,6 +100,9 @@ void BufferCache

::RunGarbageCollector(bool aggressive_gc) { } } +/// End-of-frame bookkeeping: retires async buffer downloads, drops stale uniform ranges and +/// the occasional threshold report, and — with PreflushPerFrame() — drains this frame's +/// queued Preflushable ranges into the set the next fence downloads. template void BufferCache

::TickFrame() { // Homebrew console apps don't create or bind any channels, so this will be nullptr. @@ -261,6 +264,9 @@ bool BufferCache

::OnCPUWrite(DAddr device_addr, u64 size) { return false; } +/// Resolves the guest read of [device_addr, device_addr + size) to a download area: page +/// aligned, and marked preemtive (no synchronous flush needed) only when the data is already +/// back in guest memory — downloaded at a fence and not still pending a frame drain. template std::optional BufferCache

::GetFlushArea(DAddr device_addr, u64 size) { diff --git a/src/video_core/buffer_cache/buffer_cache_base.h b/src/video_core/buffer_cache/buffer_cache_base.h index a28d60e3a7..68eabc96b0 100644 --- a/src/video_core/buffer_cache/buffer_cache_base.h +++ b/src/video_core/buffer_cache/buffer_cache_base.h @@ -605,6 +605,26 @@ class BufferCache : public VideoCommon::ChannelSetupCachesUsesNativeTiling(); } + /// Reads EDEN_NX2_LAZY_PREFLUSH=1 once (see LazyPreflushDownloads). + static bool EnvLazyPreflush() { + const char* const value = std::getenv("EDEN_NX2_LAZY_PREFLUSH"); + return value != nullptr && value[0] == '1'; + } + + /// Reads EDEN_NX2_PREFLUSH_PER_FRAME=1 once (see PreflushPerFrame). + static bool EnvPreflushPerFrame() { + const char* const value = std::getenv("EDEN_NX2_PREFLUSH_PER_FRAME"); + return value != nullptr && value[0] == '1'; + } + + /// Reads EDEN_NX2_PREFLUSH_MIN_BYTES once: the byte threshold under which a Preflushable + /// range still downloads at every fence; one page when unset or unparsable. + static u64 EnvPreflushPerFrameMinBytes() { + const char* const value = std::getenv("EDEN_NX2_PREFLUSH_MIN_BYTES"); + const long parsed = value == nullptr ? 4096 : std::atol(value); + return parsed <= 0 ? u64{4096} : static_cast(parsed); + } + /// EDEN_NX2_LAZY_PREFLUSH=1: ranges the guest CPU has read once (Preflushable) are not /// re-downloaded at every fence either. Every fence that follows a GPU write to such a range /// copies it back whether anything reads it again or not; Ocarina of Time read GPU-written @@ -614,10 +634,7 @@ class BufferCache : public VideoCommon::ChannelSetupCaches(parsed); - }(); + static const u64 min_bytes = EnvPreflushPerFrameMinBytes(); return min_bytes; } diff --git a/src/video_core/engines/maxwell_3d.cpp b/src/video_core/engines/maxwell_3d.cpp index aff2508f59..f93fbccde7 100644 --- a/src/video_core/engines/maxwell_3d.cpp +++ b/src/video_core/engines/maxwell_3d.cpp @@ -211,6 +211,10 @@ bool Maxwell3D::IsMethodExecutable(u32 method) { } } +/// Handles a write to a macro method or its ARGS register: accumulates the call's parameter +/// words and dispatches the macro when the call is complete, choosing between the stack +/// snapshot path (parameters copied at arrival) and the per-call vector that accurate mode +/// refreshes in place. void Maxwell3D::ProcessMacro(Core::System& system, u32 method, const u32* base_start, u32 amount, bool is_last_call) { if (executing_macro == 0) { // A macro call must begin by writing the macro method's register, not its argument. diff --git a/src/video_core/memory_manager.cpp b/src/video_core/memory_manager.cpp index 5a9c5c66f0..ca97b78476 100644 --- a/src/video_core/memory_manager.cpp +++ b/src/video_core/memory_manager.cpp @@ -583,6 +583,16 @@ bool MemoryManager::IsMemoryDirty(GPUVAddr gpu_addr, size_t size, return result; } +/// Reads EDEN_NX2_MEMSTATS once: whether the MaxContinuousRange cache logs its hit/miss and +/// page-table-change counters (see MaxContinuousRange below). +static bool MemstatsEnabled() { + static const bool on = std::getenv("EDEN_NX2_MEMSTATS") != nullptr; + return on; +} + +/// Length of the largest continuously mapped range starting at gpu_addr and not exceeding +/// size, answered from a per-thread direct-mapped cache invalidated by page-table changes; +/// with EDEN_NX2_MEMSTATS=1, hit/miss and remap counters are logged every 500k queries. size_t MemoryManager::MaxContinuousRange(GPUVAddr gpu_addr, size_t size) const { // Native storage buffers ask this for up to 8 MiB on each bind, ~70,000 times a second in // title 1's forest (17% of the GPU thread walking pages): keep answers per thread until the @@ -603,10 +613,7 @@ size_t MemoryManager::MaxContinuousRange(GPUVAddr gpu_addr, size_t size) const { // EDEN_NX2_MEMSTATS=1: per-thread hit/miss and page-table-change counts, logged every // 500k queries (finds whether the miss cost of this cache is thrash or remaps). thread_local u64 stat_queries{}, stat_hits{}, stat_misses{}, stat_serials{}, stat_last_serial{}; - const bool stats = [] { - static const bool on = std::getenv("EDEN_NX2_MEMSTATS") != nullptr; - return on; - }(); + const bool stats = MemstatsEnabled(); if (stats) [[unlikely]] { ++stat_queries; stat_serials += serial != stat_last_serial; From 18a0809ba636bb8dff62f7f42264583de4e8a8c1 Mon Sep 17 00:00:00 2001 From: Brandon Bunnelle Date: Fri, 6 Nov 2026 14:34:06 -0500 Subject: [PATCH 4/5] buffer_cache: drop the EDEN_NX2_PREFLUSH_PER_FRAME experiment Per review: the flag stayed FPS-neutral (the eager path's fencing thread sits near 5% busy) and every throttle variant damaged the image on Ocarina of Time (per-frame: washed out; size-split: half-fresh snapshots; region-uniform: gamma bistable; trap-armed force-flush: constant flicker). The negative result stays documented in the PR description; the eager per-fence download remains. Co-Authored-By: Claude Code --- src/video_core/buffer_cache/buffer_cache.h | 14 +---- .../buffer_cache/buffer_cache_base.h | 53 +------------------ 2 files changed, 2 insertions(+), 65 deletions(-) diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h index 0099b924ff..0375bad7ac 100644 --- a/src/video_core/buffer_cache/buffer_cache.h +++ b/src/video_core/buffer_cache/buffer_cache.h @@ -101,22 +101,13 @@ void BufferCache

::RunGarbageCollector(bool aggressive_gc) { } /// End-of-frame bookkeeping: retires async buffer downloads, drops stale uniform ranges and -/// the occasional threshold report, and — with PreflushPerFrame() — drains this frame's -/// queued Preflushable ranges into the set the next fence downloads. +/// the occasional threshold report. template void BufferCache

::TickFrame() { // Homebrew console apps don't create or bind any channels, so this will be nullptr. if (!channel_state) { return; } - // PreflushPerFrame(): the frame boundary is where this frame's Preflushable writes become - // the next fence's downloads (one commit per frame, not one per compute fence). - if (PreflushPerFrame() && !pending_preflush_adds.Empty()) { - pending_preflush_adds.ForEach([&](DAddr start, DAddr end) { - uncommitted_gpu_modified_ranges.Add(start, end - start); - }); - pending_preflush_adds.Clear(); - } runtime.TickFrame(slot_buffers); native_indirect_uniforms_this_frame.clear(); if (frame_tick % 600 == 0 && !uploaded_buffer_ranges.empty()) { @@ -280,10 +271,7 @@ std::optional BufferCache

::GetFlushArea(DA // rest of the range still needs this flush. With LazyPreflushDownloads nothing downloads at // fences, so this fast path must not claim the data is already back: every read of a // GPU-modified range flushes (preemtive false) through OnCPURead's synchronous flush. - // A range still pending its frame drain (PreflushPerFrame) is likewise not back yet. if (!LazyPreflushDownloads() && memory_tracker.IsRegionPreflushable(device_addr, size) && - !pending_preflush_adds.Intersects(device_addr_start_aligned, - device_addr_end_aligned - device_addr_start_aligned) && !HasGpuWritesNotDownloadedAtFence(device_addr_start_aligned, device_addr_end_aligned - device_addr_start_aligned)) { area->preemtive = true; diff --git a/src/video_core/buffer_cache/buffer_cache_base.h b/src/video_core/buffer_cache/buffer_cache_base.h index 68eabc96b0..a6ae645a16 100644 --- a/src/video_core/buffer_cache/buffer_cache_base.h +++ b/src/video_core/buffer_cache/buffer_cache_base.h @@ -611,20 +611,6 @@ class BufferCache : public VideoCommon::ChannelSetupCaches(parsed); - } - /// EDEN_NX2_LAZY_PREFLUSH=1: ranges the guest CPU has read once (Preflushable) are not /// re-downloaded at every fence either. Every fence that follows a GPU write to such a range /// copies it back whether anything reads it again or not; Ocarina of Time read GPU-written @@ -670,13 +656,6 @@ class BufferCache : public VideoCommon::ChannelSetupCaches cursor) { ArmReadTraps(cursor, lower - cursor); } - // Throttle by the size of the whole marked region, not the written part: a - // region's pages must all move with the same timing. Readers treat a mark as - // one structure (a doorbell page plus its payload — throttling only the large - // writes gave the guest CPU a half-fresh snapshot and washed out the screen); - // small regions stay fence-fresh, large ones frame-fresh as a whole. A - // throttled write also arms its read traps: a read that lands before the frame - // drain force-flushes instead of trusting host memory whose age oscillated - // frame to frame (the screen's gamma flipped between two states). - if (PreflushPerFrame() && range_size > PreflushPerFrameMinBytes()) { - pending_preflush_adds.Add(lower, upper - lower); - ArmReadTraps(lower, upper - lower); - } else { - uncommitted_gpu_modified_ranges.Add(lower, upper - lower); - } + uncommitted_gpu_modified_ranges.Add(lower, upper - lower); cursor = upper; }); if (cursor < end) { @@ -718,21 +684,6 @@ class BufferCache : public VideoCommon::ChannelSetupCaches: with PreflushPerFrame, ranges at most this large - /// still download at every fence (freshness-critical parameters); larger ones once per - /// frame. Default one page. - static u64 PreflushPerFrameMinBytes() { - static const u64 min_bytes = EnvPreflushPerFrameMinBytes(); - return min_bytes; - } - /// GPU writes that are not downloaded at fences: trap the guest CPU's next read of their /// pages, which flushes them once and marks them Preflushable (then DownloadsAtFence). void ArmReadTraps(DAddr device_addr, u64 size) { @@ -752,8 +703,6 @@ class BufferCache : public VideoCommon::ChannelSetupCaches read_trapped_ranges; Common::RangeSet gpu_modified_ranges; std::deque> committed_gpu_modified_ranges; - /// With PreflushPerFrame(): Preflushable ranges GPU-written since the last TickFrame. - Common::RangeSet pending_preflush_adds; // Async Buffers std::deque> async_buffers; From 53792a39344d27ca939d45732ac1f8054ca5875e Mon Sep 17 00:00:00 2001 From: Brandon Bunnelle Date: Fri, 6 Nov 2026 18:36:31 -0500 Subject: [PATCH 5/5] memory_manager: open the seqlock before table mutations The serial the MaxContinuousRange cache invalidates on bumped after the tables changed: a reader walking concurrently could cache a torn result under a still-valid serial. The ops now bump first, so a reader either sees the old serial with old tables or retries. (The GpuToCpuAddress cache and its seqlock read live on the perf branch this PR does not include.) Co-Authored-By: Claude Code --- src/video_core/memory_manager.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/video_core/memory_manager.cpp b/src/video_core/memory_manager.cpp index ca97b78476..2c2fd671e5 100644 --- a/src/video_core/memory_manager.cpp +++ b/src/video_core/memory_manager.cpp @@ -117,6 +117,9 @@ inline void MemoryManager::SetBigPageContinuous(size_t big_page_index, bool valu } GPUVAddr MemoryManager::PageTableOp(GPUVAddr gpu_addr, [[maybe_unused]] DAddr dev_addr, size_t size, PTEKind kind, MemoryManager::EntryType entry_type) { + // Seqlock open: lock-free readers (GpuToCpuAddress's cache, MaxContinuousRange) retry a + // walk whose serial moved, so a mutation in flight is never read as a torn table. + map_serial.fetch_add(1, std::memory_order_release); [[maybe_unused]] u64 remaining_size{size}; if (entry_type == EntryType::Mapped) { page_table.ReserveRange(gpu_addr, size); @@ -158,6 +161,8 @@ GPUVAddr MemoryManager::PageTableOp(GPUVAddr gpu_addr, [[maybe_unused]] DAddr de } GPUVAddr MemoryManager::BigPageTableOp(GPUVAddr gpu_addr, [[maybe_unused]] DAddr dev_addr, size_t size, PTEKind kind, MemoryManager::EntryType entry_type) { + // Seqlock open, as in PageTableOp above. + map_serial.fetch_add(1, std::memory_order_release); [[maybe_unused]] u64 remaining_size{size}; GPUVAddr modified_start{}; u64 modified_size{};