diff --git a/docs/README-BuildKit.md b/docs/README-BuildKit.md index ed3affc6..59301a09 100644 --- a/docs/README-BuildKit.md +++ b/docs/README-BuildKit.md @@ -153,7 +153,8 @@ beside the game. a DLSS multiplier of 2x, or 3x and 4x on RTX 50 cards; FSR 3.1 on any card where it loads; it needs DLSS or FSR 3.1 upscaling on, and the frame limit then counts the frames on screen; with no limit chosen it is the display's refresh rate), HDR (the game's picture tone mapped to your display, with its own grading rows, or Native, the game's own HDR; turn HDR on in Windows first) and the - resolution. Those marked "after restart" take effect when the game starts again. + resolution. A setting that takes effect only when the game starts again says "after restart" once you change it, + until you set it back, and the Restart button at the bottom then counts the settings waiting for it. - **Controls** in the menu has Button prompts: the game's own, or Xbox, PlayStation, Steam Deck or keyboard and mouse ones when the build drew them (a kit with `prompt-art`). It takes effect after a restart. The keyboard and mouse prompts show the keys `nx2navi.ini` binds when the build runs: after rebinding keys, run diff --git a/runtime/cfg.c b/runtime/cfg.c index b7edc4e9..8066e17e 100644 --- a/runtime/cfg.c +++ b/runtime/cfg.c @@ -610,6 +610,42 @@ int cfg_int(const char *name) { int cfg_bool(const char *name) { return strcmp(cfg_value(name), "0") != 0; } +// ---- the restart-only settings' values at start (cfg_init), which the running game keeps using whatever the menu sets +// later: cfg_pending compares against them, so the menu says "after restart" only while a restart would change +// something. One per row of metas, NULL for a live one. +static const char *at_start[sizeof metas / sizeof *metas]; + +static const char *meta_value(size_t i) { + char name[80]; + snprintf(name, sizeof name, PREFIX "%s", metas[i].key); + return cfg_value(name); +} + +static void start_values(void) { + for (size_t i = 0; i < sizeof metas / sizeof *metas; i++) + if (!metas[i].live) at_start[i] = strdup(meta_value(i)); // a copy: cfg_load (the probes) frees what it read +} + +// A value the environment sets never waits: the menu cannot change it and a restart keeps the environment. The command +// line's --monitor and --audio-device arrive that way after the start values are taken (main.c). +static int pending_at(size_t i) { + char name[80]; + if (!at_start[i]) return 0; + snprintf(name, sizeof name, PREFIX "%s", metas[i].key); + return !cfg_from_env(name) && strcmp(at_start[i], cfg_value(name)) != 0; +} + +int cfg_pending(const char *key) { + const CfgMeta *m = cfg_meta(key); + return m ? pending_at((size_t)(m - metas)) : 0; +} + +int cfg_pending_count(void) { + int n = 0; + for (size_t i = 0; i < sizeof metas / sizeof *metas; i++) n += pending_at(i); + return n; +} + // ---- cfg_save: the file as lines, each with its line ending typedef struct { const char *s; size_t n; } Line; // n includes the '\n' (and '\r') when present @@ -879,4 +915,5 @@ void cfg_init(void) { if (profile_of()) rt_log("cfg: device profile Steam Deck: defaults for the keys nx2navi.ini does not set (NX2NAVI_DEVICE_PROFILE=desktop " "turns it off)"); + start_values(); } diff --git a/runtime/cfg.h b/runtime/cfg.h index da86b2b9..62dc21ed 100644 --- a/runtime/cfg.h +++ b/runtime/cfg.h @@ -6,7 +6,7 @@ // is kept). A name without the NX2NAVI_ prefix goes to getenv unchanged. Safe from any thread once cfg_init has returned. const char *cfg_get(const char *name); -// Reads nx2navi.ini once, from mem_init after the address space is claimed and before any setting is read. Logs the file, its key count, unknown keys and environment overrides. +// Reads nx2navi.ini once, from mem_init after the address space is claimed and before any setting is read. Logs the file, its key count, unknown keys and environment overrides. Keeps the restart-only settings' values (cfg_pending). void cfg_init(void); // Replaces the loaded keys with those of the file at path (tests). 1 when the file was read, 0 when missing. @@ -55,6 +55,11 @@ const CfgMeta *cfg_meta_at(int i); // What key reads when the player has not set it: the device profile's value (the Steam Deck's), else the row's default; // NULL for a key without a row. The menu's "default" mark follows it. const char *cfg_default(const char *key); +// A restart-only setting (live 0) whose value differs from the one cfg_init read at start, which the running game still +// uses: 1 while a restart would change it, 0 otherwise and for a live key or one without a row (the menu's "after +// restart" note). cfg_pending_count: how many restart-only settings differ. +int cfg_pending(const char *key); +int cfg_pending_count(void); // The setting's value for consumers: the environment or nx2navi.ini value, else the CfgMeta default. name has the NX2NAVI_ // prefix, like cfg_get; a name without a row is a bug (rt_fatal). A row with an empty default has no fixed value: its diff --git a/runtime/cfg_probe.c b/runtime/cfg_probe.c index bf61d250..44c40088 100644 --- a/runtime/cfg_probe.c +++ b/runtime/cfg_probe.c @@ -1,4 +1,5 @@ -// --probe-cfg: nx2navi.ini parsing, the environment override and a missing file (cfg.c). Needs no game files. +// --probe-cfg: nx2navi.ini parsing, the environment override, a missing file and the settings waiting for a restart +// (cfg.c). Needs no game files. #include #include #include @@ -17,6 +18,26 @@ static int fails; #define EQ(a, b) ((a) && !strcmp((a), (b))) int probe_cfg(void) { + // cfg_pending, before this probe's files replace the settings cfg_init read: a restart-only setting counts while its + // value differs from the one at start, a live one never + if (!cfg_from_env("NX2NAVI_MICROPHONE") && !cfg_from_env("NX2NAVI_VOLUME")) { + const char *mic = cfg_value("NX2NAVI_MICROPHONE"); + int base = cfg_pending_count(); + CHECK(!cfg_pending("NX2NAVI_MICROPHONE"), "pending: unchanged since the start"); + cfg_set("NX2NAVI_MICROPHONE", strcmp(mic, "0") ? "0" : "1"); + CHECK(cfg_pending("NX2NAVI_MICROPHONE") && cfg_pending_count() == base + 1, "pending: a restart-only setting changed"); + cfg_set("NX2NAVI_MICROPHONE", mic); + CHECK(!cfg_pending("NX2NAVI_MICROPHONE") && cfg_pending_count() == base, "pending: set back to its start value"); + cfg_set("NX2NAVI_VOLUME", "7"); + CHECK(!cfg_pending("NX2NAVI_VOLUME") && cfg_pending_count() == base, "pending: a live setting never counts"); + // a value the environment sets after the start (the command line's --monitor arrives that way) never waits: + // no row can change it and a restart keeps it + if (!cfg_from_env("NX2NAVI_MONITOR")) { + cfg_setenv("NX2NAVI_MONITOR", "2"); + CHECK(!cfg_pending("NX2NAVI_MONITOR") && cfg_pending_count() == base, "pending: set by the environment after the start"); + cfg_setenv("NX2NAVI_MONITOR", NULL); + } + } char path[1024]; if (!plat_exe_path(path, sizeof path)) return 1; char *cut = plat_last_sep(path); diff --git a/runtime/main.c b/runtime/main.c index ae53d39b..f452a8e5 100644 --- a/runtime/main.c +++ b/runtime/main.c @@ -597,17 +597,18 @@ int main(int argc, char **argv) { if (++i == argc) return usage(argv[0]); *path = argv[i]; } else if (!strcmp(argv[i], "--audio-device")) { - // Same approach as every setting: NX2NAVI_AUDIO_DEVICE is read from the environment first (cfg.c raw_get), and - // nothing has read it yet because the audio renderer opens in rt_boot. An empty value would remove the variable - // (plat_setenv) and hand the choice back to nx2navi.ini, so "default" is the spelling for the system default. + // Same approach as every setting: NX2NAVI_AUDIO_DEVICE is read from the environment first (cfg.c raw_get). + // cfg_init has read the settings by now, so the variable goes in through cfg_setenv, which makes the lookups + // read again. An empty value would remove the variable (plat_setenv) and hand the choice back to + // nx2navi.ini, so "default" is the spelling for the system default. if (++i == argc || !*argv[i]) return usage(argv[0]); - plat_setenv("NX2NAVI_AUDIO_DEVICE", argv[i]); + cfg_setenv("NX2NAVI_AUDIO_DEVICE", argv[i]); } else if (!strcmp(argv[i], "--monitor")) { - // The settings read NX2NAVI_MONITOR from the environment first (cfg.c raw_get), and nothing has read it yet: - // the window opens in rt_boot. An empty value would remove the variable (plat_setenv) and hand the choice back - // to nx2navi.ini, so "auto" is the spelling for "no monitor choice" on the command line. + // The settings read NX2NAVI_MONITOR from the environment first (cfg.c raw_get); cfg_setenv as above. An empty + // value would remove the variable (plat_setenv) and hand the choice back to nx2navi.ini, so "auto" is the + // spelling for "no monitor choice" on the command line. if (++i == argc || !*argv[i]) return usage(argv[0]); - plat_setenv("NX2NAVI_MONITOR", argv[i]); + cfg_setenv("NX2NAVI_MONITOR", argv[i]); } else if (!strcmp(argv[i], "--seconds")) { char *end; if (++i == argc) return usage(argv[0]); diff --git a/runtime/ui/menu.cpp b/runtime/ui/menu.cpp index 48cf9f29..0f8440e0 100644 --- a/runtime/ui/menu.cpp +++ b/runtime/ui/menu.cpp @@ -822,6 +822,10 @@ static void upscaling_name(char *out, size_t size) { else snprintf(out, size, mode ? "DLSS %s" : "NVIDIA %s", menu_text(dlss[mode])); } +// The Upscaling row's NVIDIA DLSS waits for a restart (vk_dlaa_needs_restart), where DLSS is offered: the row's note and +// the Restart button's count (restart_pending). +static bool upscaling_restart(void) { return dlss_offered() && vk_dlaa_needs_restart(); } + static void row_upscaling(void) { static const char *const modes[] = {"dlaa", "quality", "balanced", "performance", "ultra_performance"}; static const UiTextId mode_names[] = {UI_MENU_DLSS_DLAA, UI_MENU_DLSS_QUALITY, UI_MENU_DLSS_BALANCED, @@ -867,7 +871,7 @@ static void row_upscaling(void) { // FSR 3.1 still set where it cannot load: nothing upscales, so the row says so under its value const char *note = nullptr; if (!fsr && now.kind == FSR3) note = menu_text(UI_MENU_FSR3_OFF); - else if (dlss && vk_dlaa_needs_restart()) note = menu_text(UI_MENU_AFTER_RESTART); + else if (upscaling_restart()) note = menu_text(UI_MENU_AFTER_RESTART); int nv = opt_choice_index("NX2NAVI_UPSCALER", UI_MENU_UPSCALING, names, n, cur, 1, !fsr ? UI_MENU_UPSCALING_DESC_NO_FSR : dlss ? UI_MENU_UPSCALING_DESC : UI_MENU_UPSCALING_DESC_FSR, note); @@ -973,6 +977,15 @@ static void row_framegen_mult_ngx(void) { if (nv >= 0 && nv < n) opt_apply("NX2NAVI_FG_MULT", values[nv]); } +// Frame generation to or from NVIDIA's waits for a restart: AMD's follows the setting, NVIDIA's is set up with the device +// (vk_dlssg.c). Through NGX it follows the setting too, once NGX is up, which needs DLSS upscaling on when the game +// starts. The row's note and the Restart button's count (restart_pending). +static bool framegen_restart(void) { + const bool set = !strcmp(cfg_value("NX2NAVI_FRAMEGEN"), "dlss"); + if (!dlss_offered()) return false; + return framegen_ngx() ? set && !vk_ngxfg_wanted() : set != (vk_dlssg_wanted() != 0); +} + // Frame generation (vk_fg.cpp, at once; NVIDIA's after a restart); it needs a temporal upscaler's motion vectors static void row_framegen(void) { // the generated frames are made from DLSS's or FSR 3.1's motion vectors: without one of them it would do nothing, @@ -1004,7 +1017,7 @@ static void row_framegen(void) { // AMD's follows the setting; NVIDIA's is set up with the device, so only a change to or from it waits for a restart. // Through NGX it follows the setting too, once NGX is up, which needs DLSS upscaling on when the game starts. const bool ngx = framegen_ngx(); - const bool restart = ngx ? dlss && cur == 2 && !vk_ngxfg_wanted() : dlss && (cur == 2) != (vk_dlssg_wanted() != 0); + const bool restart = framegen_restart(); if (restart) note = UI_MENU_AFTER_RESTART; else if (cur == 1 && !fsr) note = UI_MENU_FSR3_OFF; // set where it cannot load: "in use: off" and why // NVIDIA's set since the start but not running: "in use: off" and why (vk_dlssg_status; the log has the details). @@ -1260,6 +1273,17 @@ static const char *prompts_note(bool *restart) { } } +// How many settings wait for a restart, on the Restart button: the restart-only ones whose value differs from the one the +// game started with (cfg.c cfg_pending_count), NVIDIA's upscaling, frame generation and HDR while their rows say "after +// restart" (their settings are live ones, so the rows decide), and the prompts when a changed pad layout asks for +// others (the prompts setting itself, when changed, is among the first). +static int restart_pending(void) { + bool prompts; + prompts_note(&prompts); + return cfg_pending_count() + upscaling_restart() + framegen_restart() + (vk_hdr_needs_restart() != 0) + + (prompts && !cfg_pending("NX2NAVI_PROMPTS")); +} + // The confirmation of the menu's Restart button, over the menu; Cancel has the cursor first. Restart writes nx2navi.ini, // then closes the game as Quit does and starts it again (gfx_window_request_restart). It waits while the game writes a // save, and the game stays when nx2navi.ini cannot be written, as the changed settings would not come back. From the @@ -1480,8 +1504,15 @@ static void layout_menu(uint32_t w, uint32_t h, float scale) { ImGui::SetCursorPosY(y + st.FramePadding.y); ImGui::TextColored(ImVec4(0.95f, 0.45f, 0.35f, 1), "%s", menu_text(UI_MENU_SAVE_FAILED)); } - const char *cont = menu_text(UI_MENU_CONTINUE), *restart = menu_text(UI_MENU_RESTART_YES), *quit = menu_text(UI_MENU_QUIT); - float bw = fmaxf(fmaxf(ImGui::CalcTextSize(cont).x, ImGui::CalcTextSize(restart).x), ImGui::CalcTextSize(quit).x) + st.FramePadding.x * 4; + // Restart says how many settings wait for it (restart_pending); "###restart" keeps the button's id, and so the cursor + // on it, when the count changes + char shown[80], restart[96]; + const int pending = restart_pending(); + if (pending) snprintf(shown, sizeof shown, menu_text(UI_MENU_RESTART_COUNT), pending); + else snprintf(shown, sizeof shown, "%s", menu_text(UI_MENU_RESTART_YES)); + snprintf(restart, sizeof restart, "%s###restart", shown); + const char *cont = menu_text(UI_MENU_CONTINUE), *quit = menu_text(UI_MENU_QUIT); + float bw = fmaxf(fmaxf(ImGui::CalcTextSize(cont).x, ImGui::CalcTextSize(shown).x), ImGui::CalcTextSize(quit).x) + st.FramePadding.x * 4; // the save menu's brown buttons, with the game's cursor on the selected one (ui/menu_skin.cpp) float gap = st.ItemSpacing.x + skin_cursor_reach(scale); // right-aligned; in a window too narrow for all three, from the left edge diff --git a/runtime/ui/option_rows.cpp b/runtime/ui/option_rows.cpp index 21937619..748dfba1 100644 --- a/runtime/ui/option_rows.cpp +++ b/runtime/ui/option_rows.cpp @@ -347,13 +347,15 @@ static int row_text(const char *key, const char *label, const char *desc, const dl->AddText(font, small, ImVec2(nx, ny), v.note ? IM_COL32(250, 206, 130, 255) : SKIN_TEXT_DIM, t, NULL, 0, &nclip); if (tw > 0) note_right = nx - fs * 0.6f; } - // the label and its note, clipped before the value + // the label and its note, clipped before the value. A restart-only setting says "after restart" while its value + // differs from the one the game started with (cfg_pending), so the note marks what a restart would change; the + // description says it whatever the value (desc_set) ImVec4 clip(p.x, p.y, value_left - fs * 0.6f, p.y + h); dl->AddText(font, fs, ImVec2(p.x + pad, ty), off ? kGrey : SKIN_TEXT, label, NULL, 0, &clip); - const char *note = disabled ? disabled - : off ? menu_text(UI_MENU_SET_BY_ENV) - : m && !m->live ? menu_text(UI_MENU_AFTER_RESTART) - : NULL; + const char *note = disabled ? disabled + : off ? menu_text(UI_MENU_SET_BY_ENV) + : m && !m->live && cfg_pending(key) ? menu_text(UI_MENU_AFTER_RESTART) + : NULL; ImVec4 note_clip(p.x, p.y, note_right, p.y + h); if (note) dl->AddText(font, small, ImVec2(p.x + pad, ny), off ? IM_COL32(200, 150, 90, 255) : SKIN_TEXT_DIM, note, NULL, 0, diff --git a/runtime/ui/option_rows.h b/runtime/ui/option_rows.h index e865b80b..d99bd1f6 100644 --- a/runtime/ui/option_rows.h +++ b/runtime/ui/option_rows.h @@ -2,8 +2,8 @@ // row's description on the right. Each row shows a setting of the cfg table (cfg.h) and changes it through cfg_set: A // steps it (toggle, next value), left and right step it, and the mouse steps it on the two framed arrows beside its // value and nowhere else on the row (a click on the row selects it; held on an arrow it repeats). A row marks the -// default value, says "after restart" when the setting is read at start, and is greyed out with a note when an -// environment variable fixes it. +// default value, says "after restart" while a setting read at start differs from the value the game started with +// (cfg_pending), and is greyed out with a note when an environment variable fixes it. // // opt_begin("##screen", scale); // opt_toggle("NX2NAVI_FULLSCREEN", UI_MENU_FULLSCREEN, UI_MENU_FULLSCREEN_DESC); diff --git a/runtime/ui_text.c b/runtime/ui_text.c index 14f09063..cc191d04 100644 --- a/runtime/ui_text.c +++ b/runtime/ui_text.c @@ -282,6 +282,7 @@ static const char *const text_en[UI_TEXT_COUNT] = { [UI_MENU_RESTART_CONFIRM] = "Restart the game now? Progress since your last save is lost.", [UI_MENU_QUIT_CONFIRM] = "Quit the game? Progress since your last save is lost.", [UI_MENU_RESTART_YES] = "Restart", + [UI_MENU_RESTART_COUNT] = "Restart (%d)", [UI_MENU_RESTART_SAVING] = "The game is saving: Restart is possible in a moment.", [UI_MENU_QUIT_SAVING] = "The game is saving: Quit is possible in a moment.", [UI_MENU_GROUP_STICKS] = "Analog sticks", @@ -842,6 +843,7 @@ static const char *const text_ru[UI_TEXT_COUNT] = { [UI_MENU_RESTART_CONFIRM] = "Перезапустить игру сейчас? Прогресс после последнего сохранения будет потерян.", [UI_MENU_QUIT_CONFIRM] = "Выйти из игры? Прогресс после последнего сохранения будет потерян.", [UI_MENU_RESTART_YES] = "Перезапустить", + [UI_MENU_RESTART_COUNT] = "Перезапустить (%d)", [UI_MENU_RESTART_SAVING] = "Игра сохраняется: перезапуск будет доступен через мгновение.", [UI_MENU_QUIT_SAVING] = "Игра сохраняется: выход будет доступен через мгновение.", [UI_MENU_GROUP_STICKS] = "Стики", @@ -1526,6 +1528,7 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = { [UI_MENU_RESTART_CONFIRM] = "Reiniciar o jogo agora? O progresso desde o último salvamento é perdido.", [UI_MENU_QUIT_CONFIRM] = "Sair do jogo? O progresso desde o último salvamento é perdido.", [UI_MENU_RESTART_YES] = "Reiniciar", + [UI_MENU_RESTART_COUNT] = "Reiniciar (%d)", [UI_MENU_RESTART_SAVING] = "O jogo está salvando: dá para reiniciar em um instante.", [UI_MENU_QUIT_SAVING] = "O jogo está salvando: dá para sair em um instante.", [UI_MENU_STICK_SENS] = "Velocidade da câmera (analógico)", @@ -2183,6 +2186,7 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = { [UI_MENU_RESTART_CONFIRM] = "现在重启游戏?上次存档之后的进度将会丢失。", [UI_MENU_QUIT_CONFIRM] = "退出游戏?上次存档之后的进度将会丢失。", [UI_MENU_RESTART_YES] = "重启", + [UI_MENU_RESTART_COUNT] = "重启(%d)", [UI_MENU_RESTART_SAVING] = "游戏正在存档:稍后即可重启。", [UI_MENU_QUIT_SAVING] = "游戏正在存档:稍后即可退出。", [UI_MENU_STICK_SENS] = "摇杆镜头速度", diff --git a/runtime/ui_text.h b/runtime/ui_text.h index 9b45edf2..e07b8059 100644 --- a/runtime/ui_text.h +++ b/runtime/ui_text.h @@ -114,6 +114,7 @@ typedef enum { UI_MENU_PROMPTS_OFF_MODS, UI_MENU_PROMPTS_OFF_PATCHES, // "Restart now" under the prompts while they change after restart, and its confirmation (Cancel: UI_MENU_RESET_NO) UI_MENU_RESTART, UI_MENU_RESTART_DESC, UI_MENU_RESTART_CONFIRM, UI_MENU_QUIT_CONFIRM, UI_MENU_RESTART_YES, UI_MENU_RESTART_SAVING, UI_MENU_QUIT_SAVING, + UI_MENU_RESTART_COUNT, // the bar's Restart with the count of settings waiting for it (printf: %d) UI_MENU_GROUP_STICKS, UI_MENU_LSTICK_DEADZONE, UI_MENU_RSTICK_DEADZONE, UI_MENU_STICK_DEADZONE_DESC, UI_MENU_STICK_DEADZONE_NOW, UI_MENU_STICK_SENS, UI_MENU_STICK_SENS_DESC, UI_MENU_MOUSE_MODE, UI_MENU_MOUSE_MODE_DESC, UI_MENU_MOUSE_STICK, UI_MENU_MOUSE_DIRECT, diff --git a/tools/menu_preview.cpp b/tools/menu_preview.cpp index daa5d451..187de365 100644 --- a/tools/menu_preview.cpp +++ b/tools/menu_preview.cpp @@ -73,6 +73,7 @@ int cfg_from_env(const char *) { return 0; } void cfg_set(const char *, const char *) {} int cfg_save(void) { return 1; } int cfg_reset(const char *const *, int) { return 0; } +int cfg_pending(const char *) { return 0; } // nothing changed since the start: no "after restart" notes unsigned cfg_version(void) { return 1; } void menu_sound_play(MenuSound) {} const char *ov_language(void) { return "en-US"; }