diff --git a/nx2navi.ini.example b/nx2navi.ini.example index 83ecc493..b3d5514f 100644 --- a/nx2navi.ini.example +++ b/nx2navi.ini.example @@ -420,6 +420,11 @@ # instead of by the thread that draws alone (the same bytes, 2 to 3 times sooner). 0: one thread, as before 0.10. # Read at start. # par_jobs = 1 +# While the video memory in use is over tex_vram_pressure percent of the budget, the pipelines the game built ahead +# of play and no draw has used yet are given back, a few hundred every 5 seconds: on a card short of video memory they +# are what costs least to lose, and a draw that needs one later builds it again from the pipeline cache. 0: every +# pipeline stays for the run, as before 0.10. Read at start. +# pipe_release = 1 # Low-end GPU mode (experimental; also in the settings menu's Game tab). For older graphics cards, and ones short of # video memory: 1 turns on the settings written for them and measured on a Radeon RX 580, for every key below that # this file does not set itself. Mesh shaders run as compute where the card has none (mesh_emulation), video memory diff --git a/runtime/cfg.c b/runtime/cfg.c index bb6c2ab6..cdf4f42f 100644 --- a/runtime/cfg.c +++ b/runtime/cfg.c @@ -45,7 +45,7 @@ static const char *const known[] = { "COLOR_PRESET", "COLOR_SATURATION", "CONTACT_DEBUG", "CONTACT_SHADOWS", "CORE_DIAG", "CRIT", "CRIT_OUT", "DMABUF", "DMABUF_MB", "DRAW_CRUMBS", "GPA_LATE", "DESC_TEMPLATE", "DEVICE_PROFILE", "DISCORD_APP_ID", "DISCORD_IMAGE", "DISCORD_RPC", "DLAA", "DLAA_CAM_KEEP", "DLAA_CUT", "DLSS_IDLE_FREE", "DLAA_EDGE_FILL", "DLAA_AT_UPSCALER", "FFX_DEBUG", "FFX_KEYS", "FSR3_SHARPEN", "DLAA_JITTER", "DLAA_MV", "DLAA_MASKDEBUG", "DLAA_MVDEBUG", "DLAA_OBJMV", "DLAA_SURVEY", "DLAA_TARGET_KEEP", "DLAA_TIMES", "DLSS", "DLSS_CHAR_MASK", "DLSS_PRESET", "DLSS_PREBLOOM", "DLSS_RANGE", "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", "GPU_TIMELINE", - "FFX_PIPE_CACHE", "FLIGHT_SPIKE_MS", "STUTTER_MAP", "PAR_JOBS", "FLIGHT_THUMBS", "FLIGHT_EVERY", "FLIGHT_FLASH", "FLIGHT_FLASH_DIFF", "FLIGHT_FLASH_CELLS", "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", + "FFX_PIPE_CACHE", "FLIGHT_SPIKE_MS", "STUTTER_MAP", "PAR_JOBS", "PIPE_RELEASE", "FLIGHT_THUMBS", "FLIGHT_EVERY", "FLIGHT_FLASH", "FLIGHT_FLASH_DIFF", "FLIGHT_FLASH_CELLS", "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", "WB_SKIP_MB", "GPL", "GPL_DRAW_BUILD", "GPL_FEEDBACK", "GPL_MESH", "PIPE_AHEAD", "PIPE_FAST_WORKERS", "PIPE_FRAME_WAIT_MS", "PIPE_LINK_DRAW_MS", "PIPE_SKIP_STEADY", "DRAW_WRITERS_WAIT", "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 @@ -160,7 +160,7 @@ static const struct { const char *key, *section; } sections[] = { {"PUSH_SKIP", "performance"}, {"SPLIT_DRAWS", "performance"}, {"SPLIT_IDLE", "performance"}, {"SPLIT_IN_PASS", "performance"}, {"SPLIT_US", "performance"}, {"VK_VALIDATION", "performance"}, {"VK_DYN_VERTEX", "performance"}, {"VK_DYN_A2C", "performance"}, {"VK_SUBGROUP_PIN", "performance"}, {"VK_RECONVERGE", "performance"}, {"VK_NAN_PRESERVE", "performance"}, {"VK_NAN_BITS", "performance"}, - {"DRAW_CRUMBS", "diagnostics"}, {"TRACE", "diagnostics"}, {"FLIGHT", "diagnostics"}, {"PASS_CRUMBS", "diagnostics"}, {"FLIGHT_SPIKE_MS", "diagnostics"}, {"STUTTER_MAP", "diagnostics"}, {"PAR_JOBS", "performance"}, + {"DRAW_CRUMBS", "diagnostics"}, {"TRACE", "diagnostics"}, {"FLIGHT", "diagnostics"}, {"PASS_CRUMBS", "diagnostics"}, {"FLIGHT_SPIKE_MS", "diagnostics"}, {"STUTTER_MAP", "diagnostics"}, {"PAR_JOBS", "performance"}, {"PIPE_RELEASE", "performance"}, {"FLIGHT_FLASH", "diagnostics"}, {"FLIGHT_FLASH_DIFF", "diagnostics"}, {"FLIGHT_FLASH_CELLS", "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"}, @@ -414,6 +414,8 @@ static const CfgMeta metas[] = { {"STUTTER_MAP", "0", "0|1", RESTART}, // par_jobs.c: the worker pool that splits a large texture's copy out of the game's memory (0: the thread that makes it alone) {"PAR_JOBS", "1", "0|1", RESTART}, + // vk_pipeline.c "pipelines no draw has used, given back while video memory is short" (0: every pipeline stays) + {"PIPE_RELEASE", "1", "0|1", RESTART}, // flight.c "The flash trigger": on or off, and flight_flash.h flash_test's two thresholds {"FLIGHT_FLASH", "1", "0|1", RESTART}, {"FLIGHT_FLASH_DIFF", "24", "4..255", RESTART}, {"FLIGHT_FLASH_CELLS", "3", "1..144", 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}, diff --git a/runtime/vk_pipeline.c b/runtime/vk_pipeline.c index bbe62ad2..93d8d69a 100644 --- a/runtime/vk_pipeline.c +++ b/runtime/vk_pipeline.c @@ -5022,6 +5022,85 @@ static void fast_retire(Pipe *p) { fast_retired++; } +// ---- pipelines no draw has used, given back while video memory is short +// The boot builds the recorded pipelines ahead of play (the prewarm and the shipped pipeline list), and the driver keeps +// each one's compiled code in video memory until it is destroyed. In a 4K session with DLSS and frame generation the +// device-local memory outside the allocator was 4.2 GB with 6,300 graphics pipelines alive, 3,589 of them not drawn +// once in that run. On a card short of video memory those are the cheapest thing to give back: no draw has recorded +// them. So while the device-local memory in use is over NX2NAVI_TEX_VRAM_PRESSURE percent of the budget (vk_mem_vram, +// which NX2NAVI_VRAM_BUDGET_MB caps for a test), the autosave thread takes up to RELEASE_BATCH graphics pipelines that +// no draw has looked up in this run out of the table every RELEASE_EVERY_MS, and destroys them with their entries once +// the GPU is past the submission being recorded and one more (as fast_retire). A draw that needs one later misses it as +// it would a pipeline never built (pipe_miss): its stage libraries link it, or the look-ahead queued them +// (vk_pipeline_hint), and its optimised build comes from the pipeline cache, which still holds it. +// Only entries no lookup reached go (used_min 0; pipe_minute is set before the first lookup, ensure_init), so no draw's +// memo (vk_exec_draw.c, the PM context) nor any caller holds their VkPipeInfo. Not taken: mesh pipelines (without +// stage libraries their build is long), entries building or with a link queued, failed ones, and compute pipelines. +// The next pass logs the device-local use then and now, so a log says what a batch gave back. +// NX2NAVI_PIPE_RELEASE=0 keeps every pipeline for the run, as before 0.10.0. +#define RELEASE_EVERY_MS 5000 +#define RELEASE_BATCH 256 +static uint64_t rel_total, rel_batches; +static uint64_t rel_used0; // device-local use (MiB) when the last batch went; 0 none to report + +static void released_destroy_cb(void *arg) { // the completion thread: the entry is out of the table since its batch + Pipe *p = arg; + if (p->info.pipeline) vkDestroyPipeline(P.device, p->info.pipeline, NULL); + free(p->bindings); + free(p); +} + +// Caller holds P.lock. +static int releasable(const Pipe *p) { + if (p->used_min || !p->info.pipeline || p->building || p->job || p->link_queued || p->fast || p->t_miss || !p->lay) + return 0; + if (p->info.bind_point != VK_PIPELINE_BIND_POINT_GRAPHICS || p->key_size != sizeof(GKey)) return 0; + const GKey *k = (const GKey *)p->key; + return k->vs && gfx_shader_stage(k->vs) != GFX_STAGE_MS; +} + +static void pressure_release(uint64_t now) { + static int on = -1, pct; + static uint64_t last; + if (on < 0) { + on = cfg_bool("NX2NAVI_PIPE_RELEASE"); + pct = cfg_int("NX2NAVI_TEX_VRAM_PRESSURE"); + if (pct <= 0 || pct > 100) pct = 80; + if (!on) rt_log("vk_pipeline: every pipeline stays for the run, also while video memory is short (NX2NAVI_PIPE_RELEASE=0)"); + } + if (!on || now - last < RELEASE_EVERY_MS) return; + last = now; + uint64_t used = 0, budget = 0; + int exact = 0; + if (!vk_mem_vram(&used, &budget, &exact) || !budget) return; + if (rel_used0) { + rt_log("vk_pipeline: device-local memory in use %llu MiB when the last batch of unused pipelines went, %llu MiB " + "now", (unsigned long long)rel_used0, (unsigned long long)(used >> 20)); + rel_used0 = 0; + } + if (used * 100 < budget * (uint64_t)pct) return; + Pipe *gone[RELEASE_BATCH]; + int n = 0; + plat_mutex_lock(&P.lock); + for (int b = 0; b < PIPE_BUCKETS && n < RELEASE_BATCH; b++) + for (Pipe **pp = &pipes[b]; *pp && n < RELEASE_BATCH;) { + Pipe *p = *pp; + if (releasable(p)) *pp = p->next, gone[n++] = p; + else pp = &p->next; + } + uint64_t at = vk_sync_issued() + 2; + plat_mutex_unlock(&P.lock); + if (!n) return; + for (int i = 0; i < n; i++) vk_completion_add(at, released_destroy_cb, gone[i]); + rel_total += (uint64_t)n, rel_batches++; + rel_used0 = used >> 20; + if (rel_batches <= 16 || !(rel_batches & (rel_batches - 1))) + rt_log("vk_pipeline: video memory in use %llu of %llu MiB (over %d%%): %d graphics pipelines no draw used in this " + "run given back, %llu in %llu batches so far; a draw that needs one builds it again from the pipeline " + "cache (NX2NAVI_PIPE_RELEASE)", (unsigned long long)(used >> 20), (unsigned long long)(budget >> 20), pct, n, + (unsigned long long)rel_total, (unsigned long long)rel_batches); +} + // The whole build of job j, which the caller took off its queue, and the job's end: its pipeline is entered, the // threads waiting for it are woken and j is freed. lowrun: 1 on a pool worker that took j from the low queue. Caller // holds P.lock, which is released for the build. @@ -8852,6 +8931,7 @@ static void autosave_once(void) { if (!P.ready) return; uint64_t now = plat_now_ms(); __atomic_store_n(&pipe_minute, minute_now(), __ATOMIC_RELAXED); + pressure_release(now); // while video memory is short: the pipelines no draw used go if (now - last_sizes >= SIZES_EVERY_MS && (P.cache || P.play)) last_sizes = now, cache_sizes_ask(); if (now - last_keys >= 2000) { last_keys = now;