From 8a980e4fd57236bc7cced8ba34b9ed0c5b88686f Mon Sep 17 00:00:00 2001 From: chrg6 Date: Wed, 11 Nov 2026 19:37:27 -0500 Subject: [PATCH] Add NX2NAVI_WB_SKIP_MB: without the lazy write-back, large written ranges go back to guest memory for their first 64 KiB only. Where the lazy write-back is off (AMD's driver, outside Windows), what a binding written by a shader or a copy holds goes back to guest memory at the end of every submission. With NX2NAVI_WB_SKIP_MB=n a written range of n MiB or more goes back for its first 64 KiB, which carries the few words the CPU reads at the start of such buffers, and the rest stays in the mirror, whose pool is then never evicted. A CPU read further in would see older bytes, and a CPU write further in would have the next sync copy that page's older guest bytes over the GPU's, so it is a test switch, default 0. Measured with NX2NAVI_WB_LAZY=0 on an RTX 5090, 75 s runs with warm caches, off against NX2NAVI_WB_SKIP_MB=1: Kakariko walk GPU median 6.60 against 4.20 ms a frame, 98 against 132 fps; Hyrule Field walk GPU 10.09 against 4.88 ms, 76 against 132 fps, 1% low 61 against 98 fps. 78 to 94 MiB a frame stayed in the mirrors. The last frames of both runs in each area show the same picture, and the logs have no errors. Co-Authored-By: Claude Opus 5.5 --- runtime/cfg.c | 6 +++--- runtime/vk_memory.c | 42 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/runtime/cfg.c b/runtime/cfg.c index 3de30d28..1c640201 100644 --- a/runtime/cfg.c +++ b/runtime/cfg.c @@ -47,7 +47,7 @@ static const char *const known[] = { "DESC_TEMPLATE", "DEVICE_PROFILE", "DISCORD_APP_ID", "DISCORD_IMAGE", "DISCORD_RPC", "DLAA", "DLAA_CAM_KEEP", "DLSS_IDLE_FREE", "DLAA_JITTER", "DLAA_MV", "DLAA_MASKDEBUG", "DLAA_MVDEBUG", "DLAA_OBJMV", "DLAA_SURVEY", "DLAA_TARGET_KEEP", "DLAA_TIMES", "DLSS", "DLSS_CHAR_MASK", "DLSS_PRESET", "DLSS_REACTIVE", "DUMP", "DUMP_BUF", "DUMP_DIR", "DUMP_LOG", "DUMP_RAW", "DUMP_SHADERS", "DUMP_FILE", "DS3", "LOG_CS_BINDS", "LOG_TEX_FLAGS", "LOG_MESH", "MESH", "POOL", "NATIVE_TENSOR", "LOG_RCOND", "LOG_RT_SIZES", "DUMP_WHITE", "DUMP_AFTER", "DUMP_MOVIE", "SUBMIT_FINISH", "ACQUIRE_WAIT", "CRASH_FILTER_GUARD", "CS_FALLBACK", "EXEFS", "FEEDBACK", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES", "FFX_PIPE_CACHE", "FLIGHT_SPIKE_MS", "FLIGHT_THUMBS", "FLIGHT_EVERY", "FLUSH_SPAN", "FLUSH_UPLOAD", "FOG_CULL", "FOLIAGE_SHADOW_CULL", "FOLIAGE_SHADOW_VRS", "FOV", "FOV_PATCH", "FG_HUDLESS_SHOW", "FG_MULT", "FPS60", "FPS_LIMIT", "FRAMEGEN", "FRAMEGEN_MULTIPLIER", "FREEZE_IDLE", "FREEZE_ASYNC", "FREEZE_BUDGET_US", "FREE_CORES", - "FULLSCREEN", "FXAA", "GAME_ASPECT", "GAME_RES", "GPA", "GPA_EMPTY", "GPA_TWIN", "GPA_TWIN_MAX_KB", "MIRROR_CAPTURE_KB", "GPL", "GPL_DRAW_BUILD", "GPL_FEEDBACK", "PIPE_AHEAD", "PIPE_FAST_WORKERS", "PIPE_FRAME_WAIT_MS", "PIPE_LINK_DRAW_MS", "PIPE_SKIP_STEADY", "PROG_LEAD", "GPU", "GPU_INDEX", "GPU_PASSES", "GPU_NAME", "HDR", + "FULLSCREEN", "FXAA", "GAME_ASPECT", "GAME_RES", "GPA", "GPA_EMPTY", "GPA_TWIN", "GPA_TWIN_MAX_KB", "MIRROR_CAPTURE_KB", "WB_SKIP_MB", "GPL", "GPL_DRAW_BUILD", "GPL_FEEDBACK", "PIPE_AHEAD", "PIPE_FAST_WORKERS", "PIPE_FRAME_WAIT_MS", "PIPE_LINK_DRAW_MS", "PIPE_SKIP_STEADY", "PROG_LEAD", "GPU", "GPU_INDEX", "GPU_PASSES", "GPU_NAME", "HDR", "HDR_GAME_NITS", "HDR_LIVE", "HEAP_LAZY", "HDR_PEAK_NITS", "HDR_UI_NITS", // retired: the HDR look's settings of earlier versions, now fixed (vk_hdr.c, shaders/hdr_tonemap.frag); read by // nothing, listed so that a file which still sets them logs no unknown key @@ -140,7 +140,7 @@ static const struct { const char *key, *section; } sections[] = { {"VK_DYN_VERTEX", "performance"}, {"VK_DYN_A2C", "performance"}, {"VK_SUBGROUP_PIN", "performance"}, {"VK_RECONVERGE", "performance"}, {"VK_NAN_PRESERVE", "performance"}, {"VK_NAN_BITS", "performance"}, {"TRACE", "diagnostics"}, {"FLIGHT", "diagnostics"}, {"PASS_CRUMBS", "diagnostics"}, {"FLIGHT_SPIKE_MS", "diagnostics"}, - {"WB_FENCE_LIMIT", "diagnostics"}, {"WB_MERGE", "diagnostics"}, {"WB_EXACT", "diagnostics"}, {"MEM_CENSUS", "diagnostics"}, {"WB_NO_REUP", "diagnostics"}, {"WB_PROBE", "diagnostics"}, {"WB_SPLIT", "performance"}, {"SHADOW_BULK_WRITE", "performance"}, {"WB_LAZY", "performance"}, {"HEAP_LAZY", "performance"}, {"BARRIER_GAME", "performance"}, {"MESH_AHEAD", "performance"}, {"MESH_FALLBACK", "performance"}, {"MESH_FALLBACK_MB", "performance"}, {"MESH_FALLBACK_WORKERS", "performance"}, {"WB_LAZY_TOUCH", "diagnostics"}, {"WB_LAZY_CHUNK", "performance"}, {"FLUSH_UPLOAD", "diagnostics"}, {"GPA_TWIN_MAX_KB", "diagnostics"}, {"MIRROR_CAPTURE_KB", "diagnostics"}, + {"WB_FENCE_LIMIT", "diagnostics"}, {"WB_MERGE", "diagnostics"}, {"WB_EXACT", "diagnostics"}, {"MEM_CENSUS", "diagnostics"}, {"WB_NO_REUP", "diagnostics"}, {"WB_PROBE", "diagnostics"}, {"WB_SPLIT", "performance"}, {"SHADOW_BULK_WRITE", "performance"}, {"WB_LAZY", "performance"}, {"HEAP_LAZY", "performance"}, {"BARRIER_GAME", "performance"}, {"MESH_AHEAD", "performance"}, {"MESH_FALLBACK", "performance"}, {"MESH_FALLBACK_MB", "performance"}, {"MESH_FALLBACK_WORKERS", "performance"}, {"WB_LAZY_TOUCH", "diagnostics"}, {"WB_LAZY_CHUNK", "performance"}, {"FLUSH_UPLOAD", "diagnostics"}, {"GPA_TWIN_MAX_KB", "diagnostics"}, {"MIRROR_CAPTURE_KB", "diagnostics"}, {"WB_SKIP_MB", "diagnostics"}, {"WB_MERGE_MAX", "diagnostics"}, {"MV_SURVEY", "diagnostics"}, {"TEXUP_LOG_MS", "diagnostics"}, {"WAIT_WATCH_S", "diagnostics"}, {"VMA_BLOCK_MB", "diagnostics"}, {"ASTC_BC_PROBE", "diagnostics"}, {"DEVICE_LOST_AT", "diagnostics"}, {"STAGING_FAIL_AT", "diagnostics"}, @@ -311,7 +311,7 @@ static const CfgMeta metas[] = { // diagnostics {"TRACE", "", NULL, RESTART}, {"FLIGHT", "0", "0|1", RESTART}, {"FLIGHT_SPIKE_MS", "100", "0..60000", RESTART}, {"WB_FENCE_LIMIT", "65536", "0..1073741824", RESTART}, {"WB_MERGE", "1", "0|1", RESTART}, {"WB_EXACT", "1", "0|1", RESTART}, {"MEM_CENSUS", "0", "0|1", RESTART}, {"WB_NO_REUP", "1", "0|1", RESTART}, {"WB_PROBE", "0", "0|1", RESTART}, {"WB_SPLIT", "1", "0|1|2|3", RESTART}, {"WB_LAZY", WB_LAZY_DEFAULT, "0|1", RESTART}, {"HEAP_LAZY", "0", "0|1", RESTART}, {"WB_LAZY_TOUCH", "0", "0..16", RESTART}, {"WB_LAZY_CHUNK", "0", "0|1", RESTART}, {"FLUSH_UPLOAD", "0", "0|1", RESTART}, {"GPA_TWIN_MAX_KB", "0", "0..1048576", RESTART}, - {"MIRROR_CAPTURE_KB", "0", "0..1048576", RESTART}, // vk_memory.c capture_cap + {"MIRROR_CAPTURE_KB", "0", "0..1048576", RESTART}, {"WB_SKIP_MB", "0", "0..4096", RESTART}, // vk_memory.c capture_cap, wb_skip {"WB_MERGE_MAX", "32", "1..256", RESTART}, {"TEXUP_LOG_MS", "0", "0..60000", RESTART}, // vk_texture.c vk_texstat_frame {"WAIT_WATCH_S", "10", "0..3600", RESTART}, // kernel.c wait_watch: a lock or thread-end wait this long is logged {"VMA_BLOCK_MB", "64", "16..256", RESTART}, // vk_headroom.c vk_vma_block_size diff --git a/runtime/vk_memory.c b/runtime/vk_memory.c index a5087569..326418b4 100644 --- a/runtime/vk_memory.c +++ b/runtime/vk_memory.c @@ -69,6 +69,7 @@ typedef struct PoolRec { // that synced it, in frame pm_frame and in the one before (NX2NAVI_PRESNAP_PLACE) uint32_t wb_sub; // replay: the last submission that wrote mirror bytes back to guest memory uint64_t wb_fence; // replay: that submission's fence (mirror_capture waits for no write-back) + uint8_t wb_kept; // replay: a write-back left bytes in the mirror alone (wb_skip): never evicted uint32_t retry_sub; // replay: a mirror refused for the VRAM budget or a failed allocation is asked // again from here, 0 never uint8_t refused_why; // replay: the MR_* reason of its refusal, for the counts when it is asked again @@ -2203,6 +2204,7 @@ static uint64_t mirror_evict(uint64_t need, uint32_t min_idle, const char *why) uint32_t age = msub - r->acc_sub; if (age < min_idle || r->wb_sub == msub || msub - r->wb_sub < 2) continue; if (!r->twin && r->gpa_rec) continue; // the table's writes are in the mirror alone + if (r->wb_kept) continue; // so are the bytes wb_skip kept from guest memory int at = nv; while (at > 0 && msub - v[at - 1]->acc_sub < age) at--; if (at == MAXV) continue; @@ -3101,9 +3103,38 @@ static int wb_lazy(void) { static void gpu_wrote_range(const VkMemSpan *sp, uint64_t size, int shader); +// NX2NAVI_WB_SKIP_MB (default 0, off), a test switch for drivers that run without the lazy write-back (AMD's, and +// outside Windows: wb_lazy_choice.h): there, what a binding written by a shader or a copy holds goes back to guest +// memory at the end of every submission, up to 66 MiB a frame in Hyrule Field. With it, a written range of at least +// this many MiB goes back only for its first WB_SKIP_HEAD bytes and the rest stays in the mirror. The CPU reads a few +// words at the start of such buffers (12 bytes into ranges of 6.3 and 15.2 MiB, see "Lazy write-back" in memory.c), +// which the head still carries; a CPU read further in would see older bytes, and a CPU write further in would have the +// next sync copy that page's older guest bytes over what the GPU wrote there. The pool's mirror is never evicted from +// then on, since guest memory lacks those bytes. +#define WB_SKIP_HEAD (64u << 10) +static uint64_t wb_skip(void) { + static int64_t at = -1; + if (at < 0) { + int mb = cfg_int("NX2NAVI_WB_SKIP_MB"); + at = mb > 0 ? (int64_t)mb << 20 : 0; + if (at) + rt_log("mem: without the lazy write-back, written ranges of %d MiB or more go back to guest memory only for " + "their first %u KiB (NX2NAVI_WB_SKIP_MB)", mb, WB_SKIP_HEAD >> 10); + } + return (uint64_t)at; +} +static int64_t m_wb_skipped; // bytes wb_skip kept in mirrors since the last summary + static void gpu_wrote(const VkMemSpan *sp, uint64_t size, int shader) { PoolRec *r = sp->pool; if (!r || r->mstate != MS_ON || sp->buffer != r->mbuf || !size || !wb_lazy()) { + uint64_t skip = r && r->mstate == MS_ON && sp->buffer == r->mbuf ? wb_skip() : 0; + if (skip && size >= skip && !wb_lazy()) { + gpu_wrote_range(sp, WB_SKIP_HEAD, shader); + r->wb_kept = 1; + plat_add64(&m_wb_skipped, (int64_t)(size - WB_SKIP_HEAD)); + return; + } gpu_wrote_range(sp, size, shader); return; } @@ -3708,10 +3739,15 @@ static void mirror_log(uint64_t frame) { a.presnap_queries / f, a.presnap_bytes / f / M, a.sync_queries / f, a.prologues / f, qpc_ms(a.presnap_t + a.sync_t) / f, qpc_ms(a.presnap_t) / f, qpc_ms(a.sync_t) / f, (long long)m_evicted_n, m_evicted_bytes / M, (long long)m_retries, (long long)m_untwinned_n, m_untwinned_bytes / M, ver); - char capl[160] = ""; + char capl[240] = ""; + if (wb_skip()) { + snprintf(capl, sizeof capl, "; written ranges kept in the mirror alone %.3f MiB", + (uint64_t)plat_xchg64(&m_wb_skipped, 0) / f / M); + } if (capture_cap()) - snprintf(capl, sizeof capl, "; copied by the CPU at hand-over %.3f MiB in %.1f ranges, %.3f ms; left to the GPU " - "copy %.3f MiB", a.cap_bytes / f / M, a.cap_ranges / f, qpc_ms(a.cap_t) / f, a.cap_left / f / M); + snprintf(capl + strlen(capl), sizeof capl - strlen(capl), "; copied by the CPU at hand-over %.3f MiB in %.1f " + "ranges, %.3f ms; left to the GPU copy %.3f MiB", a.cap_bytes / f / M, a.cap_ranges / f, + qpc_ms(a.cap_t) / f, a.cap_left / f / M); rt_log("mem: mirror frame %llu: upload sources per frame, overlaps counted twice: written pages %.3f MiB; flush ranges " "%.2f, %.3f MiB, from %.1f flushes of %.3f MiB; flushed and not copied (write-watched) %.3f MiB%s", (unsigned long long)frame, a.page_bytes / f / M, a.flush_n / f,