diff --git a/docs/architecture.md b/docs/architecture.md index 8243d2a0..662fe91b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -260,6 +260,13 @@ keeps per-core data without locks and relies on that, so the host models it (`ru the holes of fuller ones, a few per frame and only in frames with time to spare; which blocks is decided from a table of what lies in them (`vk_tex_compact.h`), and the old image of a moved texture stays until no descriptor set and no submission can still name it (`vk_texture.c`, "the pass"). +- Memory report: beside each `mem: holes` line, `mem: by owner` splits the device-local memory the driver counts into + VMA's allocations by the file that made them (`vk_vma_budget.c`, "owners"; the textures further by the texture + cache's census), the free space inside VMA's blocks and what VMA never allocated (pipelines, descriptor memory, the + swapchain, DLSS and frame generation features); `mem: host` gives the host-visible allocations, the process's + commit and working set and the game's heap; `vk: released textures` how often released textures came back and how + old they were, and how many were made again after a drop (`vk_memory.c` `owner_log`). A figure that only climbs over + a session is a leak. `vram_use` (`cfg.c` `VRAMLOW`) trades these caches for work done again. - Allocation: Vulkan Memory Allocator. Debugging: Vulkan validation layers, per-pass frame dumps (`NX2NAVI_DUMP`, `vk_dump.c`), a sampling profiler (`NX2NAVI_PROFILE`, `tools/prof_report.py`) and a flight recorder (`NX2NAVI_FLIGHT`). RenderDoc cannot capture, because it hides `VK_EXT_external_memory_host` from the application. diff --git a/nx2navi.ini.example b/nx2navi.ini.example index 65da0cea..bbea2a82 100644 --- a/nx2navi.ini.example +++ b/nx2navi.ini.example @@ -540,6 +540,14 @@ # cpu or astc_bc = 0) and the budget is under 20480 MiB, auto goes one step lower, as they take four times the memory. # The log says what was picked ("texture detail:"). Read at start. # texture_detail = auto +# Video memory use (the Screen tab's row): low, balanced or high. balanced, the default, frees what DLSS, frame +# generation's HUD-less picture, FSR 1 and SMAA held once they have been switched off for a while (about 0.9 GiB with +# DLSS off at 5120x2142); high keeps it, so switching them on again makes nothing. low also sets, unless set here: +# tex_released_mb = 1024 (released textures keep at most 1 GiB instead of a quarter of the video memory budget; one +# the game takes back after its drop is made and uploaded again and shows its stand-in for a frame), +# tex_vram_pressure = 70 (textures give way from 70% of the budget) and pipe_seed_boot = 0 (0.3-0.6 GiB of video +# memory and 2-4 GB of commit less; a first visit to an area can miss a few objects for a frame). Read at start. +# vram_use = balanced # Sun shadow map size: game (the game's own, which changes with the view and drops to about 400 near the ground, so # leaf shadows look blocky and jump) or a fixed 256, 512, 768, 1024, 1536 or 2048. Above 960 the texture heap grows with # it. 256 is coarser than the game's own and is for integrated GPUs short of memory bandwidth. Read at start. diff --git a/runtime/cfg.c b/runtime/cfg.c index 981b2850..4e73b784 100644 --- a/runtime/cfg.c +++ b/runtime/cfg.c @@ -79,7 +79,7 @@ static const char *const known[] = { "UPLOAD_BATCH_MB", "VRAM_HEAP_LIMIT", "VRAM_RESERVE_MB", "TEX_CLOBBER", "LOW_END_GPU", "ZSTD_CHECK", "ZSTD_NATIVE", "BIND_REUSE", "DYN_DELTA", "STG_ARENA", "SUBMIT_BATCH", "SUBMIT_THREAD", "ASTC_GPU_DESWIZZLE", "ASTC_SCRATCH_KEEP", "ASTC_ARENA", "ASTC_BC1", - "TEXUP_LOG_MS", "DYN_RES", "DYN_RES_TARGET", "DYN_RES_STEP", "TEXTURE_DETAIL", + "TEXUP_LOG_MS", "DYN_RES", "DYN_RES_TARGET", "DYN_RES_STEP", "TEXTURE_DETAIL", "VRAM_USE", "RECORDER_CORE", "RECORDER_PRIO", "REPLAY_TIME", "LOCK_TIME", "POOL_CREATE_BUDGET", "POOL_CREATE_MS", "POOL_FINI_IDLE", "POOL_LOAD_BACKLOG", "POOL_LOAD_MS", "VMA_BLOCK_MB", "SYNC_WAKE_ALL", "LOG_ASYNC", "DEVICE_LOST_AT", "STAGING_FAIL_AT", "VRAM_BALLAST_MB", "TEX_COMPACT", "TEX_COMPACT_MIN_MB", "TEX_COMPACT_FRAME_MB", // texture compaction (vk_texture.c); no menu row @@ -106,7 +106,7 @@ static const struct { const char *key, *section; } sections[] = { {"FULLSCREEN", "display"}, {"WINDOW_SIZE", "display"}, {"PERF_OVERLAY", "display"}, {"VK_PRESENT", "display"}, {"GPU_INDEX", "display"}, {"MONITOR", "display"}, {"OUTPUT_RES", "display"}, {"GAME_ASPECT", "display"}, {"GAME_RES", "display"}, {"RENDER_SCALE", "display"}, {"UPSCALER", "display"}, {"SHARPNESS", "display"}, {"STANDIN_FILTER", "display"}, {"ASPECT", "display"}, {"FOV", "display"}, {"ANISO", "display"}, - {"TEXTURE_DETAIL", "display"}, + {"TEXTURE_DETAIL", "display"}, {"VRAM_USE", "display"}, {"COLOR_PRESET", "display"}, {"COLOR_BRIGHTNESS", "display"}, {"COLOR_CONTRAST", "display"}, {"COLOR_SATURATION", "display"}, {"COLOR_GAMMA", "display"}, {"HDR", "display"}, {"HDR_LIVE", "display"}, {"HDR_PEAK_NITS", "display"}, {"HDR_GAME_NITS", "display"}, @@ -208,6 +208,10 @@ static const CfgMeta metas[] = { {"ASPECT", "fit", "fit|stretch", LIVE}, {"FOV", "100", "70..140", LIVE}, {"ANISO", "16", "game|2|4|8|16", LIVE}, // vk_texture.c sampler_features, vk_sampler_aniso_live; 16: the game asks 1x or 2x {"TEXTURE_DETAIL", "auto", "auto|full|reduced|low", RESTART}, // vk_texture.c "texture detail", read at the first texture + // video memory use: low sets the VRAMLOW defaults below (vk_texture.c released_cap_bytes and sweep_pressure, + // vk_pipeline.c seed_boot); high keeps what features switched off hold (cfg_vram_keep: vk_dlaa.c idle_release, + // vk_fsr.c idle_note, vk_aa.c work_idle) + {"VRAM_USE", "balanced", "low|balanced|high", RESTART}, // colour adjustment (vk_color.c); the preset is the menu's, the four values are what the present uses {"COLOR_PRESET", "default", "default|vivid|clarity|custom", LIVE}, {"COLOR_BRIGHTNESS", "0", "-30..30", LIVE}, {"COLOR_CONTRAST", "100", "70..150", LIVE}, {"COLOR_SATURATION", "100", "50..200", LIVE}, @@ -642,6 +646,33 @@ static const char *lowend_value(const char *key) { return NULL; } +// vram_use = low (NX2NAVI_VRAM_USE, the Screen tab's Video memory use row): defaults for the keys the player does not +// set, after the device profile and the low-end mode. Each trades memory for work the game then does again: +// - TEX_RELEASED_MB 1024: released textures keep at most 1 GiB (by default a quarter of the video memory budget, 4 GiB +// on a 32 GB card, 1.9 GiB on an 8 GB one). One the game takes back after its drop is made and uploaded or decoded +// again, and its first frame shows the stand-in (vk_texture.c released_cap_bytes). +// - TEX_VRAM_PRESSURE 70: textures that come back intact give way from 70% of the budget on, not 80% (sweep_pressure). +// - PIPE_SEED_BOOT 0: the shared list's pipelines this install never drew are made at their first draw, not at the +// start, which leaves out 0.3-0.6 GiB of video memory and 2-4 GB of commit; a first visit to an area can miss a few +// objects for a frame or two while they are made (vk_pipeline.c seed_boot). +// Unsampled live textures are not dropped in any of the three (tex_disuse_evict stays 0): that eviction left the +// ground flat and see-through for good (vk_texture.c disuse_evict_on). high keeps what a switched-off feature holds +// (cfg_vram_keep), as before balanced, the default, frees it. +static const struct { const char *key, *value; } VRAMLOW[] = { + {"TEX_RELEASED_MB", "1024"}, {"TEX_VRAM_PRESSURE", "70"}, {"PIPE_SEED_BOOT", "0"}, +}; +static const char *vram_use_value(const char *key) { + const char *set = raw_get(PREFIX "VRAM_USE"); + if (!set || strcmp(set, "low")) return NULL; + for (size_t i = 0; i < sizeof VRAMLOW / sizeof *VRAMLOW; i++) + if (!strcasecmp(VRAMLOW[i].key, key)) return VRAMLOW[i].value; + return NULL; +} +int cfg_vram_keep(void) { + const char *set = cfg_get(PREFIX "VRAM_USE"); + return set && !strcmp(set, "high"); +} + static int profile_of(void) { int p = __atomic_load_n(&profile, __ATOMIC_ACQUIRE); if (p >= 0) return p; @@ -672,7 +703,8 @@ static const char *cfg_get_now(const char *name) { if (profile_of()) for (size_t i = 0; i < sizeof DECK / sizeof *DECK; i++) if (!strcasecmp(DECK[i].key, name + PREFIX_LEN)) return DECK[i].value; - return lowend_value(name + PREFIX_LEN); + const char *low = lowend_value(name + PREFIX_LEN); + return low ? low : vram_use_value(name + PREFIX_LEN); } // Whether name's value comes from the player (the environment or nx2navi.ini), not a profile or the table. @@ -774,6 +806,7 @@ const char *cfg_default(const char *key) { if (!strcasecmp(DECK[i].key, name)) return DECK[i].value; const char *low = lowend_value(name); if (low) return low; + if ((low = vram_use_value(name))) return low; const CfgMeta *m = cfg_meta(key); return m ? m->def : NULL; } diff --git a/runtime/cfg.h b/runtime/cfg.h index 75fa8db8..049aa3da 100644 --- a/runtime/cfg.h +++ b/runtime/cfg.h @@ -34,6 +34,8 @@ int cfg_ini_read(const char *path, const char *key, char *out, size_t cap); // // 1 when the player set name (the environment or nx2navi.ini), 0 when cfg_get gives a device profile's default or NULL. int cfg_is_set(const char *name); int cfg_low_end_active(void); // the low-end GPU mode was on when the game started (cfg.c LOWEND) +// vram_use = high: what a feature switched off holds stays for the run (cfg.c VRAMLOW, the comment above it) +int cfg_vram_keep(void); // The keys of the file's lines that are no settings of this version, lower-case with ", " between them ("" when there // are none; a list too long to keep ends early), and how many there are. Such a line does nothing: the log says so at // start and the settings menu shows it (ui/menu.cpp), so that nobody takes one for the cause of what they see. diff --git a/runtime/ui/menu.cpp b/runtime/ui/menu.cpp index 00c5f6f9..64bf3c71 100644 --- a/runtime/ui/menu.cpp +++ b/runtime/ui/menu.cpp @@ -766,6 +766,15 @@ static void row_texture_detail(void) { opt_choice_list("NX2NAVI_TEXTURE_DETAIL", UI_MENU_TEX_DETAIL, vals, names, 4, UI_MENU_TEX_DETAIL_DESC); } +// Video memory use (cfg.c VRAMLOW, cfg_vram_keep): low sets smaller texture caches and builds the shared pipeline list +// at first use, high keeps what switched-off features made. Read at the start, so the row notes the restart. +static void row_vram_use(void) { + static const char *const vals[] = {"low", "balanced", "high"}; + const char *names[] = {menu_text(UI_MENU_VRAM_USE_LOW), menu_text(UI_MENU_VRAM_USE_BALANCED), + menu_text(UI_MENU_VRAM_USE_HIGH)}; + opt_choice_list("NX2NAVI_VRAM_USE", UI_MENU_VRAM_USE, vals, names, 3, UI_MENU_VRAM_USE_DESC); +} + static void row_shadow_scale(void) { static const char *const vals[] = {"1", "2", "3", "4"}; static const char *const names[] = {"960 (1x)", "1920 (2x)", "2880 (3x)", "3840 (4x)"}; @@ -1320,6 +1329,7 @@ static void tab_screen(float scale) { const char *aniso_names[] = {menu_text(UI_MENU_ANISO_GAME), "2x", "4x", "8x", "16x"}; opt_choice_list("NX2NAVI_ANISO", UI_MENU_ANISO, aniso, aniso_names, 5, UI_MENU_ANISO_DESC); row_texture_detail(); + row_vram_use(); opt_end(); } diff --git a/runtime/ui_text.c b/runtime/ui_text.c index 1f261615..5fc5a30e 100644 --- a/runtime/ui_text.c +++ b/runtime/ui_text.c @@ -98,6 +98,13 @@ static const char *const text_en[UI_TEXT_COUNT] = { [UI_MENU_TEX_DETAIL_DESC] = "Sharpness of large surface textures. Reduced and Low leave out their largest versions, so that an area fits the video memory of 6 and 8 GB cards; only surfaces seen up close look softer. Automatic picks by the card's video memory.", [UI_MENU_TEX_DETAIL_AUTO] = "Automatic: %s", [UI_MENU_TEX_DETAIL_FULL] = "Full", [UI_MENU_TEX_DETAIL_REDUCED] = "Reduced", [UI_MENU_TEX_DETAIL_LOW] = "Low", + [UI_MENU_VRAM_USE] = "Video memory use", + [UI_MENU_VRAM_USE_DESC] = "How much video memory the game keeps for later. Low keeps fewer textures of places you " + "left and builds some graphics only when first needed: returning to a place can show " + "simpler surfaces for a moment, and a first visit can miss a few objects for a frame. High " + "also keeps what an upscaler or anti-aliasing switched off had made. Takes effect after a " + "restart.", + [UI_MENU_VRAM_USE_LOW] = "Low", [UI_MENU_VRAM_USE_BALANCED] = "Balanced", [UI_MENU_VRAM_USE_HIGH] = "High", [UI_MENU_LOWEND_INTRO] = "The settings of the low-end GPU mode. Each can be changed here; most take effect after a restart.", [UI_MENU_LOWEND_RESTART] = "The mode turns on after a restart. Its settings are listed here then.", [UI_MENU_LOWEND_NOT_FOR_CARD] = "Not for this graphics card: it has mesh shaders and 8 GB of video memory or more. " @@ -916,6 +923,13 @@ static const char *const text_ru[UI_TEXT_COUNT] = { [UI_MENU_TEX_DETAIL_DESC] = "Чёткость больших текстур поверхностей. «Сниженная» и «Низкая» не загружают их самые крупные версии, чтобы область помещалась в видеопамять карт на 6 и 8 ГБ; мягче выглядят только поверхности вблизи. «Авто» выбирает по объёму видеопамяти.", [UI_MENU_TEX_DETAIL_AUTO] = "Авто: %s", [UI_MENU_TEX_DETAIL_FULL] = "Полная", [UI_MENU_TEX_DETAIL_REDUCED] = "Сниженная", [UI_MENU_TEX_DETAIL_LOW] = "Низкая", + [UI_MENU_VRAM_USE] = "Расход видеопамяти", + [UI_MENU_VRAM_USE_DESC] = "Сколько видеопамяти игра держит про запас. «Низкий» хранит меньше текстур покинутых мест и " + "создаёт часть графики только при первой надобности: при возвращении поверхности могут на " + "миг выглядеть проще, а при первом посещении на кадр могут пропасть несколько объектов. " + "«Высокий» также хранит то, что создали отключённые масштабирование или сглаживание. " + "Действует после перезапуска.", + [UI_MENU_VRAM_USE_LOW] = "Низкий", [UI_MENU_VRAM_USE_BALANCED] = "Сбалансированный", [UI_MENU_VRAM_USE_HIGH] = "Высокий", [UI_MENU_SHARPNESS] = "Резкость FSR", [UI_MENU_SHARPNESS_DESC] = "Насколько FSR повышает резкость. 0 отключает.", [UI_MENU_SHARPNESS_NEEDS_FSR] = "только с AMD FSR 1", @@ -1887,6 +1901,13 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = { "da placa.", [UI_MENU_TEX_DETAIL_AUTO] = "Automático: %s", [UI_MENU_TEX_DETAIL_FULL] = "Completo", [UI_MENU_TEX_DETAIL_REDUCED] = "Reduzido", [UI_MENU_TEX_DETAIL_LOW] = "Baixo", + [UI_MENU_VRAM_USE] = "Uso da memória de vídeo", + [UI_MENU_VRAM_USE_DESC] = "Quanta memória de vídeo o jogo guarda para depois. Baixo guarda menos texturas dos " + "lugares que você deixou e cria parte dos gráficos só quando são precisos pela primeira " + "vez: ao voltar a um lugar, as superfícies podem parecer mais simples por um instante, e " + "numa primeira visita alguns objetos podem faltar por um quadro. Alto também guarda o que " + "um escalonador ou antisserrilhamento desligado criou. Vale depois de reiniciar.", + [UI_MENU_VRAM_USE_LOW] = "Baixo", [UI_MENU_VRAM_USE_BALANCED] = "Equilibrado", [UI_MENU_VRAM_USE_HIGH] = "Alto", [UI_MENU_COLOR] = "Cor", [UI_MENU_COLOR_PRESET] = "Perfil de cor", [UI_MENU_COLOR_PRESET_DESC] = "Define os quatro valores abaixo. Vívido: mais contraste e saturação, sombras um " @@ -2719,6 +2740,10 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = { [UI_MENU_TEX_DETAIL_DESC] = "大型表面纹理的清晰度。“降低”和“低”不加载它们最大的版本,使一个区域能放进 6 GB 和 8 GB 显卡的显存;只有近处的表面看起来略微柔和。“自动”根据显卡的显存选择。", [UI_MENU_TEX_DETAIL_AUTO] = "自动:%s", [UI_MENU_TEX_DETAIL_FULL] = "完整", [UI_MENU_TEX_DETAIL_REDUCED] = "降低", [UI_MENU_TEX_DETAIL_LOW] = "低", + [UI_MENU_VRAM_USE] = "显存占用", + [UI_MENU_VRAM_USE_DESC] = "游戏为之后保留多少显存。“低”保留较少已离开地点的纹理,部分图形在首次需要时才创建:回到某地时表面可能" + "短暂显得较简单,首次到访时可能有几个物体缺失一帧。“高”还会保留已关闭的放大或抗锯齿创建的内容。重启后生效。", + [UI_MENU_VRAM_USE_LOW] = "低", [UI_MENU_VRAM_USE_BALANCED] = "平衡", [UI_MENU_VRAM_USE_HIGH] = "高", [UI_MENU_HDR_GROUP] = "HDR", [UI_MENU_HDR] = "HDR 输出", [UI_MENU_HDR_DESC] = "在 HDR 显示器上呈现更亮的高光。请先在 Windows 中开启 HDR。", diff --git a/runtime/ui_text.h b/runtime/ui_text.h index 049492d9..d2d41995 100644 --- a/runtime/ui_text.h +++ b/runtime/ui_text.h @@ -50,6 +50,7 @@ typedef enum { UI_MENU_TEX_DETAIL, UI_MENU_TEX_DETAIL_DESC, UI_MENU_TEX_DETAIL_AUTO, // "Automatic: %s", %s the level it picks on this card UI_MENU_TEX_DETAIL_FULL, UI_MENU_TEX_DETAIL_REDUCED, UI_MENU_TEX_DETAIL_LOW, + UI_MENU_VRAM_USE, UI_MENU_VRAM_USE_DESC, UI_MENU_VRAM_USE_LOW, UI_MENU_VRAM_USE_BALANCED, UI_MENU_VRAM_USE_HIGH, UI_MENU_SHARPNESS, UI_MENU_SHARPNESS_DESC, UI_MENU_SHARPNESS_NEEDS_FSR, UI_MENU_COLOR, // group heading over the colour rows UI_MENU_COLOR_PRESET, UI_MENU_COLOR_PRESET_DESC, UI_MENU_COLOR_DEFAULT, UI_MENU_COLOR_VIVID, UI_MENU_COLOR_CLARITY, diff --git a/runtime/vk_aa.c b/runtime/vk_aa.c index 84b43b49..bdb13006 100644 --- a/runtime/vk_aa.c +++ b/runtime/vk_aa.c @@ -149,7 +149,7 @@ static void img_free(Img *m); // long pause or loading screen with smaa set also counts as unused, and the images are made again after it. #define WORK_IDLE 3600 static void work_idle(uint64_t frame) { - if (!P.luma.img || frame - P.smaa_frame < WORK_IDLE) return; + if (!P.luma.img || frame - P.smaa_frame < WORK_IDLE || cfg_vram_keep()) return; // vram_use = high keeps them rt_log("aa: SMAA's work images unused for %d frames: freed (%ux%u)", WORK_IDLE, P.w, P.h); img_free(&P.luma), img_free(&P.edges), img_free(&P.weights); P.w = P.h = 0; diff --git a/runtime/vk_dlaa.c b/runtime/vk_dlaa.c index 814323c9..50685936 100644 --- a/runtime/vk_dlaa.c +++ b/runtime/vk_dlaa.c @@ -2707,7 +2707,7 @@ int vk_dlaa_resized_take(void) { #define IDLE_FREE_PRESENTS 600 static uint64_t off_at; // D.presents when DLSS went off static void idle_release(void) { - if (!idle_free()) return; + if (!idle_free() || cfg_vram_keep()) return; // vram_use = high keeps them if (D.hl && D.presents - D.hl_at >= IDLE_FREE_PRESENTS && !D.hl_fresh) { rt_log("dlaa: the HUD-less picture for frame generation went unused for %d presents: freed", IDLE_FREE_PRESENTS); hl_destroy(); diff --git a/runtime/vk_fsr.c b/runtime/vk_fsr.c index 73fcd3e3..09b9b18e 100644 --- a/runtime/vk_fsr.c +++ b/runtime/vk_fsr.c @@ -51,7 +51,7 @@ static struct { #define IDLE_FREE 600 static void free_images(VkCtx *c); static void idle_note(unsigned slot) { - if (!F.img[0]) return; + if (!F.img[0] || cfg_vram_keep()) return; // vram_use = high keeps them F.since |= 1u << (slot % SLOTS); if (++F.idle < IDLE_FREE || (F.since & F.used_slots) != F.used_slots) return; rt_log("vk: FSR's images unused for %u presents: freed (%ux%u)", F.idle, F.w, F.h);