// The in-game menu core: Dear ImGui state, the event queue the window adapter fills, gamepad navigation, the Vulkan back // end on our device and the menu's pages. Free of any window system (menu.h). ImGui runs on the present path only, // under gfx_present.c's present lock; the event functions only queue. // // The Vulkan set-up: dynamic rendering into the swapchain image, an internal descriptor pool, vertex buffers rotated // over more frames than are in flight. // // The look: colours, rounding, the tab bar with L / R and Q / E, the window over a dimmed game. The pages are rows of // option_rows.cpp. // // Contains code Copyright (c) 2026 Peiikai, Copyright (c) 2026 Revivedx and the Savage Wind Recomp contributors (MIT; see // THIRD_PARTY_NOTICES.md). #include #include #include #include #include #include #include "imgui.h" #include "imgui_internal.h" // SetFocusID, NavCursorVisible #include "backends/imgui_impl_vulkan.h" // A private copy of stb_truetype, the parser Dear ImGui builds its font atlas with, to check a system font before the // atlas gets it (font_usable). Its asserts only mark the font bad here. static int g_font_bad; #define STBTT_STATIC #define STB_TRUETYPE_IMPLEMENTATION #define STBTT_assert(x) ((x) ? (void)0 : (void)(g_font_bad = 1)) #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-function" #include "imstb_truetype.h" #include "menu_sound.h" #include "menu_assets.h" #include "ui/menu_art.h" #include "ui/menu_skin.h" #pragma clang diagnostic pop #include "ui/amiibo_page.h" #include "ui/binds_page.h" #include "ui/debug_menu.h" #include "ui/option_rows.h" #include "ui/perf_panel.h" extern "C" { #include "amiibo.h" #include "bugcap.h" #include "cheats.h" #include "debug_tools.h" #include "cfg.h" #include "folder_pick.h" #include "fs.h" #include "input.h" #include "input_map.h" #include "mods.h" #include "perfmon.h" #include "platform/platform.h" #include "save_share.h" #include "sprint.h" #include "sysinfo.h" #include "text_speed.h" #include "ui/menu.h" #include "ui_text.h" #include "vk_dlssg.h" #include "vk_lowlat.h" // host.h and vk.h pull in cpu.h, which is C only: the functions the menu needs from them, as declared there. void rt_log(const char *fmt, ...) __attribute__((format(printf, 1, 2))); void vk_queue_lock(void); void vk_queue_unlock(void); void res_sizes(unsigned out[2], unsigned scene[2]); int res_adjustable(void); int wide_available(void); int wide_scene_settable(void); void wide_picture(unsigned *w, unsigned *h); double wide_scene_share(void); double wide_scene_wanted(void); int wide_scene_restart(void); const char *const *vk_color_preset(const char *name); int vk_hdr_wanted(void); int vk_hdr_active(void); int vk_hdr_needs_restart(void); int vk_dlaa_needs_restart(void); int vk_ngxfg_wanted(void); // vk_dlaa.c: DLSS's frame generation through NGX itself can run in this run int vk_ngxfg_max(void); // ... and how many frames it may generate per rendered one (0: it cannot) int vk_ffx_available(void); // vk_ffx.cpp: FSR 3.1 upscaling can run (asked without loading the library) int vk_ffx_framegen_available(void); // ... and FSR 3.1 frame generation int vk_ffx4_available(void); // vk_ffx4.cpp: AMD's FSR 4 runs on this device (its libraries, card and driver) int vk_ffx_fsr4(void); // vk_ffx.cpp: ... and the setting asks for it (NX2NAVI_FSR4) void vk_ffx4_retry(void); // vk_ffx4.cpp: FSR 4 chosen again after failures that turned it off const char *vk_ffx4_name(void); // vk_ffx4.cpp: "AMD FSR 4.1.2" once the driver's library was asked int vk_fg4_available(void); // vk_fg4.cpp: the frame generation of AMD's FSR 4 library runs on this device int vk_fg4_failed(void); // ... it failed in this run, FSR 3.1's generates the frames int vk_fg4_active(void); // ... its swapchain is in place const char *vk_fg4_name(void); // ... the version the library runs here ("3.1.6", "4.0.2") int vk_texture_detail_auto(void); // vk_texture.c: the texture detail auto picks on this card, 0 full to 2 low int vk_mesh_shader_native(void); // vk_device.c: the GPU has mesh shaders (the mesh emulation does nothing) int vk_low_end_card(void); // vk_device.c: no mesh shaders, or under 8 GiB of video memory int vk_exec_replay_workers_auto(const char **why); // vk_exec.c: replay_workers = auto's count on this PC double frame_step_limit_hz(void); int frame_limit_adjustable(void); int rtss_limit_state(void); int frame_free_running(void); extern const int rt_game_version; extern const char rt_port_version[]; void cheats_frame(uint64_t frame); // cheats.c bool tab_cheats(float scale); // ui/cheats_tab.cpp; true: restart into the rebuild } // Roboto Medium (Apache-2.0, ui/fonts/LICENSE-Roboto.txt) with Latin and Cyrillic glyphs. The file comes in with // #embed where the compiler has it (clang 19 and later define __has_embed). An older one, which a Linux build with the // distribution's compiler can meet, includes the same bytes as numbers from roboto_ttf.h, which tools/build_shaders.py // writes beside this file from the font when the game is built (a kit carries it). #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wc23-extensions" // #embed in C++ static const unsigned char roboto_ttf[] = { #ifdef __has_embed #embed "fonts/Roboto-Medium.ttf" #else #include "roboto_ttf.h" #endif }; #pragma clang diagnostic pop // ---- open state, any thread static std::atomic g_open; // the menu is open static std::atomic g_reset; // opened or closed since the last laid-out frame: drop stale input // g_open, the input block and the log line change together under open_lock, so they cannot disagree when the window thread // (F1, Esc), the polling thread (combo) and the present path (Continue, B) race. static PlatMutex open_lock = PLAT_MUTEX_INIT; static PlatMutex ev_lock = PLAT_MUTEX_INIT; // the event queue below static int ev_n; // want 0 or 1 sets the state; -1 flips it. Returns the new state. The window events queued before the change go with it: // keys pressed just before a close would otherwise reach the menu when it next opens. static int change_open(int want) { plat_mutex_lock(&open_lock); int open = want < 0 ? !g_open.load() : want; if (g_open.load() != open) { g_open.store(open); g_reset.store(1); plat_mutex_lock(&ev_lock); ev_n = 0; plat_mutex_unlock(&ev_lock); input_menu(open); menu_sound_play(open ? MENU_SOUND_OPEN : MENU_SOUND_CLOSE); rt_log("menu: %s", open ? "open" : "closed"); } plat_mutex_unlock(&open_lock); return open; } static void set_open(int open) { change_open(open); } // The debug menu (ui/debug_menu.cpp; F9 with NX2NAVI_DEBUG_MENU = 1) is laid out in this menu's place: open is the one // state for both, with its input, its sounds and the game's blocked pad, and g_debug says which window it shows. static std::atomic g_debug{0}; extern "C" int menu_debug_enabled(void) { return cfg_bool("NX2NAVI_DEBUG_MENU"); } static int g_failed_flag(); // Until the game's first frame (menu_overlay_frame), boot screen included: the menu does not open, as it would not be // drawn while its keys queued up and changed settings once the game ran. static std::atomic g_booting{1}; extern "C" int menu_is_open(void) { return g_open.load(std::memory_order_relaxed); } extern "C" void menu_toggle(void) { if (g_failed_flag() || g_booting.load(std::memory_order_acquire)) return; if (!menu_is_open()) g_debug.store(0); // F1 opens the settings; with either window open it closes that one change_open(-1); } // F9: opens the debug menu, and closes it again; with the settings menu open it does nothing. extern "C" void menu_debug_toggle(void) { if (!menu_debug_enabled() || g_failed_flag() || g_booting.load(std::memory_order_acquire)) return; if (menu_is_open()) { if (g_debug.load()) change_open(0); return; } g_debug.store(1); change_open(1); } // ---- events from the window adapter, queued while the menu is open; the present path hands them to ImGui enum { EV_KEY, EV_POS, EV_BUTTON, EV_WHEEL, EV_FOCUS, EV_CHAR, EV_MAX = 256 }; struct Ev { int type, a, b; float x, y; }; static Ev ev_q[EV_MAX]; static void push(Ev e) { if (!menu_is_open()) return; plat_mutex_lock(&ev_lock); // checked again under the lock: change_open empties the queue after the state changes, so an event of the closed // menu cannot slip in behind that if (!menu_is_open()) { plat_mutex_unlock(&ev_lock); return; } if (e.type == EV_POS && ev_n && ev_q[ev_n - 1].type == EV_POS) ev_q[ev_n - 1] = e; // only the newest position else if (ev_n < EV_MAX) ev_q[ev_n++] = e; plat_mutex_unlock(&ev_lock); } extern "C" void menu_event_key(int key, int down) { push({EV_KEY, key, down, 0, 0}); } extern "C" void menu_event_mouse_pos(float x, float y) { push({EV_POS, 0, 0, x, y}); } extern "C" void menu_event_mouse_button(int button, int down) { push({EV_BUTTON, button, down, 0, 0}); } extern "C" void menu_event_wheel(float dx, float dy) { push({EV_WHEEL, 0, 0, dx, dy}); } extern "C" void menu_event_focus(int focused) { push({EV_FOCUS, focused, 0, 0, 0}); } // a typed character, for the text fields of the debug menu extern "C" void menu_event_char(unsigned c) { push({EV_CHAR, (int)c, 0, 0, 0}); } // The clipboard's text a page asked for (gfx_window_paste): the window thread reads it, the page takes it a frame later. static PlatMutex paste_lock = PLAT_MUTEX_INIT; static char paste_text[256]; static int paste_ready; extern "C" void menu_event_paste(const char *utf8) { plat_mutex_lock(&paste_lock); snprintf(paste_text, sizeof paste_text, "%s", utf8 ? utf8 : ""); paste_ready = 1; plat_mutex_unlock(&paste_lock); } extern "C" int menu_paste_take(char *out, size_t cap) { plat_mutex_lock(&paste_lock); int ready = paste_ready; if (ready && cap) snprintf(out, cap, "%s", paste_text); paste_ready = 0; plat_mutex_unlock(&paste_lock); return ready; } static void drain_events(ImGuiIO &io) { Ev q[EV_MAX]; plat_mutex_lock(&ev_lock); int n = ev_n; for (int i = 0; i < n; i++) q[i] = ev_q[i]; ev_n = 0; plat_mutex_unlock(&ev_lock); for (int i = 0; i < n; i++) { const Ev &e = q[i]; switch (e.type) { case EV_KEY: io.AddKeyEvent((ImGuiKey)e.a, e.b != 0); // ImGui takes the modifier state (io.KeyShift, io.KeyCtrl: Shift+Tab, Ctrl+Up) from these keys only if (e.a == ImGuiKey_LeftShift) io.AddKeyEvent(ImGuiMod_Shift, e.b != 0); if (e.a == ImGuiKey_LeftCtrl) io.AddKeyEvent(ImGuiMod_Ctrl, e.b != 0); break; case EV_POS: io.AddMousePosEvent(e.x, e.y); break; case EV_BUTTON: io.AddMouseButtonEvent(e.a, e.b != 0); break; case EV_WHEEL: io.AddMouseWheelEvent(e.x, e.y); break; case EV_FOCUS: io.AddFocusEvent(e.a != 0); break; case EV_CHAR: io.AddInputCharacter((unsigned)e.a); break; } } } // ---- gamepad navigation: the Switch buttons of our SDL3 gamepad to ImGui's gamepad keys. Nintendo layout: A (east) // activates, B (south) cancels. static const struct { int bit; ImGuiKey key; } pad_keys[] = { {B_A, ImGuiKey_GamepadFaceDown}, {B_B, ImGuiKey_GamepadFaceRight}, {B_X, ImGuiKey_GamepadFaceUp}, {B_Y, ImGuiKey_GamepadFaceLeft}, {B_L, ImGuiKey_GamepadL1}, {B_R, ImGuiKey_GamepadR1}, {B_ZL, ImGuiKey_GamepadL2}, {B_ZR, ImGuiKey_GamepadR2}, {B_LEFT, ImGuiKey_GamepadDpadLeft}, {B_UP, ImGuiKey_GamepadDpadUp}, {B_RIGHT, ImGuiKey_GamepadDpadRight}, {B_DOWN, ImGuiKey_GamepadDpadDown}, }; static uint64_t pad_prev; // buttons at the last frame; a button held when the menu opened counts once released static int stick_dir = -1; // the D-pad button the left stick holds, -1 none // The left stick moves the cursor as the D-pad does (ImGui's own stick handling only scrolls the page): past half way // it holds the D-pad direction of its larger axis, kept until that axis falls under 0.35; ImGui repeats a held one. static int stick_to_dpad(int32_t lx, int32_t ly) { float x = lx / 32767.0f, y = ly / 32767.0f; // y up float along = stick_dir == B_LEFT ? -x : stick_dir == B_RIGHT ? x : stick_dir == B_UP ? y : stick_dir == B_DOWN ? -y : 0; if (stick_dir >= 0 && along > 0.35f) return stick_dir; if (fmaxf(fabsf(x), fabsf(y)) < 0.5f) return -1; return fabsf(x) > fabsf(y) ? (x < 0 ? B_LEFT : B_RIGHT) : (y > 0 ? B_UP : B_DOWN); } static void pad_events(ImGuiIO &io, int reset) { uint64_t b; int32_t lx, ly; int have = input_pad_read(&b, &lx, &ly); if (have) io.BackendFlags |= ImGuiBackendFlags_HasGamepad; else io.BackendFlags &= ~ImGuiBackendFlags_HasGamepad; stick_dir = stick_to_dpad(lx, ly); if (stick_dir >= 0) b |= 1ull << stick_dir; if (reset) { pad_prev = b; return; } for (const auto &k : pad_keys) { uint64_t m = 1ull << k.bit; if ((b ^ pad_prev) & m) io.AddKeyEvent(k.key, (b & m) != 0); } pad_prev = b; } // ---- ImGui and its Vulkan back end (present path) static int g_ctx_ok; // context made static std::atomic g_failed; // set-up failed for good (the menu stays closed) static int g_failed_flag() { return g_failed.load(std::memory_order_relaxed); } static int g_vk_ok; // back end initialised for g_vk_fmt static VkFormat g_vk_fmt; static float g_font_scale; // the scale of the uploaded font atlas; 0 when none is uploaded static float g_style_scale; // the scale the style was last sized for static ImGuiStyle g_base_style; // the menu's style at 1080 lines static uint64_t g_last_ns; static uint64_t g_cost_ns, g_cost_frames; // CPU time of layout and recording while open, logged per 600 frames // Sizes are tuned for 1080 lines and follow the window's height, or its width in a window narrower than 1.47:1, where // the menu's 1400 pixels at 1080 lines would no longer fit 88 % of the width (layout_menu). Under 720 lines at 100% // display scale they stay at the 720 size; that floor grows with the window's DPI, so a small window on a 150% or 200% // display (sizes are physical pixels) still gets text as large as the display scale asks for. static float menu_scale(uint32_t w, uint32_t h) { float least = 720.0f * (float)gfx_window_dpi() / 96.0f, lines = fminf((float)h, (float)w * 0.68f); return (lines >= least ? lines : least) / 1080.0f; } // The gold of the title and its Triforce (the rest of the menu's colours: ui/menu_skin.cpp skin_style). static const ImVec4 kGold(0.93f, 0.76f, 0.38f, 1); // The Triforce, its top at (x, y), h high, drawn with the title. static void triforce(ImDrawList *dl, float x, float y, float h, ImU32 col) { float w = h * 1.1547f; // equilateral: width = height * 2 / sqrt(3) ImVec2 t(x, y), l(x - w / 2, y + h), r(x + w / 2, y + h), ml(x - w / 4, y + h / 2), mr(x + w / 4, y + h / 2), mb(x, y + h); dl->AddTriangleFilled(t, mr, ml, col); dl->AddTriangleFilled(ml, mb, l, col); dl->AddTriangleFilled(mr, r, mb, col); } static void make_context(void) { ImGui::CreateContext(); ImGuiIO &io = ImGui::GetIO(); io.IniFilename = nullptr; io.LogFilename = nullptr; io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard | ImGuiConfigFlags_NavEnableGamepad; io.BackendPlatformName = "nx2navi_menu"; // no window switcher on Ctrl+Tab (the menu is one window; the switcher would list its inner windows by their ids) GImGui->ConfigNavWindowingKeyNext = GImGui->ConfigNavWindowingKeyPrev = 0; skin_style(g_base_style); ImGui::GetStyle() = g_base_style; g_ctx_ok = 1; } // Dear ImGui's Vulkan back end calls this with each VkResult it gets (it went unchecked before): failures are logged and // counted, so a texture whose descriptor set was not allocated is not drawn with (ui/menu_art.cpp upload_rgba). static std::atomic g_vk_errors; static void menu_vk_check(VkResult r) { if (r == VK_SUCCESS) return; if (g_vk_errors.fetch_add(1) < 8) rt_log("menu: Dear ImGui's Vulkan back end got VkResult %d", (int)r); } extern "C" int menu_vk_errors(void) { return g_vk_errors.load(); } static void vk_down(void) { if (!g_vk_ok) return; menu_art_release(); ImGui_ImplVulkan_Shutdown(); g_vk_ok = 0; g_font_scale = 0; } static int vk_up(const MenuTarget *t) { if (g_vk_ok && g_vk_fmt == t->format) return 1; if (g_vk_ok) { // the swapchain changed format: pipelines for the new one vk_queue_lock(); vkQueueWaitIdle(t->queue); vk_queue_unlock(); vk_down(); } ImGui_ImplVulkan_InitInfo ii = {}; ii.ApiVersion = VK_API_VERSION_1_3; ii.Instance = t->instance; ii.PhysicalDevice = t->phys; ii.Device = t->device; ii.QueueFamily = t->queue_family; ii.Queue = t->queue; ii.DescriptorPoolSize = 32; // the art's sets, and two font atlases while one replaces the other ii.CheckVkResultFn = menu_vk_check; ii.MinImageCount = 2; ii.ImageCount = 4; // vertex buffers rotate per recorded frame: more than gfx_present.c's 2 frames in flight ii.MSAASamples = VK_SAMPLE_COUNT_1_BIT; ii.UseDynamicRendering = true; g_vk_fmt = t->format; ii.PipelineRenderingCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO; ii.PipelineRenderingCreateInfo.colorAttachmentCount = 1; ii.PipelineRenderingCreateInfo.pColorAttachmentFormats = &g_vk_fmt; if (!ImGui_ImplVulkan_Init(&ii)) return 0; g_vk_ok = 1; return 1; } // Roboto Medium (Latin, Cyrillic, dashes and quotes) at 26 px per 1080 lines, with the glyphs of the Japanese, Korean // and Chinese language names merged in from a system font when there is one (each language is named in its own script). // A catalog row also holds menu text (Simplified Chinese), so every string of the row goes into the builder: the merged // font must cover the whole menu of that language, not only its name. Made again when the window height changes; the old // texture goes after the queue is idle (ui/menu_art.cpp menu_font_upload). static const ImWchar kRanges[] = {0x0020, 0x00FF, 0x0400, 0x052F, 0x2010, 0x2027, 0}; static const struct { const char *lang; UiTextId name; } kCjk[] = { {"ja", UI_MENU_LANGNAME_JA}, {"ko", UI_MENU_LANGNAME_KO}, {"zh-Hans", UI_MENU_LANGNAME_ZH_HANS}, {"zh-Hant", UI_MENU_LANGNAME_ZH_HANT}, }; static uint32_t be32(const uint8_t *p) { return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | p[3]; } // 1 when Dear ImGui can take this font file: more than the 100 bytes AddFontFromMemoryTTF asserts on, every table of its // directory inside the file (stb_truetype reads tables without bounds checks, so a cut-off file faults when the glyphs // are made), and stb_truetype parses it. ImGui asserts on a font it cannot parse, and an assert ends the process with // 0xc0000409 and no line in the log. static int font_usable(const uint8_t *d, uint64_t n) { if (!d || n <= 100 || n > INT_MAX) return 0; int off = stbtt_GetFontOffsetForIndex(d, 0); if (off < 0 || (uint64_t)off + 12 > n) return 0; uint32_t tables = (uint32_t)d[off + 4] << 8 | d[off + 5]; if ((uint64_t)off + 12 + 16ull * tables > n) return 0; for (uint32_t i = 0; i < tables; i++) { const uint8_t *r = d + off + 12 + 16 * i; if ((uint64_t)be32(r + 8) + be32(r + 12) > n) return 0; } stbtt_fontinfo fi; g_font_bad = 0; return stbtt_InitFont(&fi, d, off) && !g_font_bad; } extern "C" int menu_font_check(void) { uint64_t n = sizeof roboto_ttf; uint8_t *bad = (uint8_t *)malloc(n); if (!bad) return 0; memcpy(bad, roboto_ttf, n); uint32_t tables = (uint32_t)bad[4] << 8 | bad[5]; for (uint32_t i = 0; i < tables; i++) if (!memcmp(bad + 12 + 16 * i, "cmap", 4)) memset(bad + 12 + 16 * i, 0, 4); // no character map int ok = font_usable(roboto_ttf, n) && !font_usable(roboto_ttf, 4096) && !font_usable(roboto_ttf, 100) && !font_usable(bad, n); memset(bad, 0, 4); // an unknown font tag ok = ok && !font_usable(bad, n); free(bad); return ok; } // The glyphs of every string of a catalog row (ui_text_visit), for the font that draws that language. static void add_row_glyphs(void *ctx, const char *text) { static_cast(ctx)->AddText(text); } // The game's own interface fonts from the user's RomFS (menu_assets.c), read once: ClarimoUDPE for the menu's text, // CetraDisplayPro for its title and tabs. Roboto stays merged behind them for the glyphs they lack; without them the // menu keeps Roboto alone. static uint8_t *g_game_font, *g_title_font_data; static size_t g_game_font_n, g_title_font_n; static ImFont *g_title_font; static void game_fonts_read(void) { static int done; if (done) return; done = 1; g_game_font = menu_asset_font("Font2/Scalable/ClarimoUDPE-Regular.Nin_Ounce_NVN2.bfotf.zs", &g_game_font_n); g_title_font_data = menu_asset_font("Font2/Scalable/CetraDisplayPro-Medium.Nin_Ounce_NVN2.bfotf.zs", &g_title_font_n); if (g_game_font && !font_usable(g_game_font, g_game_font_n)) free(g_game_font), g_game_font = NULL; if (g_title_font_data && !font_usable(g_title_font_data, g_title_font_n)) free(g_title_font_data), g_title_font_data = NULL; rt_log("menu: the game's interface fonts: text %s, titles %s", g_game_font ? "ClarimoUDPE" : "not found (Roboto)", g_title_font_data ? "CetraDisplayPro" : "not found"); } static int fonts_up(float scale) { enum { N = sizeof kCjk / sizeof *kCjk }; ImFontAtlas *fa = ImGui::GetIO().Fonts; fa->Clear(); game_fonts_read(); g_title_font = nullptr; ImFontConfig fc; fc.FontDataOwnedByAtlas = false; if (g_game_font) { fa->AddFontFromMemoryTTF(g_game_font, (int)g_game_font_n, 25.5f * scale, &fc, kRanges); ImFontConfig rc; rc.MergeMode = true; rc.FontDataOwnedByAtlas = false; fa->AddFontFromMemoryTTF((void *)roboto_ttf, (int)sizeof roboto_ttf, 26.0f * scale, &rc, kRanges); } else { fa->AddFontFromMemoryTTF((void *)roboto_ttf, (int)sizeof roboto_ttf, 26.0f * scale, &fc, kRanges); } uint8_t *data[N] = {}; uint64_t sizes[N] = {}; ImVector ranges[N], title_ranges[N]; int merged = 0; for (int i = 0; i < N; i++) { char path[512]; uint64_t &size = sizes[i]; if (!plat_font_path(kCjk[i].lang, path, sizeof path) || !(data[i] = plat_file_read(path, &size))) continue; if (!font_usable(data[i], size)) { rt_log("menu: system font %s left out: the font parser cannot read it (%llu bytes)", path, (unsigned long long)size); free(data[i]); data[i] = NULL; continue; } merged++; ImFontGlyphRangesBuilder b; b.AddText(ui_text_in("en", kCjk[i].name)); ui_text_visit(kCjk[i].lang, add_row_glyphs, &b); // Text the menu shows that no catalog row holds: a mod's cheat names and its folder name, // both read from disk at runtime. Their glyphs would otherwise fall back to the missing-glyph box. for (int k = 0; k < cheats_count(); k++) if (const CheatInfo *ci = cheats_info(k); ci && ci->name) b.AddText(ci->name); for (int k = 0; k < mods_count(); k++) if (const ModInfo *mi = mods_at(k); mi) b.AddText(mi->name); b.BuildRanges(&ranges[i]); ImFontConfig mc; mc.MergeMode = true; mc.FontDataOwnedByAtlas = false; fa->AddFontFromMemoryTTF(data[i], (int)size, 26.0f * scale, &mc, ranges[i].Data); } if (g_title_font_data) { // after the text font's merges, which go to the font added last ImFontConfig tc; tc.FontDataOwnedByAtlas = false; g_title_font = fa->AddFontFromMemoryTTF(g_title_font_data, (int)g_title_font_n, 31.0f * scale, &tc, kRanges); ImFontConfig rc; rc.MergeMode = true; rc.FontDataOwnedByAtlas = false; fa->AddFontFromMemoryTTF((void *)roboto_ttf, (int)sizeof roboto_ttf, 30.0f * scale, &rc, kRanges); // what the title face shows in the CJK languages: the menu's title and tabs (Simplified Chinese menu text) and // the boot screen's title (the game's language) static const UiTextId titles[] = {UI_MENU_TITLE, UI_MENU_TAB_SCREEN, UI_MENU_TAB_GAME, UI_MENU_TAB_SOUND, UI_MENU_TAB_CONTROLS, UI_BOOT_TITLE}; for (int i = 0; i < N; i++) { if (!data[i]) continue; ImFontGlyphRangesBuilder b; for (UiTextId id : titles) b.AddText(ui_text_in(kCjk[i].lang, id)); b.BuildRanges(&title_ranges[i]); ImFontConfig mc; mc.MergeMode = true; mc.FontDataOwnedByAtlas = false; fa->AddFontFromMemoryTTF(data[i], (int)sizes[i], 31.0f * scale, &mc, title_ranges[i].Data); } } skin_atlas_add(fa); // the boxes' grain tile (ui/menu_skin.cpp), drawn into the atlas once it is built skin_atlas_fill(fa); // builds the atlas, which the upload below then takes as it is unsigned char *px = nullptr; int aw = 0, ah = 0; fa->GetTexDataAsRGBA32(&px, &aw, &ah); ImTextureID tex = 0; int ok = menu_font_upload(px, aw, ah, &tex); if (ok) fa->SetTexID(tex); // ClarimoUDPE's line (ascent to descent, which is all Dear ImGui spaces lines by) leaves wrapped lines touching: the // line height grows by a sixth over the glyphs' size, the glyphs stay as rasterized. if (ok && g_game_font && fa->Fonts.Size) fa->Fonts[0]->FontSize *= 1.16f; fa->ClearInputData(); // the glyphs are in the texture: the font files and the pixels are not needed again fa->ClearTexData(); for (uint8_t *d : data) free(d); rt_log("menu: font atlas at %.1f px, language names from %d of %d system fonts", 26.0f * scale, merged, (int)N); g_font_scale = ok ? scale : 0; return ok; } // The atlas for scale. While the scale keeps changing (the window being resized), the atlas is made again only once it // has held for a quarter of a second, and the old one is drawn scaled meanwhile (io.FontGlobalScale): each atlas reads // the system fonts from disk and waits for the queue to upload. 0 when no atlas could be made. static float g_scale_seen; static uint64_t g_scale_seen_ns; static int fonts_for(float scale, uint64_t now) { ImGuiIO &io = ImGui::GetIO(); if (g_font_scale == scale) { io.FontGlobalScale = 1; return 1; } if (scale != g_scale_seen) g_scale_seen = scale, g_scale_seen_ns = now; if (g_font_scale > 0 && now - g_scale_seen_ns < 250000000ull) { io.FontGlobalScale = scale / g_font_scale; return 1; } io.FontGlobalScale = 1; return fonts_up(scale); } extern "C" void menu_vk_release(void) { if (g_ctx_ok) vk_down(); } // ---- test hooks // NX2NAVI_MENU_TEST=[,...][:]: at each such presented frame (up to 8) the menu opens on // its first tab; each tab stays frames (default 30) and its last frame's swapchain image goes to // NX2NAVI_MENU_TEST_DIR (default: the working directory) as menu-test--.png; after the last tab the menu // closes. // NX2NAVI_MENU_TEST_CLICK=:[,...]: mouse clicks on the rows of the page shown (ui/option_rows.cpp click_test). // NX2NAVI_MENU_TEST_SET=:=[,=...][;:...]: cfg_set of each key (NX2NAVI_ prefix // optional) at that presented frame; changes live settings with no input, menu open or not. nx2navi.ini is written only by // the pseudo-key SAVE=1 (cfg_save, as a row change does; a failed write shows the menu's save note). FOCUS= (the // NX2NAVI_ prefix optional) puts the cursor on that setting's row and scrolls it into view when the row is next drawn // (opt_focus_key), so a page's shot shows a row below the first screenful; it does not switch tabs. // Ocarina of Time: the options this port has enum { TAB_SCREEN, TAB_GAME, TAB_SOUND, TAB_CONTROLS, TAB_COUNT }; enum { TEST_PAGE_FRAMES = 30, TEST_SETS = 16, TEST_OPENS = 8 }; struct TestSet { uint64_t frame; char key[64], value[64]; }; static struct { int parsed; uint64_t at[TEST_OPENS]; // NX2NAVI_MENU_TEST frames, in order int n_at, next_at; // how many; the next to open at uint64_t per_page; uint64_t opened; // the frame the hook opened the menu; 0 not yet int page; // the tab being shot; -1 none TestSet sets[TEST_SETS]; int n_sets, next_set; // NX2NAVI_MENU_TEST_SET, in frame order } g_test = {0, {}, 0, 0, 0, 0, -1, {}, 0, 0}; static void test_parse(void) { g_test.parsed = 1; if (const char *e = cfg_get("NX2NAVI_MENU_TEST")) { long long per = 0; if (const char *c = strchr(e, ':')) per = atoll(c + 1); for (const char *s = e; *s && *s != ':' && g_test.n_at < TEST_OPENS;) { char *end; unsigned long long at = strtoull(s, &end, 10); if (end == s) break; if (at > 0 && (!g_test.n_at || at > g_test.at[g_test.n_at - 1])) g_test.at[g_test.n_at++] = at; s = *end == ',' ? end + 1 : end; } g_test.per_page = per > 0 ? (uint64_t)per : TEST_PAGE_FRAMES; } const char *s = cfg_get("NX2NAVI_MENU_TEST_SET"); while (s && *s) { // groups ":=,..." separated by ';' char *end; unsigned long long f = strtoull(s, &end, 10); if (*end != ':') break; s = end + 1; while (*s && *s != ';' && g_test.n_sets < TEST_SETS) { size_t n = strcspn(s, ",;"); const char *eq = (const char *)memchr(s, '=', n); if (eq) { TestSet &t = g_test.sets[g_test.n_sets++]; t.frame = f; int kn = (int)(eq - s), vn = (int)(s + n - eq - 1); snprintf(t.key, sizeof t.key, "%s%.*s", strncmp(s, "NX2NAVI_", strlen("NX2NAVI_")) ? "NX2NAVI_" : "", kn, s); snprintf(t.value, sizeof t.value, "%.*s", vn, eq + 1); } s += n; if (*s == ',') s++; } while (*s && *s != ';') s++; if (*s == ';') s++; } if (g_test.n_at || g_test.n_sets) rt_log("menu: test hook: open at frame %llu (%d times), %llu frames per page, %d settings changes", (unsigned long long)(g_test.n_at ? g_test.at[0] : 0), g_test.n_at, (unsigned long long)g_test.per_page, g_test.n_sets); } static void test_apply_sets(uint64_t frame) { for (; g_test.next_set < g_test.n_sets && g_test.sets[g_test.next_set].frame <= frame; g_test.next_set++) { const TestSet &t = g_test.sets[g_test.next_set]; rt_log("menu: test hook: frame %llu: %s=%s", (unsigned long long)frame, t.key, t.value); if (!strcmp(t.key, "NX2NAVI_SAVE")) { rt_log("menu: test hook: cfg_save %s", cfg_save() ? "wrote" : "failed"); } else if (!strcmp(t.key, "NX2NAVI_FOCUS")) { char row[80]; snprintf(row, sizeof row, "%s%s", strncmp(t.value, "NX2NAVI_", strlen("NX2NAVI_")) ? "NX2NAVI_" : "", t.value); opt_focus_key(row); } else { cfg_set(t.key, t.value); } } } // ---- the lists the Screen and Game tabs build enum { MAX_GPUS = 8 }; static char g_gpu_names[MAX_GPUS][VK_MAX_PHYSICAL_DEVICE_NAME_SIZE]; static int g_gpu_n = -1; // -1: not listed yet // The Vulkan devices in vkEnumeratePhysicalDevices order, which NX2NAVI_GPU_INDEX counts in (vk_device.c pick_device). static void list_gpus(VkInstance inst) { if (g_gpu_n >= 0) return; VkPhysicalDevice pd[MAX_GPUS]; uint32_t n = MAX_GPUS; if (vkEnumeratePhysicalDevices(inst, &n, pd) < 0) n = 0; // VK_INCOMPLETE keeps the first MAX_GPUS for (uint32_t i = 0; i < n; i++) { VkPhysicalDeviceProperties p; vkGetPhysicalDeviceProperties(pd[i], &p); snprintf(g_gpu_names[i], sizeof g_gpu_names[i], "%s", p.deviceName); } g_gpu_n = (int)n; } // Window sizes: the common 16:9 sizes that fit the monitor, the 1920x1080 default and the current size. static const int kSizes[][2] = {{1280, 720}, {1366, 768}, {1600, 900}, {1920, 1080}, {2560, 1440}, {3200, 1800}, {3840, 2160}}; enum { MAX_SIZES = sizeof kSizes / sizeof *kSizes + 1 }; static void row_window_size(void) { int sz[MAX_SIZES][2], n = 0, mw = 0, mh = 0, cw = 0, ch = 0; int have = gfx_window_monitor_size(&mw, &mh); int cur_ok = sscanf(cfg_value("NX2NAVI_WINDOW_SIZE"), "%dx%d", &cw, &ch) == 2; bool listed = false; for (size_t i = 0; i < sizeof kSizes / sizeof *kSizes; i++) { int w = kSizes[i][0], h = kSizes[i][1]; bool cur = cur_ok && w == cw && h == ch; if (have && (w > mw || h > mh) && !cur && !(w == 1920 && h == 1080)) continue; listed |= cur; sz[n][0] = w, sz[n][1] = h, n++; } if (cur_ok && !listed) { // the current size where its area puts it int at = n; while (at > 0 && (long)sz[at - 1][0] * sz[at - 1][1] > (long)cw * ch) at--; memmove(sz[at + 1], sz[at], sizeof sz[0] * (n - at)); sz[at][0] = cw, sz[at][1] = ch, n++; } char vals[MAX_SIZES][24], shown[MAX_SIZES][32]; const char *pv[MAX_SIZES], *pn[MAX_SIZES]; for (int i = 0; i < n; i++) { snprintf(vals[i], sizeof vals[i], "%dx%d", sz[i][0], sz[i][1]); snprintf(shown[i], sizeof shown[i], "%d × %d", sz[i][0], sz[i][1]); // U+00D7, the multiplication sign pv[i] = vals[i]; pn[i] = shown[i]; } opt_choice_list("NX2NAVI_WINDOW_SIZE", UI_MENU_WINDOW_SIZE, pv, pn, n, UI_MENU_WINDOW_SIZE_DESC); } // The game resolution (resolution.c): 16:9 sizes from its smallest, 1280x720 (for weak GPUs), up, the common 21:9 and // 32:9 monitor sizes, the 16:10 screens of the Steam Deck, handhelds and laptops and the 3:2 screens of laptops and // tablets (Surface, Framework; the camera and the HUD follow // another shape, resolution.c res_init; the installer lays the HUD out for each, installer/steps.py ASPECT_SIZES), and // the current value when it is another size. static void row_output_res(void) { static const char *const vals[] = {"1280x720", "1600x900", "1920x1080", "2560x1440", "3200x1800", "3840x2160", "2560x1080", "3440x1440", "3840x1600", "5120x2160", "3840x1080", "5120x1440", "1280x800", "1680x1050", "1920x1200", "2560x1600", "1920x1280", "2160x1440", "2256x1504", "2880x1920", "3000x2000"}; enum { N = sizeof vals / sizeof *vals }; char shown[N][32]; const char *names[N]; for (int i = 0; i < N; i++) { int w = 0, h = 0; sscanf(vals[i], "%dx%d", &w, &h); snprintf(shown[i], sizeof shown[i], "%d × %d", w, h); // U+00D7, the multiplication sign names[i] = shown[i]; } opt_choice_list("NX2NAVI_OUTPUT_RES", UI_MENU_OUTPUT_RES, vals, names, N, UI_MENU_OUTPUT_RES_DESC); } static void row_render_scale(void) { static const char *const vals[] = {"game", "50", "67", "75", "100", "125", "150", "200"}; enum { N = sizeof vals / sizeof *vals }; char shown[N][32]; const char *names[N]; names[0] = menu_text(UI_MENU_RENDER_SCALE_GAME); for (int i = 1; i < N; i++) { snprintf(shown[i], sizeof shown[i], "%s %%", vals[i]); names[i] = shown[i]; } opt_choice_list("NX2NAVI_RENDER_SCALE", UI_MENU_RENDER_SCALE, vals, names, N, UI_MENU_RENDER_SCALE_DESC); } static void row_shadow_res(void) { static const char *const vals[] = {"game", "256", "512", "768", "1024", "1536", "2048"}; enum { N = sizeof vals / sizeof *vals }; const char *names[N]; names[0] = menu_text(UI_MENU_SHADOW_RES_GAME); for (int i = 1; i < N; i++) names[i] = vals[i]; opt_choice_list("NX2NAVI_SHADOW_RES", UI_MENU_SHADOW_RES, vals, names, N, UI_MENU_SHADOW_RES_DESC); } // Texture detail (vk_texture.c "texture detail"): large surface textures made without their largest one or two mip // levels. Read when the first texture is made, so the row notes the restart. The automatic entry names the level it // picks on this card. static void row_texture_detail(void) { static const char *const vals[] = {"auto", "full", "reduced", "low"}; static const UiTextId level[] = {UI_MENU_TEX_DETAIL_FULL, UI_MENU_TEX_DETAIL_REDUCED, UI_MENU_TEX_DETAIL_LOW}; static char picked[96]; // the row keeps the pointer for the frame int a = vk_texture_detail_auto(); snprintf(picked, sizeof picked, menu_text(UI_MENU_TEX_DETAIL_AUTO), menu_text(level[a < 0 || a > 2 ? 0 : a])); const char *names[] = {picked, menu_text(level[0]), menu_text(level[1]), menu_text(level[2])}; opt_choice_list("NX2NAVI_TEXTURE_DETAIL", UI_MENU_TEX_DETAIL, vals, names, 4, UI_MENU_TEX_DETAIL_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)"}; opt_choice_list("NX2NAVI_SHADOW_SCALE", UI_MENU_SHADOW_SCALE, vals, names, 4, UI_MENU_SHADOW_SCALE_DESC); } static void row_shadow_range(void) { static const char *const vals[] = {"100", "150", "200", "300"}; static const char *const names[] = {"160 m (1x)", "240 m (1.5x)", "320 m (2x)", "480 m (3x)"}; opt_choice_list("NX2NAVI_SHADOW_RANGE", UI_MENU_SHADOW_RANGE, vals, names, 4, UI_MENU_SHADOW_RANGE_DESC); } static void row_gpu(void) { const char *vals[MAX_GPUS + 1], *names[MAX_GPUS + 1]; char idx[MAX_GPUS][4]; vals[0] = ""; names[0] = menu_text(UI_MENU_GPU_AUTO); for (int i = 0; i < g_gpu_n; i++) { snprintf(idx[i], sizeof idx[i], "%d", i); vals[i + 1] = idx[i]; names[i + 1] = g_gpu_names[i]; } opt_choice_list("NX2NAVI_GPU_INDEX", UI_MENU_GPU, vals, names, g_gpu_n + 1, UI_MENU_GPU_DESC); } // The regions of the Mals/.Product..sarc.zs files in the RomFS index ("USen", "EUpl"), listed once; 0 when // the index has none (no RomFS yet). static int mals_regions(const char *out[24]) { static char reg[24][5]; static int n = -1; // -1: not scanned (the index may load after the menu first draws) if (n < 0 && romfs_file_count()) { n = 0; for (uint64_t i = 0, c = romfs_file_count(); i < c && n < 24; i++) { uint64_t off, size; const char *p = romfs_entry(i, &off, &size); if (!p || strncmp(p, "Mals/", 5) || strlen(p) < 10 || p[9] != '.') continue; memcpy(reg[n], p + 5, 4); reg[n][4] = 0; bool seen = false; for (int j = 0; j < n; j++) seen |= !strcmp(reg[j], reg[n]); if (!seen) n++; } } for (int i = 0; i < n; i++) out[i] = reg[i]; return n > 0 ? n : 0; } // The game language: one entry per Mals language file the game ships (ui_text_game_langs_in from the RomFS index, else // ui_text_game_langs by version), each under its own name. A code that names no entry exactly (older "en", "fr-CA" on // a build without it) selects the first entry of the same catalog language. static void row_game_lang(void) { UiGameLang langs[16]; const char *regions[24]; int nr = mals_regions(regions); int n = nr ? ui_text_game_langs_in(regions, nr, langs, 16) : ui_text_game_langs(rt_game_version, langs, 16); const char *vals[17], *names[17]; vals[0] = ""; names[0] = menu_text(UI_MENU_GAME_LANG_AUTO); const char *now = cfg_value("NX2NAVI_LANG"); int cur = *now ? -1 : 0; for (int i = 1; i <= n; i++) { vals[i] = langs[i - 1].code; names[i] = menu_text(langs[i - 1].name); if (*now && !strcmp(now, vals[i])) cur = i; } for (int i = 1; i <= n && *now && cur < 0; i++) if (!strcmp(ui_text_lang(now), ui_text_lang(vals[i]))) cur = i; opt_choice_list("NX2NAVI_LANG", UI_MENU_GAME_LANG, vals, names, n + 1, UI_MENU_GAME_LANG_DESC, cur); } // The frame limit (frame_rate.c): unset, the older fps60 = 0 still means 30. The monitor choice shows the rate it gives. // The note says when RivaTuner Statistics Server does the limiting (rtss_limit.c), cannot change its profile, or let the // game run past its limit. static void row_fps_limit() { // 40 and 45: the Steam Deck's screen runs at 40 to 60 Hz (the OLED to 90); 40 divides 40 and 120 Hz evenly // 165 and 240: common gaming monitors; unlimited only where the game steps by the clock (frame_rate.c) static const char *const vals[] = {"30", "40", "45", "60", "90", "120", "144", "165", "240", "display", "unlimited"}; static const UiTextId ids[] = {UI_MENU_FPS_30, UI_MENU_FPS_40, UI_MENU_FPS_45, UI_MENU_FPS_60, UI_MENU_FPS_90, UI_MENU_FPS_120, UI_MENU_FPS_144, UI_MENU_FPS_144, UI_MENU_FPS_144, UI_MENU_FPS_DISPLAY, UI_MENU_FPS_UNLIMITED}; int n = frame_free_running() ? 11 : 10; const char *names[11]; for (int i = 0; i < n; i++) names[i] = menu_text(ids[i]); names[7] = "165 FPS", names[8] = "240 FPS"; static char display[64]; double hz = gfx_window_refresh_hz(); if (hz > 0) { snprintf(display, sizeof display, menu_text(UI_MENU_FPS_DISPLAY_HZ), hz); names[9] = display; } const char *set = cfg_get("NX2NAVI_FPS_LIMIT"), *old = cfg_get("NX2NAVI_FPS60"); int cur = (!set || !*set) && old && !strcmp(old, "0") ? 0 : -1; int rtss = rtss_limit_state(); const char *note = rtss == 1 ? menu_text(UI_MENU_FPS_RTSS) : rtss == 2 ? menu_text(UI_MENU_FPS_RTSS_READONLY) : rtss == 3 ? menu_text(UI_MENU_FPS_RTSS_IGNORED) : nullptr; opt_choice_list("NX2NAVI_FPS_LIMIT", UI_MENU_FPS_LIMIT, vals, names, n, UI_MENU_FPS_LIMIT_DESC, cur, note); } // "in use: ..." under the VSync row when the swapchain does other than the row says: VSync when it is off and the driver // cannot present without waiting, Immediate when it is on and the driver has neither mailbox nor relaxed FIFO // (gfx_present.c pick_present_mode), so the game tears; NULL otherwise, and before the first swapchain. static const char *vsync_note(void) { const char *got = gfx_window_present_mode(); if (!*got) return nullptr; bool off = !strcmp(cfg_value("NX2NAVI_VK_PRESENT"), "immediate"), tears = !strcmp(got, "immediate"); if (off == tears) return nullptr; static char note[96]; snprintf(note, sizeof note, menu_text(UI_MENU_PRESENT_IN_USE), menu_text(tears ? UI_MENU_PRESENT_IMMEDIATE : UI_MENU_PRESENT_VSYNC)); return note; } // Low latency without frame generation (NX2NAVI_LOW_LATENCY, vk_lowlat.c, at once): off, on, and on with boost where // the driver's mode is NVIDIA's (AMD's has no boost; a 2 set there shows as on, which is what it does). Greyed out with // the reason where it cannot run: a driver with neither mode, a run in which DLSS frame generation brings its own, and // NVIDIA's while a frame generation presents the frames (vk_lowlat_status). Set and refused by the driver, it says so. // In a run that started with it off the driver's mode is not on the device: the row can be set and says "after restart". static void row_low_latency(void) { static const char *const values[] = {"0", "1", "2"}; const char *names[] = {menu_text(UI_MENU_OFF), menu_text(UI_MENU_ON), menu_text(UI_MENU_LOW_LATENCY_BOOST)}; const int st = vk_lowlat_status(), n = st == VK_LOWLAT_AMD ? 2 : 3; int cur = cfg_int("NX2NAVI_LOW_LATENCY"); cur = cur < 0 ? 0 : cur >= n ? n - 1 : cur; if (st == VK_LOWLAT_NO_DRIVER) opt_disable_next(UI_MENU_LOW_LATENCY_NO_DRIVER); else if (st == VK_LOWLAT_DLSSG) opt_disable_next(UI_MENU_LOW_LATENCY_DLSSG); else if (st == VK_LOWLAT_FG) opt_disable_next(UI_MENU_LOW_LATENCY_FG); const char *note = st == VK_LOWLAT_FAILED && cur ? menu_text(UI_MENU_LOW_LATENCY_FAILED) : st == VK_LOWLAT_RESTART && cur ? menu_text(UI_MENU_AFTER_RESTART) : nullptr; int nv = opt_choice_index("NX2NAVI_LOW_LATENCY", UI_MENU_LOW_LATENCY, names, n, cur, 0, UI_MENU_LOW_LATENCY_DESC, note); if (nv >= 0 && nv < n) opt_apply("NX2NAVI_LOW_LATENCY", values[nv]); } // ---- the tabs // The PCI vendor of the GPU the game renders on (menu_overlay_frame reads it from the device it is given; 0 before the // first frame). DLSS runs on NVIDIA GPUs only, so the rows below leave it out elsewhere (AMD, Intel). static std::atomic g_gpu_vendor; static VkPhysicalDevice g_gpu_vendor_of; static bool dlss_offered(void) { uint32_t v = g_gpu_vendor.load(std::memory_order_relaxed); return v == 0 || v == 0x10de; // unknown (no frame yet): as before, offered } // One upscaling choice over three settings: NX2NAVI_UPSCALER (how the picture is fitted to the window), NX2NAVI_DLAA // (DLSS or FSR 3.1 on) with NX2NAVI_TAA (which of them) and NX2NAVI_DLSS (the mode, the render scale: widescreen.c // wide_present). A temporal mode fits the picture with bilinear, the upscaler having made it; DLSS's model is NVIDIA's own // for the mode (vk_dlaa.c evaluate, dlss_preset at its default). The list: bilinear, FSR 1, the DLSS modes where DLSS // runs (dlss_offered), the FSR 3.1 modes where FSR 3.1 loads (vk_ffx_available); a DLSS or FSR 3.1 mode still set where // it does not run stays listed last until another choice replaces it, so the row shows what is set. // FSR 4's name in the Upscaling row and the overlay: the version the driver's library runs to its minor number, as // AMD names its releases ("AMD FSR 4.1" for 4.1.2); "AMD FSR 4" until the library was asked (vk_ffx4_available). static const char *fsr4_label(void) { static char s[32]; const char *v = strstr(vk_ffx4_name(), "FSR "); int major = 0, minor = 0; if (v && sscanf(v + 4, "%d.%d", &major, &minor) == 2 && major >= 4) snprintf(s, sizeof s, "AMD FSR %d.%d", major, minor); else snprintf(s, sizeof s, "AMD FSR 4"); return s; } // Native (the Upscaling row): the scene drawn at the picture's own size, which the upscaler's stand-in then only // decodes 1:1 (vk_tonemap.c), with no DLSS or FSR. It is scene_share = 1 (widescreen.c plain_share) with dlaa = 0; a // scene_share of 1 or more set by hand in nx2navi.ini reads the same. static bool share_native(void) { const char *s = cfg_get("NX2NAVI_SCENE_SHARE"); return s && *s && atof(s) >= 1; } // The Upscaling row's name of what is set now, for the performance overlay. static void upscaling_name(char *out, size_t size) { static const char *const modes[] = {"dlaa", "quality", "balanced", "performance", "ultra_performance"}; static const UiTextId dlss[] = {UI_MENU_DLSS_DLAA, UI_MENU_DLSS_QUALITY, UI_MENU_DLSS_BALANCED, UI_MENU_DLSS_PERFORMANCE, UI_MENU_DLSS_ULTRA}; static const UiTextId fsr3[] = {UI_MENU_FSR3_NATIVE, UI_MENU_DLSS_QUALITY, UI_MENU_DLSS_BALANCED, UI_MENU_DLSS_PERFORMANCE, UI_MENU_DLSS_ULTRA}; // FSR 3.1 set where it cannot load runs nothing (vk_dlaa.c vk_dlaa_fsr3): the game scales the picture as the rest says if (!cfg_bool("NX2NAVI_DLAA") || (!strcmp(cfg_value("NX2NAVI_TAA"), "fsr3") && !vk_ffx_available())) { if (share_native() && wide_scene_settable()) snprintf(out, size, "%s", menu_text(UI_MENU_UPSCALING_NATIVE)); else if (strcmp(cfg_value("NX2NAVI_UPSCALER"), "fsr")) snprintf(out, size, "%s", menu_text(UI_MENU_UPSCALER_BILINEAR)); else snprintf(out, size, menu_text(UI_MENU_OV_FMT_FSR), cfg_int("NX2NAVI_SHARPNESS")); return; } int mode = 0; for (int i = 0; i < 5; i++) if (!strcmp(cfg_value("NX2NAVI_DLSS"), modes[i])) mode = i; if (!strcmp(cfg_value("NX2NAVI_TAA"), "fsr3")) snprintf(out, size, "%s %s", vk_ffx_fsr4() ? fsr4_label() : "AMD FSR 3.1", menu_text(fsr3[mode])); else snprintf(out, size, mode ? "DLSS %s" : "NVIDIA %s", menu_text(dlss[mode])); } // The Anti-aliasing row's value for the performance overlay: what row_aa shows, with "TAA + " before it while DLSS's or // FSR's TAA runs with it (as upscaling_name tells it: FSR 3.1 set where it cannot load runs nothing), and TAA alone // for off then. static void aa_name(char *out, size_t size) { static const char *const values[] = {"game", "off", "fxaa", "smaa"}; static const UiTextId names[] = {UI_MENU_AA_GAME, UI_MENU_FRAMEGEN_OFF, UI_MENU_AA_FXAA, UI_MENU_AA_SMAA}; int i = 0; for (int k = 0; k < 4; k++) if (!strcmp(cfg_value("NX2NAVI_AA"), values[k])) i = k; // taa: the game's SMAA, with the upscaler or alone const bool taa = cfg_bool("NX2NAVI_DLAA") && (strcmp(cfg_value("NX2NAVI_TAA"), "fsr3") || vk_ffx_available()); if (taa && i == 1) snprintf(out, size, "TAA"); else if (taa) snprintf(out, size, "TAA + %s", menu_text(names[i])); else snprintf(out, size, "%s", menu_text(names[i])); } // 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, UI_MENU_DLSS_PERFORMANCE, UI_MENU_DLSS_ULTRA}; // FSR 3.1's modes take the same render scales as DLSS's (Native AA is FSR's name for DLAA's full size) static const UiTextId fsr_names[] = {UI_MENU_FSR3_NATIVE, UI_MENU_DLSS_QUALITY, UI_MENU_DLSS_BALANCED, UI_MENU_DLSS_PERFORMANCE, UI_MENU_DLSS_ULTRA}; // FSR 4 (vk_ffx4.cpp) is the same setting as FSR 3.1 with NX2NAVI_FSR4 on, and takes the same modes: listed where // it runs on this device, after FSR 3.1's. Native (share_native) is listed where the scene's share can be set // (widescreen.c wide_scene_settable), after the two entries that enlarge the game's smaller scene. enum { BILINEAR, FSR1, NATIVE, DLSS, FSR3, FSR4 }; struct Entry { int kind, mode; }; static char text[15][96]; Entry list[18]; const char *names[18]; int n = 0; auto add = [&](int kind, int mode) { list[n] = {kind, mode}; if (kind == BILINEAR) names[n] = menu_text(UI_MENU_UPSCALER_BILINEAR); else if (kind == FSR1) names[n] = menu_text(UI_MENU_UPSCALER_FSR); else if (kind == NATIVE) names[n] = menu_text(UI_MENU_UPSCALING_NATIVE); else { char *t = text[(kind == FSR4 ? 10 : kind == FSR3 ? 5 : 0) + mode]; if (kind == DLSS) snprintf(t, sizeof text[0], mode ? "DLSS %s" : "NVIDIA %s", menu_text(mode_names[mode])); else snprintf(t, sizeof text[0], "%s %s", kind == FSR4 ? fsr4_label() : "AMD FSR 3.1", menu_text(fsr_names[mode])); names[n] = t; } n++; }; // what is set now Entry now = {share_native() ? NATIVE : strcmp(cfg_value("NX2NAVI_UPSCALER"), "fsr") ? BILINEAR : FSR1, 0}; if (cfg_bool("NX2NAVI_DLAA")) { now = {strcmp(cfg_value("NX2NAVI_TAA"), "fsr3") ? DLSS : vk_ffx_fsr4() ? FSR4 : FSR3, 0}; // set where it does for (int i = 0; i < 5; i++) // not run: FSR 3.1 does if (!strcmp(cfg_value("NX2NAVI_DLSS"), modes[i])) now.mode = i; } const bool dlss = dlss_offered(), fsr = vk_ffx_available(), fsr4 = fsr && vk_ffx4_available(); add(BILINEAR, 0); add(FSR1, 0); if (wide_scene_settable() || now.kind == NATIVE) add(NATIVE, 0); for (int i = 0; dlss && i < 5; i++) add(DLSS, i); for (int i = 0; fsr && i < 5; i++) add(FSR3, i); for (int i = 0; fsr4 && i < 5; i++) add(FSR4, i); if (!dlss && now.kind == DLSS) add(DLSS, now.mode); if (!fsr && now.kind == FSR3) add(FSR3, now.mode); int cur = 0; for (int i = 0; i < n; i++) if (list[i].kind == now.kind && list[i].mode == now.mode) cur = i; // FSR 3.1 still set where it cannot load: nothing upscales, so the row says so under its value. A scene larger than // the render-target heap the start made (Native, or DLAA chosen in a run that started with a smaller scene) waits // for a restart; at the heap's limit for this picture it stays smaller, and the note says how large it is // (widescreen.c wide_scene_restart, heap_share) const char *note = nullptr; static char in_use[64]; const double share = wide_scene_settable() ? wide_scene_share() : 0; if (!fsr && now.kind == FSR3) note = menu_text(UI_MENU_FSR3_OFF); else if (upscaling_restart() || (share > 0 && wide_scene_restart())) note = menu_text(UI_MENU_AFTER_RESTART); else if (share > 0 && wide_scene_wanted() > share + 0.001) { char pct[16]; snprintf(pct, sizeof pct, "%.0f %%", share * 100); snprintf(in_use, sizeof in_use, menu_text(UI_MENU_PRESENT_IN_USE), pct); note = in_use; } 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); if (nv < 0 || nv >= n) return; const Entry e = list[nv]; opt_apply("NX2NAVI_UPSCALER", e.kind == FSR1 ? "fsr" : "bilinear"); opt_apply("NX2NAVI_DLAA", e.kind >= DLSS ? "1" : "0"); // Native is scene_share = 1. Leaving it unsets the key (the game's own share, widescreen.c PLAIN_SHARE); a share of // the whole picture set by hand beside DLSS or FSR stays while one of their modes is chosen if (e.kind == NATIVE) opt_apply("NX2NAVI_SCENE_SHARE", "1"); else if (share_native() && (e.kind < DLSS || now.kind == NATIVE)) opt_apply("NX2NAVI_SCENE_SHARE", ""); if (e.kind >= DLSS) opt_apply("NX2NAVI_DLSS", modes[e.mode]); if (e.kind >= DLSS) opt_apply("NX2NAVI_TAA", e.kind >= FSR3 ? "fsr3" : "dlss"); if (e.kind >= FSR3) opt_apply("NX2NAVI_FSR4", e.kind == FSR4 ? "1" : "0"); // FSR 4 that failed in a row stays listed (it runs on this device) while FSR 3.1 serves: the row shows FSR 3.1, and // choosing FSR 4 again tries it anew if (e.kind == FSR4) vk_ffx4_retry(); // The Anti-aliasing row keeps its choice, which runs along with DLSS's or FSR's TAA (vk_aa.c vk_aa_mode); its TAA // went with the temporal mode, so leaving that leaves the game's SMAA if (e.kind < DLSS && !strcmp(cfg_value("NX2NAVI_AA"), "taa")) opt_apply("NX2NAVI_AA", "game"); } // Anti-aliasing (vk_aa.c NX2NAVI_AA): the game's own SMAA (the default), off, FXAA or SMAA in its place, or TAA, which is // DLSS or FSR at the scene's size, the Upscaling row's temporal modes. Without a temporal mode set (dlaa), TAA is listed // where one runs: NVIDIA DLAA where DLSS is offered and taa does not name FSR, else FSR 4 Native AA where it runs, else // FSR 3.1 Native AA where it loads; choosing it sets that native mode, as the Upscaling row would. While a temporal mode // is set its TAA always runs, and the row picks what runs with it: the game's SMAA, nothing, FXAA or SMAA, noted as joined // by the Upscaling row's TAA. A choice then leaves the upscaler and frame generation as they are. taa itself, set before // or in the ini, shows as the game's SMAA, which is what runs with the upscaler (or without one, where it cannot run). static void row_aa(void) { enum { GAME, OFF, FXAA, SMAA, TAA }; static const char *const values[] = {"game", "off", "fxaa", "smaa", "taa"}; const bool dlss = dlss_offered(), fsr = vk_ffx_available(), fsr4 = fsr && vk_ffx4_available(); const bool temporal = cfg_bool("NX2NAVI_DLAA"), fsr_set = !strcmp(cfg_value("NX2NAVI_TAA"), "fsr3"); const char *names[5] = {menu_text(UI_MENU_AA_GAME), menu_text(UI_MENU_FRAMEGEN_OFF), menu_text(UI_MENU_AA_FXAA), menu_text(UI_MENU_AA_SMAA), nullptr}; int n = 4; enum { NONE, K_DLSS, K_FSR3, K_FSR4 }; const int kind = dlss && (!fsr_set || !fsr) ? K_DLSS : fsr4 ? K_FSR4 : fsr ? K_FSR3 : NONE; static char taa[96]; if (!temporal && kind) { if (kind == K_FSR4) snprintf(taa, sizeof taa, menu_text(UI_MENU_AA_TAA_FSR4), fsr4_label()); else snprintf(taa, sizeof taa, "%s", menu_text(kind == K_DLSS ? UI_MENU_AA_TAA_DLSS : UI_MENU_AA_TAA_FSR)); names[n++] = taa; } int cur = GAME; for (int i = 0; i < 4; i++) if (!strcmp(cfg_value("NX2NAVI_AA"), values[i])) cur = i; // FSR 3.1 set where it cannot load runs nothing (the Upscaling row says so): no TAA joins the choice then const char *note = temporal && (!fsr_set || fsr) ? menu_text(UI_MENU_AA_WITH_TAA) : nullptr; int nv = opt_choice_index("NX2NAVI_AA", UI_MENU_AA, names, n, cur, GAME, UI_MENU_AA_DESC, note); if (nv < 0 || nv >= n || nv == cur) return; if (nv == TAA) { // listed only without a temporal mode: the native one of that upscaler, as row_upscaling sets it opt_apply("NX2NAVI_UPSCALER", "bilinear"); opt_apply("NX2NAVI_DLAA", "1"); opt_apply("NX2NAVI_DLSS", "dlaa"); opt_apply("NX2NAVI_TAA", kind == K_DLSS ? "dlss" : "fsr3"); if (kind != K_DLSS) opt_apply("NX2NAVI_FSR4", kind == K_FSR4 ? "1" : "0"); if (kind == K_FSR4) vk_ffx4_retry(); } opt_apply("NX2NAVI_AA", values[nv]); } // Under the HDR value: "after restart" when what is set waits for one (the game's own HDR, to or from it: // vk_hdr_needs_restart), "in use: off" when HDR is on but the swapchain is not HDR10 (the display or Windows' HDR // switch is off, vk_hdr_surface_format). static const char *hdr_note(void) { if (vk_hdr_needs_restart()) return menu_text(UI_MENU_AFTER_RESTART); if (!vk_hdr_wanted() || vk_hdr_active() || !*gfx_window_present_mode() || !cfg_bool("NX2NAVI_HDR")) return nullptr; static char note[96]; snprintf(note, sizeof note, menu_text(UI_MENU_PRESENT_IN_USE), menu_text(UI_MENU_FRAMEGEN_OFF)); return note; } // HDR (vk_hdr.c), two kinds: the game's picture tone mapped to the display (1, one fixed look), switched on and off at // once, or the game's own HDR (native), which the game takes up at its start. The three brightness rows follow while // either is on. static void row_hdr(void) { static const char *const values[] = {"0", "1", "native"}; const char *names[] = {menu_text(UI_MENU_FRAMEGEN_OFF), menu_text(UI_MENU_HDR_TONEMAPPED), menu_text(UI_MENU_HDR_NATIVE)}; opt_choice_list("NX2NAVI_HDR", UI_MENU_HDR, values, names, 3, UI_MENU_HDR_DESC, -1, hdr_note()); if (!cfg_bool("NX2NAVI_HDR")) return; opt_slider("NX2NAVI_HDR_PEAK_NITS", UI_MENU_HDR_PEAK, 400, 10000, UI_MENU_HDR_PEAK_DESC, 50, "%.0f nits"); opt_slider("NX2NAVI_HDR_GAME_NITS", UI_MENU_HDR_GAME, 48, 500, UI_MENU_HDR_GAME_DESC, 1, "%.0f nits"); opt_slider("NX2NAVI_HDR_UI_NITS", UI_MENU_HDR_UI, 48, 500, UI_MENU_HDR_UI_DESC, 1, "%.0f nits"); } // DLSS frame generation's multiplier (NX2NAVI_FRAMEGEN_MULTIPLIER, vk_dlssg.c, at once): 2x, 3x or 4x as far as this // GPU and driver allow (vk_dlssg_multiplier_max, from Streamline). While DLSS's frame generation runs, the row lists only // those and shows the value in effect, a higher setting clamped to it, with a note saying why the rest are missing; // before the restart that starts it, or before Streamline reported its limit, it lists all three. Left out while it is // set but does not run: the frame generation row says why. static void row_framegen_mult(bool restart) { if (!restart && !vk_dlssg_active()) return; int old_dll = 0, max = restart ? 0 : vk_dlssg_multiplier_max(&old_dll); const char *note = nullptr; static char upto[64]; if (max == 2) note = menu_text(old_dll ? UI_MENU_FRAMEGEN_MULT_OLD_DLL : UI_MENU_FRAMEGEN_MULT_RTX50); else if (max == 3) snprintf(upto, sizeof upto, menu_text(UI_MENU_FRAMEGEN_MULT_UPTO), max), note = upto; if (!max) max = 4; // not known yet: every multiplier, the setting as it is static const char *const values[] = {"2", "3", "4"}; static const char *const names[] = {"2x", "3x", "4x"}; int n = max - 1, cur = vk_dlssg_multiplier_of(cfg_value("NX2NAVI_FRAMEGEN_MULTIPLIER"), max, nullptr) - 2; int nv = opt_choice_index("NX2NAVI_FRAMEGEN_MULTIPLIER", UI_MENU_FRAMEGEN_MULT, names, n, cur, 0, UI_MENU_FRAMEGEN_MULT_DESC, note); if (nv >= 0 && nv < n) opt_apply("NX2NAVI_FRAMEGEN_MULTIPLIER", values[nv]); } // NVIDIA's frame generation runs through NGX itself, not through Streamline (vk_dlaa.c vk_ngxfg_*, gfx_present.c // ng_*): every build but the Windows one. static bool framegen_ngx(void) { #ifdef _WIN32 return false; #else return true; #endif } // Its multiplier there (cfg.c FG_MULT, gfx_present.c ng_multiplier): 2x up to what the GPU allows, or Dynamic. Left out // while frame generation is set but cannot run: the frame generation row says why. static void row_framegen_mult_ngx(void) { static const char *const mult[] = {"2", "3", "4", "5", "6"}; static const char *const mult_names[] = {"2x", "3x", "4x", "5x", "6x"}; const int max = vk_ngxfg_max(); // generated frames per rendered one if (max < 1) return; const char *values[6], *names[6]; int n = 0; for (; n < max && n < 5; n++) values[n] = mult[n], names[n] = mult_names[n]; const int top = n - 1; values[n] = "dynamic", names[n++] = menu_text(UI_MENU_FG_MULT_DYNAMIC); const char *v = cfg_value("NX2NAVI_FG_MULT"); int cur = strcmp(v, "dynamic") ? top : n - 1; // a multiplier above what the GPU allows shows as the highest for (int i = 0; i < n; i++) if (!strcmp(v, values[i])) cur = i; int nv = opt_choice_index("NX2NAVI_FG_MULT", UI_MENU_FRAMEGEN_MULT, names, n, cur, 0, UI_MENU_FG_MULT_DESC, nullptr); 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); } // The frame generation of the library FSR 4 comes with (vk_fg4.cpp), as the row names it: with the version the library // runs on this card, the machine-learned 4.x on a Radeon RX 9000 and 3.1.x on any other. static const char *fg4_label(void) { static char s[48]; const char *v = vk_fg4_name(); snprintf(s, sizeof s, *v ? "AMD FSR 4 library (%s)" : "AMD FSR 4 library", v); return s; } // It is the one in use: set (NX2NAVI_FSR4_FG) with FSR 4 as the upscaler, where it runs (vk_fg.cpp use4). Without an // upscaler (dlaa = 0) the FSR 3.1 library's runs. static bool fg4_chosen(void) { return cfg_bool("NX2NAVI_FSR4_FG") && cfg_bool("NX2NAVI_DLAA") && vk_ffx_fsr4() && vk_fg4_available() && !vk_fg4_failed(); } // Frame generation (vk_fg.cpp, at once; NVIDIA's after a restart), with any Upscaling choice: vk_dlaa.c makes its depth, // motion vectors and picture without the HUD with an upscaler or without one (fg_only_frame) static void row_framegen(void) { // FSR 3.1 set where it cannot load (vk_ffx_available) has no frame generation const bool dlss = dlss_offered(), fsr = vk_ffx_framegen_available(); // Through NGX (every build but the Windows one) NVIDIA's needs NGX, which comes up only with DLSS upscaling on when // the game starts (vk_ngxfg_wanted): before such a start it waits for a DLSS mode under Upscaling and a restart const bool via_ngx = framegen_ngx() && dlss; const bool dlss_set = cfg_bool("NX2NAVI_DLAA") && strcmp(cfg_value("NX2NAVI_TAA"), "fsr3"); const char *v = cfg_value("NX2NAVI_FRAMEGEN"); int cur = !strcmp(v, "fsr3") ? 1 : !strcmp(v, "dlss") ? 2 : 0; // what the texts promise follows what runs here: frame generation with any choice, NGX's start rule, or none const UiTextId desc = !dlss && !fsr ? UI_MENU_FRAMEGEN_DESC_NONE : via_ngx ? UI_MENU_FRAMEGEN_DESC_NGX : UI_MENU_FRAMEGEN_DESC_ANY; // UI_TEXT_COUNT: no note UiTextId note = !dlss && !fsr ? UI_MENU_FRAMEGEN_NOTE : via_ngx ? UI_MENU_FRAMEGEN_NOTE_NGX : UI_TEXT_COUNT; if (!dlss && !fsr && cur == 0) opt_disable_next(UI_MENU_FRAMEGEN_NEEDS_NONE); else if (via_ngx && !fsr && !vk_ngxfg_wanted() && !dlss_set) opt_disable_next(UI_MENU_FRAMEGEN_NEEDS_DLSS); // Off, AMD's where FSR 3.1 loads and NVIDIA DLSS (vk_dlssg.c, through Streamline and nx2navi_dlssg.dll) on NVIDIA GPUs, // now that Reflex paces it from the frame's start and generated frames are presented; elsewhere one stays listed // while it is set // AMD's has two libraries where FSR 4 runs: the FSR 3.1 library's, and the one of the library FSR 4 comes with, // listed after it as a choice of its own. That one generates only beside FSR 4 upscaling, so choosing it sets the // upscaler to FSR 4 as well; both are NX2NAVI_FRAMEGEN = fsr3, told apart by NX2NAVI_FSR4_FG. enum { OFF, FSR3, FSR4LIB, DLSS }; const bool four = cur == 1 && fg4_chosen(); const bool four_offered = fsr && cfg_bool("NX2NAVI_DLAA") && vk_ffx4_available() && vk_fg4_available() && !vk_fg4_failed(); // beside FSR 4 upscaling only: not offered without an upscaler const int now = cur == 2 ? DLSS : cur == 1 ? (four ? FSR4LIB : FSR3) : OFF; const char *all_names[] = {menu_text(UI_MENU_FRAMEGEN_OFF), "AMD FSR 3.1", fg4_label(), "NVIDIA DLSS"}; int kinds[4]; const char *names[4]; int n = 0, at = 0; for (int i = 0; i < 4; i++) { if ((i == FSR3 && !fsr && cur != 1) || (i == FSR4LIB && !four_offered && !four) || (i == DLSS && !dlss && cur != 2)) continue; if (i == now) at = n; kinds[n] = i, names[n++] = all_names[i]; } // 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 = 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). // Through NGX that is NVIDIA's frame generation library missing beside the game, or a GPU it does not run on. const int st = cur != 2 || restart ? DLSSG_RUNS : ngx ? (vk_ngxfg_max() ? DLSSG_RUNS : DLSSG_NO_FILES) : vk_dlssg_status(); if (st == DLSSG_UNAVAILABLE) note = UI_MENU_FRAMEGEN_UNAVAILABLE; else if (st == DLSSG_NO_FILES) note = UI_MENU_FRAMEGEN_OFF_FILES; // the FSR 4 library's runs beside FSR 4 and has no note of its own int nv = opt_choice_index("NX2NAVI_FRAMEGEN", UI_MENU_FRAMEGEN, names, n, at, 0, desc, (four && !restart) || note == UI_TEXT_COUNT ? NULL : menu_text(note)); if (nv >= 0 && nv < n) { const int k = kinds[nv]; opt_apply("NX2NAVI_FRAMEGEN", k == OFF ? "off" : k == DLSS ? "dlss" : "fsr3"); if (k == FSR4LIB) { opt_apply("NX2NAVI_FSR4_FG", "1"); if (!vk_ffx_fsr4()) opt_apply("NX2NAVI_TAA", "fsr3"), opt_apply("NX2NAVI_FSR4", "1"), vk_ffx4_retry(); } else if (k == FSR3 && cfg_bool("NX2NAVI_FSR4_FG")) { opt_apply("NX2NAVI_FSR4_FG", "0"); } } else if (cur == 2 && dlss && ngx) row_framegen_mult_ngx(); // nothing while NGX's frame generation cannot run else if (cur == 2 && dlss) row_framegen_mult(restart); } // One Resolution row for the picture's height (NX2NAVI_GAME_RES, widescreen.c picture_height: 1080, the game's 1440 or // more) and the window outside fullscreen (NX2NAVI_WINDOW_SIZE): each height is shown as the size of the game's shape at // it (16:9 2560 x 1440, 21:9 3440 x 1440, 32:9 5120 x 1440, the window's own shape where the picture follows the window), // and choosing one sets both. The window takes it at once (gfx_present.c applies a changed size at the next present), // within the monitor, and the picture after a restart. Native draws at the window's size and leaves the window as it is. // As two rows of their own, a window of one size took a picture of another and scaled it for nothing. static void row_resolution(void) { static const int heights[] = {1080, 1440, 1800, 2160, 2880}; static const int wide169[] = {1920, 2560, 3200, 3840, 5120}, wide219[] = {2560, 3440, 4300, 5120, 6880}, wide329[] = {3840, 5120, 6400, 7680, 10240}; enum { N = sizeof heights / sizeof *heights }; const char *aspect = cfg_value("NX2NAVI_GAME_ASPECT"); int mw = 0, mh = 0, ww = 0, wh = 0; int have_mon = gfx_window_monitor_size(&mw, &mh) && mw > 0 && mh > 0; int have_win = sscanf(cfg_value("NX2NAVI_WINDOW_SIZE"), "%dx%d", &ww, &wh) == 2 && ww > 0 && wh > 0; // the window's shape where the picture follows it: the monitor's in fullscreen, else the window's own double shape = cfg_bool("NX2NAVI_FULLSCREEN") && have_mon ? (double)mw / mh : have_win ? (double)ww / wh : 16.0 / 9.0; static char sizes[N][24], names[N + 1][48]; const char *pn[N + 1]; for (int i = 0; i < N; i++) { int w = !strcmp(aspect, "21:9") ? wide219[i] : !strcmp(aspect, "32:9") ? wide329[i] : !strcmp(aspect, "window") ? ((int)(heights[i] * shape + 0.5) & ~1) : wide169[i]; int h = heights[i], fw = w, fh = h; // the window within the monitor, in the same shape (the picture keeps its height and is scaled into it) if (have_mon && (fw > mw || fh > mh)) { double k = (double)mw / fw < (double)mh / fh ? (double)mw / fw : (double)mh / fh; fw = (int)(fw * k) & ~1, fh = (int)(fh * k) & ~1; } snprintf(sizes[i], sizeof sizes[i], "%dx%d", fw, fh); snprintf(names[i], sizeof names[i], "%d × %d", w, h); // U+00D7, the multiplication sign pn[i] = names[i]; } pn[N] = menu_text(UI_MENU_GAME_RES_NATIVE); const char *res = cfg_value("NX2NAVI_GAME_RES"); int cur = -1; for (int i = 0; i < N; i++) if (atoi(res) == heights[i]) cur = i; if (!strcmp(res, "window")) cur = N; int nv = opt_choice_index("NX2NAVI_GAME_RES", UI_MENU_GAME_RES, pn, N + 1, cur, 1, UI_MENU_GAME_RES_DESC); if (nv < 0 || nv > N) return; if (nv == N) { opt_apply("NX2NAVI_GAME_RES", "window"); return; } char h[16]; snprintf(h, sizeof h, "%d", heights[nv]); opt_apply("NX2NAVI_GAME_RES", h); opt_apply("NX2NAVI_WINDOW_SIZE", sizes[nv]); } static void tab_screen(float scale) { opt_begin("##screen", scale); opt_toggle("NX2NAVI_FULLSCREEN", UI_MENU_FULLSCREEN, UI_MENU_FULLSCREEN_DESC); // the window's size is the Resolution row's below where the picture's size can be set (row_resolution); a build // without the widescreen patch sets draws the game's own size and keeps the window row if (!wide_available()) row_window_size(); // On is the default mode, mailbox with its fallbacks (gfx_present.c pick_present_mode): plain fifo would drop the // game to 30 at every slow frame on a 60 Hz display. A fifo already saved also reads as on. opt_toggle_value("NX2NAVI_VK_PRESENT", UI_MENU_PRESENT_VSYNC, UI_MENU_PRESENT_DESC, "mailbox", "immediate", vsync_note()); row_low_latency(); row_gpu(); static const UiTextId perf[] = {UI_MENU_PERF_OFF, UI_MENU_PERF_FPS, UI_MENU_PERF_FULL}; opt_choice("NX2NAVI_PERF_OVERLAY", UI_MENU_PERF_OVERLAY, perf, 3, UI_MENU_PERF_OVERLAY_DESC); opt_group(UI_MENU_GRAPHICS); // The game resolution needs the output and scene patch sets (resolution.c res_adjustable): Ocarina's build has // none and always draws its own size, so the row would change nothing if (res_adjustable()) row_output_res(); // Widescreen needs the patch sets of recomp/patches-v111.txt (widescreen.c wide_available) static const UiTextId wide[] = {UI_MENU_GAME_ASPECT_16_9, UI_MENU_GAME_ASPECT_WINDOW, UI_MENU_GAME_ASPECT_21_9, UI_MENU_GAME_ASPECT_32_9}; if (wide_available()) opt_choice("NX2NAVI_GAME_ASPECT", UI_MENU_GAME_ASPECT, wide, 4, UI_MENU_GAME_ASPECT_DESC); if (wide_available()) row_resolution(); row_upscaling(); row_aa(); row_framegen(); row_hdr(); // The sharpening runs under FSR 1 (vk_fsr.c vk_fsr_record, RCAS) and FSR 3.1 (vk_ffx.cpp, its own RCAS); with // bilinear or DLSS the slider would do nothing int dl = cfg_bool("NX2NAVI_DLAA"), fsr3 = dl && !strcmp(cfg_value("NX2NAVI_TAA"), "fsr3") && vk_ffx_available(); if (!fsr3 && (strcmp(cfg_value("NX2NAVI_UPSCALER"), "fsr") || dl)) opt_disable_next(UI_MENU_SHARPNESS_NEEDS_FSR); opt_slider("NX2NAVI_SHARPNESS", UI_MENU_SHARPNESS, 0, 100, UI_MENU_SHARPNESS_DESC, 5, "%.0f %%"); // Anisotropic filtering of the surface textures that tile (vk_texture.c aniso_raised), at once (vk_sampler_aniso_live) static const char *const aniso[] = {"game", "2", "4", "8", "16"}; 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(); opt_end(); } // ---- the Game tab's Saves group (save_share.c). Export writes the save the game would load next into a zip in the // player's Downloads folder, for another player or, with this run's log and nx2navi.ini, a bug report. Import takes a // zip the player picks in the host's file dialog and leaves its save for the next start (fs_host.c import_pending): the // game holds its progress in memory and would write it back over a save replaced under it. The work runs off the // present thread (a thread of its own, or the dialog's); the rows wait greyed meanwhile, and the line under them says // what the last one did, else what an import did at this start. static PlatMutex g_share_lock = PLAT_MUTEX_INIT; static std::atomic g_share_busy{0}; static int g_share_text = -1; // the last result's UiTextId, -1 none; a format that takes g_share_arg static char g_share_arg[PLAT_PATH_MAX + 64]; static bool g_share_warn; static void share_done(UiTextId text, const char *arg, bool warn) { plat_mutex_lock(&g_share_lock); g_share_text = text; snprintf(g_share_arg, sizeof g_share_arg, "%s", arg ? arg : ""); g_share_warn = warn; plat_mutex_unlock(&g_share_lock); g_share_busy.store(0); } static void share_export_main(void *arg) { const int bug_report = (int)(intptr_t)arg; char dir[PLAT_PATH_MAX], path[PLAT_PATH_MAX + 64]; SaveShareResult r = save_share_default_dir(dir, sizeof dir) ? save_share_export(dir, bug_report, path, sizeof path) : SAVE_SHARE_WRITE_FAILED; if (r == SAVE_SHARE_OK) share_done(UI_MENU_SAVE_EXPORTED, path, false); else share_done(r == SAVE_SHARE_NO_SAVE ? UI_MENU_SAVE_E_NO_SAVE : r == SAVE_SHARE_READ_FAILED ? UI_MENU_SAVE_E_READ_SAVE : UI_MENU_SAVE_E_WRITE, dir, true); } // The file dialog's answer, on its thread: the import's checks and the copy into the profile run here. static void share_zip_picked(const char *path, void *ctx) { (void)ctx; if (!path) { // cancelled: the last line stays g_share_busy.store(0); return; } char detail[160]; SaveShareResult r = save_share_import(path, detail, sizeof detail); UiTextId text; switch (r) { case SAVE_SHARE_OK: text = UI_MENU_SAVE_IMPORT_READY; break; case SAVE_SHARE_READ_FAILED: text = UI_MENU_SAVE_E_READ; break; case SAVE_SHARE_WRITE_FAILED: text = UI_MENU_SAVE_E_STAGE; break; case SAVE_SHARE_OTHER_GAME: text = UI_MENU_SAVE_E_OTHER_GAME; break; case SAVE_SHARE_NEWER: text = UI_MENU_SAVE_E_NEWER; break; case SAVE_SHARE_DAMAGED: text = UI_MENU_SAVE_E_DAMAGED; break; case SAVE_SHARE_UNSAFE: text = UI_MENU_SAVE_E_UNSAFE; break; default: text = UI_MENU_SAVE_E_NOT_SAVE; break; } share_done(text, r == SAVE_SHARE_OK ? detail : path, r != SAVE_SHARE_OK); } static void rows_saves(void) { opt_group(UI_MENU_SAVES); const bool busy = g_share_busy.load() != 0; int start = 0; // 1 export, 2 export for a bug report, 3 import if (busy) opt_disable_next(UI_MENU_SAVE_BUSY); if (opt_button("##save_export", UI_MENU_SAVE_EXPORT, UI_MENU_SAVE_EXPORT_DESC)) start = 1; if (busy) opt_disable_next(UI_MENU_SAVE_BUSY); if (opt_button("##save_export_bug", UI_MENU_SAVE_EXPORT_BUG, UI_MENU_SAVE_EXPORT_BUG_DESC)) start = 2; // a stat per frame while the Game tab shows; the launcher's own import drops a waiting one const bool waiting = save_share_pending() != 0; if (busy) opt_disable_next(UI_MENU_SAVE_BUSY); if (opt_button("##save_import", UI_MENU_SAVE_IMPORT, UI_MENU_SAVE_IMPORT_DESC, waiting ? menu_text(UI_MENU_SAVE_IMPORT_WAITING) : "")) start = 3; if (waiting) { if (busy) opt_disable_next(UI_MENU_SAVE_BUSY); if (opt_button("##save_import_cancel", UI_MENU_SAVE_IMPORT_CANCEL, UI_MENU_SAVE_IMPORT_CANCEL_DESC)) { if (save_share_cancel()) share_done(UI_MENU_SAVE_E_STAGE, "", true); else share_done(UI_MENU_SAVE_IMPORT_CANCELLED, "", false); } } if (start && !busy) { g_share_busy.store(1); if (start == 3) { char dir[PLAT_PATH_MAX]; file_pick(save_share_default_dir(dir, sizeof dir) ? dir : "", "zip", share_zip_picked, NULL); } else if (!plat_thread_spawn(share_export_main, (void *)(intptr_t)(start == 2), "save export", 0)) { share_done(UI_MENU_SAVE_E_READ_SAVE, "", true); } } char line[PLAT_PATH_MAX * 2] = ""; plat_mutex_lock(&g_share_lock); bool warn = g_share_warn; if (g_share_text >= 0) snprintf(line, sizeof line, menu_text((UiTextId)g_share_text), g_share_arg); plat_mutex_unlock(&g_share_lock); const char *done = fs_save_import_done(); if (!line[0] && *done) { if (!strcmp(done, "-")) snprintf(line, sizeof line, "%s", menu_text(UI_MENU_SAVE_IMPORTED_NEW)); else snprintf(line, sizeof line, menu_text(UI_MENU_SAVE_IMPORTED), done); warn = false; } if (line[0]) opt_text_str(line, warn); } static void rows_low_end(void); static void tab_game(float scale) { opt_begin("##game", scale); row_fps_limit(); // frame_rate.c: Ocarina steps by the clock, so the pacer alone sets its rate row_game_lang(); static const UiTextId mode[] = {UI_MENU_MODE_DOCKED, UI_MENU_MODE_HANDHELD}; opt_choice("NX2NAVI_OPERATION_MODE", UI_MENU_OPERATION_MODE, mode, 2, UI_MENU_OPERATION_MODE_DESC); opt_toggle("NX2NAVI_ASSUME_FOCUS", UI_MENU_ASSUME_FOCUS, UI_MENU_ASSUME_FOCUS_DESC); // The message window's typing speed (text_speed.c), where the game version has the patch set text_speed; live. The // names follow the values in their cfg.c order. if (text_speed_available()) { static const UiTextId speed[] = {UI_MENU_TEXT_SPEED_NORMAL, UI_MENU_TEXT_SPEED_FAST, UI_MENU_TEXT_SPEED_INSTANT}; opt_choice("NX2NAVI_TEXT_SPEED", UI_MENU_TEXT_SPEED, speed, 3, UI_MENU_TEXT_SPEED_DESC); } // discord_rpc.c reads the setting on its own thread and connects or clears the status within half a second opt_toggle("NX2NAVI_DISCORD_RPC", UI_MENU_DISCORD_RPC, UI_MENU_DISCORD_RPC_DESC); rows_saves(); // Testing: experimental rows, then the debug menu where the game version has it opt_group_text("Testing"); // vk_exec.c rw_init and tail_enabled read these at start: each row notes the restart and the bar's Restart does // it. A row whose setting does nothing without another one (rw_init) is greyed out until that one is set. // Automatic first, the default (vk_exec.c vk_exec_replay_workers_auto), named with the count it gives on this PC { static const char *const vals[] = {"auto", "0", "1", "2", "3", "4", "5", "6", "7", "8"}; static char auto_name[64]; snprintf(auto_name, sizeof auto_name, "%s (%d)", menu_text(UI_MENU_AUTO), vk_exec_replay_workers_auto(nullptr)); const char *names[] = {auto_name, "0", "1", "2", "3", "4", "5", "6", "7", "8"}; opt_choice_list("NX2NAVI_REPLAY_WORKERS", UI_MENU_REPLAY_WORKERS, vals, names, 10, UI_MENU_REPLAY_WORKERS_DESC); } const char *wset = cfg_value("NX2NAVI_REPLAY_WORKERS"); const bool workers = (!strcmp(wset, "auto") ? vk_exec_replay_workers_auto(nullptr) : atoi(wset)) > 0, pipe = workers && cfg_int("NX2NAVI_REPLAY_PIPE") > 0; if (!workers) opt_disable_next(UI_MENU_NEEDS_WORKERS); opt_slider("NX2NAVI_REPLAY_PIPE", UI_MENU_REPLAY_PIPE, 0, 4, UI_MENU_REPLAY_PIPE_DESC, 1, "%.0f"); if (!pipe) opt_disable_next(workers ? UI_MENU_NEEDS_PIPE : UI_MENU_NEEDS_WORKERS); opt_slider("NX2NAVI_REPLAY_SMALL", UI_MENU_REPLAY_SMALL, 0, 60, UI_MENU_REPLAY_SMALL_DESC, 4, "%.0f"); opt_toggle("NX2NAVI_TAIL_EARLY", UI_MENU_TAIL_EARLY, UI_MENU_TAIL_EARLY_DESC); // The debug menu, a tool for testing with English text of its own: off unless switched on here if (dbg_supported()) { const bool on = cfg_bool("NX2NAVI_DEBUG_MENU"); if (opt_toggle_text("debug_menu", "Debug menu (F9)", "F9 opens a window for testing: the game's flags by name, a teleport to every area's start " "positions, its cutscenes to start, a free camera, the game's speed, and a search of its " "memory with values to set and hold. All of these can break a save, and the game saves by itself after a change " "of area: keep a copy of the save first.", on)) opt_apply("NX2NAVI_DEBUG_MENU", on ? "0" : "1"); } // Last, as its own settings follow it while it is on (rows_low_end). cfg.c LOWEND: the defaults for older graphics // cards, read at start. // Offered on the cards it was written for (vk_low_end_card). On any other the row cannot turn the mode on; while // the mode is on there (set earlier, or in nx2navi.ini) the row stays usable, to turn it off again. if (!cfg_bool("NX2NAVI_LOW_END_GPU") && !cfg_low_end_active() && !vk_low_end_card()) opt_disable_next(UI_MENU_LOWEND_NOT_FOR_CARD); opt_toggle("NX2NAVI_LOW_END_GPU", UI_MENU_LOW_END_GPU, UI_MENU_LOW_END_GPU_DESC); // the mode's settings are read only while it is on (cfg.c LOWEND_ONLY), so their rows wait for the restart too if (cfg_low_end_active()) rows_low_end(); else if (cfg_bool("NX2NAVI_LOW_END_GPU")) opt_text(UI_MENU_LOWEND_RESTART); opt_end(); } static void tab_sound(float scale) { opt_begin("##sound", scale); opt_slider("NX2NAVI_VOLUME", UI_MENU_VOLUME, 0, 100, UI_MENU_VOLUME_DESC, 5, "%.0f %%"); opt_slider("NX2NAVI_MENU_SOUND_VOLUME", UI_MENU_MENU_SOUNDS, 0, 100, UI_MENU_MENU_SOUNDS_DESC, 5, "%.0f %%"); static const UiTextId channels[] = {UI_MENU_CHANNELS_AUTO, UI_MENU_CHANNELS_STEREO, UI_MENU_CHANNELS_51}; opt_choice("NX2NAVI_AUDIO_CHANNELS", UI_MENU_AUDIO_CHANNELS, channels, 3, UI_MENU_AUDIO_CHANNELS_DESC); opt_end(); } // A confirmation is open (Controls: restart, Advanced: reset): B and Esc close it, not the menu; L / R stay on the page. static bool g_modal; // The note under the layout: Automatic with a gamepad connected, the layout it picked ("in use: Xbox"), named // PlayStation for a pad whose bottom button is labelled cross. NULL otherwise. static const char *pad_layout_note(void) { int got = input_pad_layout(); if (got < 0 || strcmp(cfg_value("NX2NAVI_PAD_LAYOUT"), "auto")) return nullptr; static char note[96]; UiTextId name = got != PAD_LAYOUT_XBOX ? UI_MENU_PAD_LAYOUT_NINTENDO : input_pad_label(PAD_SOUTH) == PAD_LABEL_CROSS ? UI_MENU_PAD_LAYOUT_PLAYSTATION : UI_MENU_PAD_LAYOUT_XBOX; snprintf(note, sizeof note, menu_text(UI_MENU_PAD_LAYOUT_IN_USE), menu_text(name)); return note; } // The action a key presses, or -1: the walk key is a key of its own, but Shift is A (dash) in the default bindings, and a // walk key that also presses something walks and does that something at the same time. The bindings come from a // snapshot, and every key of an action is looked at: binds_refresh rebuilds the live table on the polling thread while // the menu draws, and an action may hold more than the two keys its cells show. static int key_action(const InputMap *m, int key) { for (int a = 0; a < ACTION_COUNT; a++) { uint8_t keys[INPUT_BINDING_MAX]; int n = input_map_keys_of(m, a, keys, INPUT_BINDING_MAX); for (int i = 0; i < n; i++) if (keys[i] == key) return a; } return -1; } // The walk key as the handful of keys a player is likely to pick. "none" rather than an empty value closes it: an // unset key falls back to the default in the settings table, the same as a fresh installation. nx2navi.ini takes any name // input_map.c knows (walks of the keyboard sticks, not of the gamepad). A key the current bindings also use says which // action it presses. static void row_walk_key(void) { static const char *const keys[] = {"none", "lshift", "lalt", "lctrl", "tab", "capslock"}; const char *names[sizeof keys / sizeof *keys] = {menu_text(UI_MENU_OFF), "Shift", "Alt", "Ctrl", "Tab", "Caps Lock"}; const char *cur = cfg_value("NX2NAVI_WALK_KEY"); static char note[64]; const char *n = nullptr; InputMap m; input_map_snapshot(&m); for (int k = KEY_A; k < KEY_COUNT && !n; k++) { if (!input_key_name(k) || strcasecmp(input_key_name(k), cur)) continue; int a = key_action(&m, k); if (a >= 0 && input_action_name(a)) snprintf(note, sizeof note, menu_text(UI_MENU_WALK_KEY_TAKEN), input_action_name(a)), n = note; } opt_choice_list("NX2NAVI_WALK_KEY", UI_MENU_WALK_KEY, keys, names, sizeof keys / sizeof *keys, UI_MENU_WALK_KEY_DESC, -1, n); } // The note under the prompts. The settings name a folder of prompts (input_prompts_folder: the prompts and the pad // layout, as a layout that puts A on the right button needs the face buttons named the other way round), but the game // reads its fonts and UI archive once at start (romfs.c overlay_init). While the folder in use differs: "files // missing" when the setup did not write the one asked for, why it was left out at start (damaged, a game resolution // that is not 16:9, a RomFS mod with the same files, NX2NAVI_ROMFS_PATCHES), else "prompts change after restart" with // *restart set: a restart would bring the prompts asked for. The action prompt's diamond of face buttons is laid out // for the pad layout at start too (romfs.c action_prompt_overlay), whatever the prompts: while the layout asks for the // other places, a restart would lay it out again, so *restart is set whatever the note, and with the folder in use the // note is "prompts change after restart". NULL when both match. static const char *prompts_note(bool *restart) { const char *layout = cfg_value("NX2NAVI_PAD_LAYOUT"), *used = romfs_prompts(); const char *want = input_prompts_folder(input_prompts_pick(cfg_get("NX2NAVI_PROMPTS"), layout), layout); int diamond = romfs_action_prompt(); // -1 when it could not be laid out: a restart would not change it *restart = diamond >= 0 && diamond != input_pad_layout_pick(layout, 0); if (want == used || (want && used && !strcmp(want, used))) return *restart ? menu_text(UI_MENU_PROMPTS_RESTART) : nullptr; // A game resolution of another shape needs a folder of its own for the PC prompts (romfs-patches-pc-x), which // the setup does not write: the shape is the reason then, not the setup. int off = romfs_prompts_off(want); if (want && !romfs_prompts_ready(want) && off != ROMFS_PROMPTS_SHAPE) return menu_text(UI_MENU_PROMPTS_MISSING); switch (off) { case ROMFS_PROMPTS_FAILED: return menu_text(UI_MENU_PROMPTS_FAILED); case ROMFS_PROMPTS_SHAPE: return menu_text(UI_MENU_PROMPTS_OFF_SHAPE); case ROMFS_PROMPTS_MODS: return menu_text(UI_MENU_PROMPTS_OFF_MODS); case ROMFS_PROMPTS_PATCHES: return menu_text(UI_MENU_PROMPTS_OFF_PATCHES); default: *restart = true; return menu_text(UI_MENU_PROMPTS_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) + (wide_scene_settable() && wide_scene_share() > 0 && wide_scene_restart() && !upscaling_restart()) + (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 // Cheats tab (restart_rebuild) Restart also lets the next start rebuild for cheats whose last rebuild failed // (cheats_rebuild_allow). The Quit button's confirmation is the same box (g_confirm_quit), and it waits too: the game // runs on while the menu is open, and closing the window finishes only a Commit that has begun (gfx_window_closed, // fs_save_flush), so a save between writing its files and committing them would be dropped. static bool restart_rebuild, g_confirm_quit; static void restart_confirm(float scale) { g_modal = false; ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, 0); const bool open = ImGui::BeginPopupModal("##confirm", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize); ImGui::PopStyleVar(); if (!open) return; g_modal = true; const ImVec2 box = ImGui::GetWindowPos(), box_size = ImGui::GetWindowSize(); skin_panel(ImGui::GetWindowDrawList(), box, ImVec2(box.x + box_size.x, box.y + box_size.y), scale); ImGui::TextUnformatted(menu_text(g_confirm_quit ? UI_MENU_QUIT_CONFIRM : UI_MENU_RESTART_CONFIRM)); bool saving = fs_save_busy() != 0; if (saving || (!g_confirm_quit && !cfg_last_save_ok())) ImGui::TextColored(ImVec4(0.95f, 0.45f, 0.35f, 1), "%s", menu_text(!saving ? UI_MENU_SAVE_FAILED : g_confirm_quit ? UI_MENU_QUIT_SAVING : UI_MENU_RESTART_SAVING)); ImGui::Spacing(); const char *yes = menu_text(g_confirm_quit ? UI_MENU_QUIT : UI_MENU_RESTART_YES), *no = menu_text(UI_MENU_RESET_NO); const ImGuiStyle &st = ImGui::GetStyle(); float bw = fmaxf(ImGui::CalcTextSize(yes).x, ImGui::CalcTextSize(no).x) + st.FramePadding.x * 4; bool close = skin_widget_button(no, ImVec2(bw, 0), scale); if (ImGui::IsWindowAppearing()) ImGui::SetItemDefaultFocus(); ImGui::SameLine(0, st.ItemSpacing.x + skin_cursor_reach(scale)); ImGui::BeginDisabled(saving); if (skin_widget_button(yes, ImVec2(bw, 0), scale)) { if (g_confirm_quit) { gfx_window_request_close(); } else if (cfg_save()) { if (restart_rebuild) cheats_rebuild_allow(); gfx_window_request_restart(); } } ImGui::EndDisabled(); if (ImGui::IsKeyPressed(ImGuiKey_Escape, false) || ImGui::IsKeyPressed(ImGuiKey_GamepadFaceRight, false)) close = true, menu_sound_play(MENU_SOUND_CANCEL); if (close) { ImGui::CloseCurrentPopup(); restart_rebuild = false; } ImGui::EndPopup(); } static void tab_controls(float scale) { opt_begin("##controls", scale); // The mouse camera first, where a player on keyboard and mouse looks for it; both rows are live. How the mouse // reaches the game, where the game version has the direct mode's patch set (mouse_direct.c; the values in their // cfg.c order, stick then direct), and its sensitivity. The slider stops short of 0, which nx2navi.ini takes to // switch the mouse camera and the cursor capture off. if (input_direct_mouse_available()) { static const UiTextId mouse_mode[] = {UI_MENU_MOUSE_STICK, UI_MENU_MOUSE_DIRECT}; opt_choice("NX2NAVI_MOUSE_MODE", UI_MENU_MOUSE_MODE, mouse_mode, 2, UI_MENU_MOUSE_MODE_DESC); } opt_slider("NX2NAVI_MOUSE_SENS", UI_MENU_MOUSE_SENS, 0.05f, 10, UI_MENU_MOUSE_SENS_DESC, 0.05f, "%.2f"); static const UiTextId layout[] = {UI_MENU_PAD_LAYOUT_AUTO, UI_MENU_PAD_LAYOUT_NINTENDO, UI_MENU_PAD_LAYOUT_XBOX, UI_MENU_PAD_LAYOUT_PLAYSTATION}; opt_choice("NX2NAVI_PAD_LAYOUT", UI_MENU_PAD_LAYOUT, layout, 4, UI_MENU_PAD_LAYOUT_DESC, pad_layout_note()); // The setting's values in their cfg.c order (nintendo, xbox, playstation, steamdeck, pc). The game reads its prompts // at start: the row's note says when a restart would bring others, and the bar's Restart button does it. static const UiTextId prompts[] = {UI_MENU_PROMPTS_NINTENDO, UI_MENU_PROMPTS_XBOX, UI_MENU_PROMPTS_PLAYSTATION, UI_MENU_PROMPTS_STEAMDECK, UI_MENU_PROMPTS_PC}; bool restart; opt_choice("NX2NAVI_PROMPTS", UI_MENU_PROMPTS, prompts, 5, UI_MENU_PROMPTS_DESC, prompts_note(&restart)); opt_toggle("NX2NAVI_MOTION", UI_MENU_MOTION, UI_MENU_MOTION_DESC); // The sprint button (sprint.c), where the game version has the patch set sprint; both rows are live. The names // follow the values in their cfg.c order. if (sprint_available()) { static const UiTextId sprint_button[] = {UI_MENU_SPRINT_OFF, UI_MENU_SPRINT_B, UI_MENU_SPRINT_L3}; static const UiTextId sprint_mode[] = {UI_MENU_SPRINT_TOGGLE, UI_MENU_SPRINT_HOLD}; opt_choice("NX2NAVI_SPRINT_BUTTON", UI_MENU_SPRINT_BUTTON, sprint_button, 3, UI_MENU_SPRINT_BUTTON_DESC); opt_choice("NX2NAVI_SPRINT_MODE", UI_MENU_SPRINT_MODE, sprint_mode, 2, UI_MENU_SPRINT_MODE_DESC); } opt_slider("NX2NAVI_RUMBLE_STRENGTH", UI_MENU_RUMBLE_STRENGTH, 0, 300, UI_MENU_RUMBLE_STRENGTH_DESC, 10, "%.0f %%"); opt_slider("NX2NAVI_RUMBLE_CURVE", UI_MENU_RUMBLE_CURVE, 0.2f, 1.5f, UI_MENU_RUMBLE_CURVE_DESC, 0.05f, "%.2f"); opt_toggle("NX2NAVI_RUMBLE_TRIGGERS", UI_MENU_RUMBLE_TRIGGERS, UI_MENU_RUMBLE_TRIGGERS_DESC); // HD rumble can be switched only while the gamepad in use is one whose actuators take the game's values (a Switch // Pro Controller or Joy-Con, input.c hd_probe); with any other the row is greyed out and says what it needs. if (!input_rumble_hd_available()) opt_disable_next(UI_MENU_RUMBLE_HD_NEEDS); opt_toggle("NX2NAVI_RUMBLE_HD", UI_MENU_RUMBLE_HD, UI_MENU_RUMBLE_HD_DESC); opt_group(UI_MENU_GROUP_STICKS); double now[2]; bool pad = input_pad_stick_percent(now) != 0; char stick_note[2][64]; for (int s = 0; s < 2; s++) snprintf(stick_note[s], sizeof stick_note[s], menu_text(UI_MENU_STICK_DEADZONE_NOW), now[s]); opt_slider("NX2NAVI_LSTICK_DEADZONE", UI_MENU_LSTICK_DEADZONE, 0, 50, UI_MENU_STICK_DEADZONE_DESC, 1, "%.0f %%", pad ? stick_note[0] : nullptr, "NX2NAVI_STICK_DEADZONE"); opt_slider("NX2NAVI_RSTICK_DEADZONE", UI_MENU_RSTICK_DEADZONE, 0, 50, UI_MENU_STICK_DEADZONE_DESC, 1, "%.0f %%", pad ? stick_note[1] : nullptr, "NX2NAVI_STICK_DEADZONE"); opt_slider("NX2NAVI_STICK_SENS", UI_MENU_STICK_SENS, 0.1f, 3, UI_MENU_STICK_SENS_DESC, 0.05f, "%.2f"); // The keyboard's sticks are all or nothing: the walk key scales them to the walk scale while it is held // (input.c walk_get) row_walk_key(); opt_slider("NX2NAVI_WALK_SCALE", UI_MENU_WALK_SCALE, 0.2f, 0.9f, UI_MENU_WALK_SCALE_DESC, 0.01f, "%.2f"); binds_rows(scale); opt_end(); binds_after_page(); } // ---- the settings of the low-end GPU mode, under its switch in the Game tab while that is on (tab_game): each is read // where the code it sets reads it static void rows_low_end(void) { opt_text(UI_MENU_LOWEND_INTRO); opt_group(UI_MENU_LOWEND_MEMORY); { // vk_texture.c sweep_released static const char *const values[] = {"", "512", "1024", "2048", "4096", "8192"}; const char *names[] = {menu_text(UI_MENU_AUTO), "512", "1024", "2048", "4096", "8192"}; opt_choice_list("NX2NAVI_TEX_RELEASED_MAX", UI_MENU_LOWEND_RELEASED, values, names, 6, UI_MENU_LOWEND_RELEASED_DESC); } opt_slider("NX2NAVI_MIRROR_HOT", UI_MENU_LOWEND_MIRROR_HOT, 0, 100, UI_MENU_LOWEND_MIRROR_HOT_DESC, 5, "%.0f %%"); { // vk_device.c vram_heap_limits static const char *const values[] = {"auto", "0"}; const char *names[] = {menu_text(UI_MENU_AUTO), menu_text(UI_MENU_OFF)}; opt_choice_list("NX2NAVI_VRAM_HEAP_LIMIT", UI_MENU_LOWEND_HEAP_LIMIT, values, names, 2, UI_MENU_LOWEND_HEAP_LIMIT_DESC); } { // vk_vma_budget.c reserve_bytes static const char *const values[] = {"512", "256", "768", "1024", "0"}; const char *names[] = {"512 MB", "256 MB", "768 MB", "1024 MB", menu_text(UI_MENU_OFF)}; opt_choice_list("NX2NAVI_VRAM_RESERVE_MB", UI_MENU_LOWEND_RESERVE, values, names, 5, UI_MENU_LOWEND_RESERVE_DESC); } { // vk_memory.c upload_batch_cap static const char *const values[] = {"128", "256", "512", "1024", "0"}; const char *names[] = {"128 MB", "256 MB", "512 MB", "1024 MB", menu_text(UI_MENU_LOWEND_NO_LIMIT)}; opt_choice_list("NX2NAVI_UPLOAD_BATCH_MB", UI_MENU_LOWEND_UPLOAD_BATCH, values, names, 5, UI_MENU_LOWEND_UPLOAD_BATCH_DESC); } opt_toggle("NX2NAVI_ASTC_SCRATCH_KEEP", UI_MENU_LOWEND_ASTC_KEEP, UI_MENU_LOWEND_ASTC_KEEP_DESC); { // vk_memory.c freeze_pass: submissions a region stays unwritten before it is frozen static const char *const values[] = {"1800", "1200", "600", "60", "0"}; const char *names[] = {"1800", "1200", "600", "60", menu_text(UI_MENU_OFF)}; opt_choice_list("NX2NAVI_FREEZE_IDLE", UI_MENU_LOWEND_FREEZE, values, names, 5, UI_MENU_LOWEND_FREEZE_DESC); } { // vk_sync.c res_mask: the result kinds the submit wait counts (31 all, 30 all but events) static const char *const values[] = {"31", "30"}; const char *names[] = {menu_text(UI_MENU_LOWEND_RESULTS_ALL), menu_text(UI_MENU_LOWEND_RESULTS_NO_EVENTS)}; opt_choice_list("NX2NAVI_SUBMIT_FINISH_KINDS", UI_MENU_LOWEND_RESULTS, values, names, 2, UI_MENU_LOWEND_RESULTS_DESC); } opt_toggle("NX2NAVI_ZSTD_NATIVE", UI_MENU_LOWEND_ZSTD, UI_MENU_LOWEND_ZSTD_DESC); // ov_zstd.c trusted opt_group(UI_MENU_LOWEND_RENDER); opt_toggle("NX2NAVI_REPLAY_ASYNC", UI_MENU_LOWEND_ASYNC, UI_MENU_LOWEND_ASYNC_DESC); // vk_backend.c const bool native = vk_mesh_shader_native() != 0; // vk_exec_draw.c: the emulation only where the GPU lacks them if (native) opt_disable_next(UI_MENU_LOWEND_MESH_NATIVE); opt_toggle("NX2NAVI_MESH_EMULATION", UI_MENU_LOWEND_MESH_EMU, UI_MENU_LOWEND_MESH_EMU_DESC); { // vk_exec_draw.c foliage_skip static const char *const values[] = {"full", "reduced", "off"}; const char *names[] = {menu_text(UI_MENU_LOWEND_FOLIAGE_FULL), menu_text(UI_MENU_LOWEND_FOLIAGE_REDUCED), menu_text(UI_MENU_OFF)}; opt_choice_list("NX2NAVI_FOLIAGE", UI_MENU_LOWEND_FOLIAGE, values, names, 3, UI_MENU_LOWEND_FOLIAGE_DESC); } if (native) opt_disable_next(UI_MENU_LOWEND_MESH_NATIVE); opt_slider("NX2NAVI_MESH_EMU_MB", UI_MENU_LOWEND_MESH_MB, 64, 2048, UI_MENU_LOWEND_MESH_MB_DESC, 64, "%.0f MB"); if (native) opt_disable_next(UI_MENU_LOWEND_MESH_NATIVE); opt_slider("NX2NAVI_MESH_EMU_TASKS", UI_MENU_LOWEND_MESH_TASKS, 4096, 65536, UI_MENU_LOWEND_MESH_TASKS_DESC, 4096, "%.0f"); if (native) opt_disable_next(UI_MENU_LOWEND_MESH_NATIVE); opt_toggle("NX2NAVI_MESH_EMU_BATCH", UI_MENU_LOWEND_MESH_BATCH, UI_MENU_LOWEND_MESH_BATCH_DESC); // vk_exec_draw.c opt_toggle("NX2NAVI_TEX_CLOBBER", UI_MENU_LOWEND_CLOBBER, UI_MENU_LOWEND_CLOBBER_DESC); static const UiTextId vec[] = {UI_MENU_OFF, UI_MENU_LOWEND_VEC_AUTO, UI_MENU_ON}; // gfx_shader.c patch_vec_index opt_choice("NX2NAVI_VEC_INDEX_FIX", UI_MENU_LOWEND_VEC_INDEX, vec, 3, UI_MENU_LOWEND_VEC_INDEX_DESC); opt_group(UI_MENU_LOWEND_BUILD); // tools/make_game.py reads it from nx2navi.ini at the next Rebuild the Game (guest_opt_ini) opt_toggle_value("NX2NAVI_GUEST_OPT", UI_MENU_LOWEND_GUEST_OPT, UI_MENU_LOWEND_GUEST_OPT_DESC, "2", "1", menu_text(UI_MENU_LOWEND_GUEST_OPT_NOTE)); opt_group(UI_MENU_LOWEND_DIAG); opt_toggle("NX2NAVI_PASS_TIMES", UI_MENU_LOWEND_PASS_TIMES, UI_MENU_LOWEND_PASS_TIMES_DESC); // vk_crumbs.c #ifdef _WIN32 { // vk_dump.c: the keys it knows static const char *const values[] = {"", "pause", "scrolllock", "insert", "f12"}; const char *names[] = {menu_text(UI_MENU_OFF), "Pause", "Scroll Lock", "Insert", "F12"}; opt_choice_list("NX2NAVI_DUMP_KEY", UI_MENU_LOWEND_DUMP_KEY, values, names, 5, UI_MENU_LOWEND_DUMP_KEY_DESC); } #endif } static const UiTextId kTabNames[TAB_COUNT] = {UI_MENU_TAB_SCREEN, UI_MENU_TAB_GAME, UI_MENU_TAB_SOUND, UI_MENU_TAB_CONTROLS}; static int g_tab; // the tab shown static int g_tab_request = -1; // select this tab at the next layout; -1 none static int g_focus_page = -1; // this tab's first row takes the cursor once it is shown; -1 none // A row of another tab: that tab opens with the row under the cursor. static void go_to(int tab, const char *key) { g_tab_request = tab; opt_focus_key(key); } // The game's interface sounds (menu_sound.c) for what the frame did: a press or click on an item, the cursor on another // item, another tab by a click (L, R, Q and E play theirs where they are read: g_tab_sounded). The first frame after // opening only records: the open sound has played, and the cursor going to the first row is part of it. g_sound_fresh // marks that frame, as the frame count alone does not when nothing was laid out while the menu was closed. static bool g_sound_fresh; static int g_tab_sounded = -1; // the tab L / R / Q / E asked for, its sound played static void menu_sounds_frame(void) { ImGuiContext &g = *GImGui; static int last_frame = -2, last_tab = -1; static ImGuiID last_nav; bool fresh = g_sound_fresh || g.FrameCount != last_frame + 1; g_sound_fresh = false; last_frame = g.FrameCount; if (!fresh) { if (g.ActiveId && g.ActiveIdIsJustActivated) menu_sound_play(MENU_SOUND_DECIDE); else if (g.NavId && g.NavId != last_nav) menu_sound_play(MENU_SOUND_CURSOR); if (g_tab != last_tab && last_tab >= 0 && g_tab != g_tab_sounded) menu_sound_play(g_tab > last_tab ? MENU_SOUND_TAB_RIGHT : MENU_SOUND_TAB_LEFT); } if (g_tab != last_tab) g_tab_sounded = -1; last_nav = g.NavId, last_tab = g_tab; } static void layout_menu(uint32_t w, uint32_t h, float scale) { ImGui::GetBackgroundDrawList()->AddRectFilled(ImVec2(0, 0), ImVec2((float)w, (float)h), IM_COL32(10, 6, 2, 150)); float ww = fminf(w * 0.88f, 1400 * scale), wh = fminf(h * 0.86f, 820 * scale); ImGui::SetNextWindowPos(ImVec2(w * 0.5f, h * 0.5f), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); ImGui::SetNextWindowSize(ImVec2(ww, wh), ImGuiCond_Always); ImTextureID frame_tex; const bool framed = menu_art(MENU_ART_FRAME_DECO, &frame_tex, nullptr, nullptr); if (framed) ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0); // the game's frame is the border ImGui::Begin("##menu", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings); if (framed) ImGui::PopStyleVar(); const ImVec2 win_p = ImGui::GetWindowPos(), win_s = ImGui::GetWindowSize(); // The game's ornamented frame (ui/menu_art.cpp; nothing without the RomFS's), in gold. Its lines lie inside the // texture (about 4 % of the width, 7 % of the height in), so the frame is drawn that far outside the window and its // lines run along the window's edge, clear of the title and the rows; unclipped, it may reach past the window. if (framed) { ImDrawList *fdl = ImGui::GetWindowDrawList(); const float ox = 15 * scale, oy = 30 * scale; fdl->PushClipRectFullScreen(); // the art holds the top and left edges with their corners; turned half a turn it gives the bottom and right ones for (int turned = 0; turned < 2; turned++) menu_art_draw9_turned(fdl, MENU_ART_FRAME_DECO, ImVec2(win_p.x - ox, win_p.y - oy), ImVec2(win_p.x + win_s.x + ox, win_p.y + win_s.y + oy), 96 * scale, 0.2f, IM_COL32(236, 196, 112, 235), 0, turned); fdl->PopClipRect(); } if (g_title_font) ImGui::PushFont(g_title_font); { // the Triforce, then "Ocarina of Time" and the page title ImVec2 at = ImGui::GetCursorScreenPos(); float th = ImGui::GetTextLineHeight(); triforce(ImGui::GetWindowDrawList(), at.x + th * 0.6f, at.y + th * 0.02f, th * 0.96f, ImGui::GetColorU32(kGold)); ImGui::SetCursorScreenPos(ImVec2(at.x + th * 1.5f, at.y)); ImGui::TextColored(kGold, "Ocarina of Time"); ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); ImGui::TextUnformatted(menu_text(UI_MENU_TITLE)); } const float title_end = ImGui::GetItemRectMax().x; if (g_title_font) ImGui::PopFont(); // the port version and the game update, right-aligned on the title line: what a bug report needs first; left out // where it would run into the title (a narrow window, a long title) char game[16], version[96]; snprintf(game, sizeof game, "%d.%d.%d", rt_game_version / 100, rt_game_version / 10 % 10, rt_game_version % 10); snprintf(version, sizeof version, "NX2navi %s - game %s", rt_port_version, game); float version_x = win_s.x - ImGui::GetStyle().WindowPadding.x - ImGui::CalcTextSize(version).x; if (win_p.x + version_x >= title_end + ImGui::GetStyle().ItemSpacing.x * 2) { ImGui::SameLine(version_x); ImGui::TextDisabled("%s", version); } // a text field takes Q and E as letters, and a window of a page keeps the tab keys const bool keeps_keys = g_modal || binds_modal() || ImGui::GetIO().WantTextInput; int step = keeps_keys ? 0 : ImGui::IsKeyPressed(ImGuiKey_GamepadR1, false) || ImGui::IsKeyPressed(ImGuiKey_E, false) ? 1 : ImGui::IsKeyPressed(ImGuiKey_GamepadL1, false) || ImGui::IsKeyPressed(ImGuiKey_Q, false) ? -1 : 0; const int ntabs = TAB_COUNT; if (step) g_tab_request = g_tab_sounded = (g_tab + step + ntabs) % ntabs; if (step) menu_sound_play(step > 0 ? MENU_SOUND_TAB_RIGHT : MENU_SOUND_TAB_LEFT); if (g_tab_request >= 0) g_focus_page = g_tab_request; // the new page's first row takes the cursor const ImGuiStyle &st = ImGui::GetStyle(); float bottom = ImGui::GetFrameHeight() + st.ItemSpacing.y * 2; if (ImGui::BeginTabBar("##tabs", ImGuiTabBarFlags_NoTabListScrollingButtons)) { for (int k = 0; k < ntabs; k++) { char label[128]; snprintf(label, sizeof label, " %s ###tab%d", menu_text(kTabNames[k]), k); // the id stays per language // the tab labels in the title face, on the rows' button (ui/menu_skin.cpp) bool shown = skin_tab_item(label, menu_text(kTabNames[k]), g_tab_request == k ? ImGuiTabItemFlags_SetSelected : 0, scale, g_title_font); if (!shown) continue; g_tab = k; if (g_focus_page == k) { opt_focus_first(); g_focus_page = -1; } ImGui::Spacing(); // the page's rows are a NavFlattened child of this window itself: ImGui clips the items of a direct child // only, so behind a second child level the rows scrolled out above the tabs won D-pad moves from the // buttons and the tabs (up from "Continue" went to the tabs, down from the tabs to "Continue") opt_reserve_bottom(bottom); switch (k) { case TAB_SCREEN: tab_screen(scale); break; case TAB_GAME: tab_game(scale); break; case TAB_SOUND: tab_sound(scale); break; case TAB_CONTROLS: tab_controls(scale); break; } ImGui::EndTabItem(); } g_tab_request = -1; ImGui::EndTabBar(); } // bottom row: the save status on the left, the two buttons on the right, on the game's control-guide bar ImGui::Spacing(); float y = ImGui::GetCursorPosY(); { float sy = win_p.y + y, fh = ImGui::GetFrameHeight(); // within the bottom row's own height: above it the page's last row may still show menu_art_draw(ImGui::GetWindowDrawList(), MENU_ART_GUIDE_BAR, ImVec2(win_p.x + st.WindowPadding.x, sy), ImVec2(win_p.x + win_s.x - st.WindowPadding.x, sy + fh), IM_COL32(96, 70, 36, 170)); } if (!cfg_last_save_ok()) { // stays until a save succeeds; the settings hold for the session ImGui::SetCursorPosY(y + st.FramePadding.y); ImGui::TextColored(ImVec4(0.95f, 0.45f, 0.35f, 1), "%s", menu_text(UI_MENU_SAVE_FAILED)); } // 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 const float buttons_x = fmaxf(st.WindowPadding.x, ImGui::GetWindowWidth() - st.WindowPadding.x - bw * 3 - gap * 2); // Lines of nx2navi.ini that are no settings of this version do nothing (cfg.c cfg_unknown_keys). Said here, in the // save status's place while saving works, so that a line added by hand is not taken for the cause of what the game // does: a player took such a line for the fix of a crash. The list is cut to the room left of the buttons. const char *unknown = cfg_unknown_keys(NULL); if (cfg_last_save_ok() && *unknown) { char note[512]; snprintf(note, sizeof note, menu_text(UI_MENU_INI_UNKNOWN), unknown); size_t keep = strlen(note), head = keep > strlen(unknown) ? keep - strlen(unknown) : keep; const float room = buttons_x - st.WindowPadding.x - gap; while (keep > head && ImGui::CalcTextSize(note).x > room) { keep--; snprintf(note + keep, sizeof note - keep, "..."); } if (keep > head) { ImGui::SetCursorPos(ImVec2(st.WindowPadding.x, y + st.FramePadding.y)); ImGui::TextColored(ImVec4(0.90f, 0.80f, 0.50f, 1), "%s", note); } } ImGui::SetCursorPos(ImVec2(buttons_x, y)); if (skin_widget_button(cont, ImVec2(bw, 0), scale)) set_open(0); ImGui::SameLine(0, gap); if (skin_widget_button(restart, ImVec2(bw, 0), scale)) g_confirm_quit = false, ImGui::OpenPopup("##confirm"); ImGui::SameLine(0, gap); if (skin_widget_button(quit, ImVec2(bw, 0), scale)) g_confirm_quit = true, ImGui::OpenPopup("##confirm"); restart_confirm(scale); menu_sounds_frame(); bool was_modal = g_modal || binds_modal() || ImGui::GetIO().WantTextInput; if (!was_modal && (ImGui::IsKeyPressed(ImGuiKey_Escape, false) || ImGui::IsKeyPressed(ImGuiKey_GamepadFaceRight, false))) set_open(0); ImGui::End(); } static int g_save_pending; // a changed setting still waits for its write to nx2navi.ini (opt_save_tick) // ---- the performance overlay (NX2NAVI_PERF_OVERLAY), in the top right corner. "fps": the frame rate over the presents of // the last half second. "full": a panel of perfmon.c's numbers: // the machine's load and temperatures, the frame rate and frame times with a graph, the render thread's waits. // Off, it costs a cfg_version load per present. static unsigned g_perf_ver = ~0u; static int g_perf_mode; // 0 off, 1 fps, 2 full static std::atomic g_perf_save; // F10 changed the mode: nx2navi.ini is written on the present thread static uint64_t g_perf_last_ns, g_perf_win_ns, g_perf_max_ns; static int g_perf_frames; static char g_perf_text[96]; static int perf_mode_of(const char *s) { return !strcmp(s, "full") ? 2 : !strcmp(s, "fps") ? 1 : 0; } extern "C" void menu_perf_cycle(void) { static const char *const modes[] = {"off", "fps", "full"}; cfg_set("NX2NAVI_PERF_OVERLAY", modes[(perf_mode_of(cfg_value("NX2NAVI_PERF_OVERLAY")) + 1) % 3]); g_perf_save.store(1); } // The device the game renders on, for perfmon's GPU counters (by LUID), NVML (by name) and ADL (by PCI bus, else // name); once, before full mode. static void perf_gpu_once(const MenuTarget *t) { static bool done; if (done) return; done = true; uint32_t ne = 0; vkEnumerateDeviceExtensionProperties(t->phys, NULL, &ne, NULL); VkExtensionProperties *ep = (VkExtensionProperties *)calloc(ne ? ne : 1, sizeof *ep); bool has_bus = false; if (ep && vkEnumerateDeviceExtensionProperties(t->phys, NULL, &ne, ep) >= 0) for (uint32_t i = 0; i < ne && !has_bus; i++) has_bus = !strcmp(ep[i].extensionName, VK_EXT_PCI_BUS_INFO_EXTENSION_NAME); free(ep); VkPhysicalDevicePCIBusInfoPropertiesEXT bus = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PCI_BUS_INFO_PROPERTIES_EXT}; VkPhysicalDeviceIDProperties id = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES, has_bus ? &bus : nullptr}; VkPhysicalDeviceProperties2 p2 = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &id}; vkGetPhysicalDeviceProperties2(t->phys, &p2); VkPhysicalDeviceMemoryProperties mp; vkGetPhysicalDeviceMemoryProperties(t->phys, &mp); uint64_t vram = 0; for (uint32_t h = 0; h < mp.memoryHeapCount; h++) if (mp.memoryHeaps[h].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) vram += mp.memoryHeaps[h].size; perfmon_set_gpu(p2.properties.deviceName, id.deviceLUIDValid ? id.deviceLUID : NULL, has_bus ? (int)bus.pciBus : -1, (float)(vram >> 20)); } static void perf_tick(uint64_t now, const MenuTarget *t) { if (cfg_version() != g_perf_ver) { g_perf_ver = cfg_version(); int m = perf_mode_of(cfg_value("NX2NAVI_PERF_OVERLAY")); if (m != g_perf_mode) { g_perf_last_ns = 0, g_perf_text[0] = 0; if (m == 2) perf_gpu_once(t); perfmon_mode(m); } g_perf_mode = m; } if (g_perf_mode != 1) return; uint64_t d = now - g_perf_last_ns; if (!g_perf_last_ns || d > 2000000000ull) { // first frame, or a stall the window should not average in g_perf_win_ns = g_perf_max_ns = 0; g_perf_frames = 0; } else { g_perf_win_ns += d; g_perf_frames++; if (d > g_perf_max_ns) g_perf_max_ns = d; if (g_perf_win_ns >= 500000000ull) { double out = gfx_output_fps(); // with frame generation: the display's rate too if (out > 0) snprintf(g_perf_text, sizeof g_perf_text, menu_text(UI_PERF_LINE_FPS_FG), g_perf_frames * 1e9 / g_perf_win_ns, out); else snprintf(g_perf_text, sizeof g_perf_text, menu_text(UI_PERF_LINE_FPS), g_perf_frames * 1e9 / g_perf_win_ns); g_perf_win_ns = g_perf_max_ns = 0; g_perf_frames = 0; } } g_perf_last_ns = now; } // The picture lines: the window, the game image the present shows and the 3D scene it holds (resolution.c), then the // settings as the menu's rows name them, so the overlay and the menu agree. static void perf_picture(const MenuTarget *t, PerfPanel *p) { unsigned out[2], scene[2]; res_sizes(out, scene); // Ocarina's build has no output and scene patch sets, so res_sizes holds the other builds' placeholders there: the // picture and the scene are widescreen.c's, the scene at the share it writes for the game (wide_present; in the // first two seconds of a run the game's own dynamic resolution still sizes it) const double share = !res_adjustable() && wide_scene_settable() ? wide_scene_share() : 0; if (share > 0) { wide_picture(&out[0], &out[1]); scene[0] = (unsigned)lround(out[0] * share), scene[1] = (unsigned)lround(out[1] * share); } char (*v)[64] = p->picture; snprintf(v[0], sizeof v[0], menu_text(UI_MENU_OV_FMT_SIZE), t->width, t->height); snprintf(v[1], sizeof v[1], menu_text(UI_MENU_OV_FMT_SIZE), (unsigned)t->image_w, (unsigned)t->image_h); snprintf(v[2], sizeof v[2], menu_text(UI_MENU_OV_FMT_SCENE), scene[0], scene[1], scene[0] * 100.0 / out[0]); upscaling_name(v[3], sizeof v[3]); aa_name(v[4], sizeof v[4]); double hz = frame_step_limit_hz(); snprintf(v[5], sizeof v[5], "%s", menu_text(UI_MENU_OV_NONE)); if (hz > 0) snprintf(v[5], sizeof v[5], menu_text(UI_MENU_OV_FPS), hz); snprintf(v[6], sizeof v[6], "%s", menu_text(strcmp(cfg_value("NX2NAVI_VK_PRESENT"), "immediate") ? UI_MENU_ON : UI_MENU_OFF)); v[7][0] = v[8][0] = 0; if (wide_available()) { const char *a = cfg_value("NX2NAVI_GAME_ASPECT"); snprintf(v[7], sizeof v[7], "%s", menu_text(!strcmp(a, "window") ? UI_MENU_GAME_ASPECT_WINDOW : !strcmp(a, "21:9") ? UI_MENU_GAME_ASPECT_21_9 : !strcmp(a, "32:9") ? UI_MENU_GAME_ASPECT_32_9 : UI_MENU_GAME_ASPECT_16_9)); const char *r = cfg_value("NX2NAVI_GAME_RES"); if (!strcmp(r, "window")) snprintf(v[8], sizeof v[8], "%s", menu_text(UI_MENU_GAME_RES_NATIVE)); else snprintf(v[8], sizeof v[8], "%sp", r); } const char *hdr = cfg_value("NX2NAVI_HDR"); snprintf(v[9], sizeof v[9], "%s", menu_text(!strcmp(hdr, "native") ? UI_MENU_HDR_NATIVE : cfg_bool("NX2NAVI_HDR") ? UI_MENU_HDR_TONEMAPPED : UI_MENU_FRAMEGEN_OFF)); const char *fg = cfg_value("NX2NAVI_FRAMEGEN"); const bool fsr_fg = !strcmp(fg, "fsr3") && vk_ffx_framegen_available(); // set but not loadable: off snprintf(v[10], sizeof v[10], "%s", fsr_fg ? (vk_fg4_active() ? fg4_label() : "AMD FSR 3.1") : !strcmp(fg, "dlss") ? "NVIDIA DLSS" : menu_text(UI_MENU_FRAMEGEN_OFF)); if (!strcmp(fg, "dlss") && vk_dlssg_active()) // its multiplier as set, clamped to what this GPU allows snprintf(v[10], sizeof v[10], "NVIDIA DLSS %dx", vk_dlssg_multiplier_of(cfg_value("NX2NAVI_FRAMEGEN_MULTIPLIER"), vk_dlssg_multiplier_max(nullptr), nullptr)); // The 3D scene's size is known only where resolution.c sets it (the builds with its patch sets); Ocarina sizes its // own scene dynamically and res_sizes would report a fixed 1600x900, so the line is left out p->scene_known = res_adjustable() != 0; } // The full panel (ui/perf_panel.cpp draws it in the menu's look): the numbers of this frame. static void perf_full_draw(const MenuTarget *t, float scale) { static PerfPanel p; perfmon_snapshot(&p.s); // the port version and the game update, formatted once per menu language static char ver[96]; static const char *ver_fmt; if (ver_fmt != menu_text(UI_MENU_VERSION)) { ver_fmt = menu_text(UI_MENU_VERSION); char game[16]; snprintf(game, sizeof game, "%d.%d.%d", rt_game_version / 100, rt_game_version / 10 % 10, rt_game_version % 10); snprintf(ver, sizeof ver, ver_fmt, rt_port_version, game); } p.version = ver; perf_picture(t, &p); p.fg_fps = gfx_output_fps() > 0 ? (float)gfx_output_fps() : 0; p.under_menu = g_open.load(std::memory_order_relaxed) != 0; perf_panel_draw(&p, (float)t->width, scale); } static void perf_draw(const MenuTarget *t, float scale) { if (g_perf_mode == 2) return perf_full_draw(t, scale); float w = (float)t->width; if (!g_perf_mode || !g_perf_text[0]) return; // with the menu open: under its window, over the dimming layout_menu drew before ImDrawList *dl = g_open.load(std::memory_order_relaxed) ? ImGui::GetBackgroundDrawList() : ImGui::GetForegroundDrawList(); ImVec2 sz = ImGui::CalcTextSize(g_perf_text); float padx = 14 * scale, pady = 9 * scale; ImVec2 p(w - 12 * scale - sz.x - padx * 2, 12 * scale); // the top right corner: the game's HUD leaves it empty // a small panel in the menu's look (ui/menu_skin.cpp), its frame scaled to the line skin_panel(dl, p, ImVec2(p.x + sz.x + padx * 2, p.y + sz.y + pady * 2), scale * 0.6f, 0.9f); dl->AddText(ImVec2(p.x + padx, p.y + pady), SKIN_TEXT, g_perf_text); } // The note after F8 (bugcap.c): the capture's number and its folder's name for a few seconds, top centre, in the // menu's look. It shows once the capture is written, so the capture's own picture is without it. static void capture_note(const MenuTarget *t, float scale) { int number = 0, ok = 0; char name[64], line1[160], line2[160] = ""; if (!bugcap_note(&number, &ok, name)) return; snprintf(line1, sizeof line1, menu_text(ok ? UI_MENU_CAPTURE_SAVED : UI_MENU_CAPTURE_FAILED), number); if (ok) snprintf(line2, sizeof line2, menu_text(UI_MENU_CAPTURE_FOLDER), name); ImDrawList *dl = ImGui::GetForegroundDrawList(); ImVec2 s1 = ImGui::CalcTextSize(line1), s2 = ImGui::CalcTextSize(line2); float padx = 16 * scale, pady = 10 * scale, gap = line2[0] ? 4 * scale : 0; float w = (s1.x > s2.x ? s1.x : s2.x) + padx * 2, h = s1.y + (line2[0] ? s2.y : 0) + gap + pady * 2; ImVec2 p(((float)t->width - w) * 0.5f, 12 * scale); skin_panel(dl, p, ImVec2(p.x + w, p.y + h), scale * 0.6f, 0.92f); dl->AddText(ImVec2(p.x + (w - s1.x) * 0.5f, p.y + pady), SKIN_TEXT, line1); if (line2[0]) dl->AddText(ImVec2(p.x + (w - s2.x) * 0.5f, p.y + pady + s1.y + gap), SKIN_TEXT_DIM, line2); } static int overlay_frame(uint64_t frame, const MenuTarget *t, const char **shot) { *shot = NULL; if (t->phys && t->phys != g_gpu_vendor_of) { // the GPU's vendor for the rows that depend on it (dlss_offered) VkPhysicalDeviceProperties props; vkGetPhysicalDeviceProperties(t->phys, &props); g_gpu_vendor.store(props.vendorID, std::memory_order_relaxed); g_gpu_vendor_of = t->phys; } menu_art_start(); // the game's interface textures decode on a worker from the first present on cheats_frame(frame); // the data cheats and the buttons of the cheats that need them held if (!g_test.parsed) test_parse(); if (g_test.next_set < g_test.n_sets && frame >= g_test.sets[g_test.next_set].frame) test_apply_sets(frame); if (g_perf_save.exchange(0)) cfg_save(); perf_tick(plat_now_ns(), t); amiibo_tick(); int cap_number, cap_ok; char cap_name[64]; const bool capture_shows = bugcap_note(&cap_number, &cap_ok, cap_name) != 0; // the note after F8 if (g_booting.load(std::memory_order_relaxed)) g_booting.store(0, std::memory_order_release); // the game runs bool open = g_open.load(std::memory_order_relaxed); // the frame the menu is first laid out after opening: also when its g_reset went to an earlier frame of the overlay // alone (the menu opened between the two loads), the pad's buttons held now must not count as presses static bool was_open; bool opened = open && !was_open; was_open = open; if (!open) { if (g_save_pending) g_save_pending = opt_save_tick(1); // closed: the last change goes to nx2navi.ini now if (g_test.next_at < g_test.n_at && !g_test.opened && frame >= g_test.at[g_test.next_at]) { g_test.opened = frame; g_test.page = 0; g_tab_request = 0; g_debug.store(cfg_get("NX2NAVI_MENU_TEST_DEBUG") != NULL); // the test shoots the debug menu's tabs instead if (g_debug.load()) debug_menu_tab(0); set_open(1); open = true; } else if (!g_perf_mode && !capture_shows) { return 0; } } if (g_failed_flag()) { set_open(0); return 0; } uint64_t t0 = plat_now_ns(); if (!g_ctx_ok) make_context(); float scale = menu_scale(t->width, t->height); if (!vk_up(t) || !fonts_for(scale, t0)) { rt_log("menu: Dear ImGui Vulkan back end set-up failed; the menu stays closed"); g_failed.store(1); set_open(0); return 0; } if (g_style_scale != scale) { ImGui::GetStyle() = g_base_style; ImGui::GetStyle().ScaleAllSizes(scale); g_style_scale = scale; } list_gpus(t->instance); ImGuiIO &io = ImGui::GetIO(); int reset = g_reset.exchange(0); // Read again after taking the reset: the menu may have opened since the read above (F1 on the window thread while // the overlay alone was drawn), and that frame's reset would be spent on an overlay frame, so the first open frame // ran without its focus row and with the opening key still down. open = g_open.load(std::memory_order_acquire); if (reset) { io.ClearEventsQueue(); // events handed over before the close and not yet taken (ImGui spreads them over frames) io.ClearInputKeys(); io.ClearInputMouse(); binds_cancel(); // a cell waiting for a press when the menu closed: no press binds on the next opening g_sound_fresh = true; g_last_ns = 0; g_focus_page = g_tab_request >= 0 ? g_tab_request : g_tab; // opening: the first row has the cursor if (GImGui->OpenPopupStack.Size) ImGui::ClosePopupToLevel(0, false); // a confirmation left open on closing g_modal = false; } io.DisplaySize = ImVec2((float)t->width, (float)t->height); double dt = g_last_ns ? (t0 - g_last_ns) / 1e9 : 1 / 60.0; io.DeltaTime = (float)(dt < 1e-4 ? 1e-4 : dt > 0.1 ? 0.1 : dt); g_last_ns = t0; if (!open) { // the overlay alone: no input reaches ImGui } else if (g_test.page < 0) { drain_events(io); pad_events(io, reset || opened); } else { // the test hook's pages: the user's input in a background test window must not change settings plat_mutex_lock(&ev_lock); ev_n = 0; plat_mutex_unlock(&ev_lock); } // no ImGui_ImplVulkan_NewFrame: all it does is upload Dear ImGui's own font atlas when the back end holds none, // which it never does here (fonts_up uploads it, ui/menu_art.cpp menu_font_upload) ImGui::NewFrame(); if (open && g_debug.load(std::memory_order_relaxed)) { debug_menu_layout(t->width, t->height, scale); // Esc, or B on the pad when no field is being typed in, closes it if (ImGui::IsKeyPressed(ImGuiKey_Escape, false) || (!io.WantTextInput && ImGui::IsKeyPressed(ImGuiKey_GamepadFaceRight, false))) set_open(0); } else if (open) { layout_menu(t->width, t->height, scale); } perf_draw(t, scale); capture_note(t, scale); ImGui::Render(); if (open) g_save_pending = opt_save_tick(0); g_cost_ns += plat_now_ns() - t0; if (g_test.page >= 0 && frame >= g_test.opened + g_test.per_page * (uint64_t)(g_test.page + 1)) { static char path[512]; // this frame's image goes to a file; then the next tab, or the menu closes const char *dir = cfg_get("NX2NAVI_MENU_TEST_DIR"); snprintf(path, sizeof path, "%s/menu-test-%llu-%d.png", dir ? dir : ".", (unsigned long long)frame, g_test.page); *shot = path; if (++g_test.page == (g_debug.load() ? DEBUG_MENU_TABS : TAB_COUNT)) { g_test.page = -1; g_test.opened = 0, g_test.next_at++; // the next NX2NAVI_MENU_TEST frame opens it again set_open(0); // the next frame lays out nothing } else { g_tab_request = g_test.page; if (g_debug.load()) debug_menu_tab(g_test.page); } } return ImGui::GetDrawData()->TotalVtxCount > 0; } // A text field takes typing: the window delivers characters (and a handheld shows its keyboard) while one is active. // Every overlay frame lets the window follow, also those overlay_frame leaves early because the menu is closed: when // only the frames the menu laid out did, closing it with a field active (F1, Minus and Plus) left SDL's text input on, // and on Windows the input method attached to the game window, until the menu opened again. extern "C" int menu_overlay_frame(uint64_t frame, const MenuTarget *t, const char **shot) { int r = overlay_frame(frame, t, shot); menu_text_input_follow(g_ctx_ok && g_open.load(std::memory_order_relaxed) && ImGui::GetIO().WantTextInput); return r; } // ---- the boot screen (boot.c's wait for the prewarm; gfx_present_boot presents it on a black frame) static std::atomic g_boot_phase; static std::atomic g_boot_done, g_boot_total; static uint64_t g_boot_t0; extern "C" void menu_boot_set(int phase, long long done, long long total) { g_boot_done.store(done, std::memory_order_relaxed); g_boot_total.store(total, std::memory_order_relaxed); g_boot_phase.store(phase, std::memory_order_release); } // A box in the middle of the screen in the menu's look (ui/menu_skin.cpp skin_boot_panel): the title and the step with // its count and a bar. The input queue is left alone: keys pressed now reach the menu once the game runs. extern "C" int menu_boot_frame(const MenuTarget *t) { if (g_failed_flag() || g_boot_phase.load(std::memory_order_acquire) < 0) return 0; // -1: done, a black frame if (!g_ctx_ok) make_context(); float scale = menu_scale(t->width, t->height); if (!vk_up(t) || !fonts_for(scale, plat_now_ns())) { rt_log("menu: Dear ImGui Vulkan back end set-up failed; no boot screen, and the menu stays closed"); g_failed.store(1); return 0; } menu_art_start(); // the box takes the game's textures as soon as they are decoded, plain shapes until then if (g_style_scale != scale) { ImGui::GetStyle() = g_base_style; ImGui::GetStyle().ScaleAllSizes(scale); g_style_scale = scale; } uint64_t now = plat_now_ns(); if (!g_boot_t0) g_boot_t0 = now; ImGuiIO &io = ImGui::GetIO(); io.DisplaySize = ImVec2((float)t->width, (float)t->height); double dt = g_last_ns ? (now - g_last_ns) / 1e9 : 1 / 60.0; io.DeltaTime = (float)(dt < 1e-4 ? 1e-4 : dt > 0.1 ? 0.1 : dt); g_last_ns = now; // no ImGui_ImplVulkan_NewFrame: all it does is upload Dear ImGui's own font atlas when the back end holds none, // which it never does here (fonts_up uploads it, ui/menu_art.cpp menu_font_upload) ImGui::NewFrame(); int phase = g_boot_phase.load(std::memory_order_acquire); long long done = g_boot_done.load(std::memory_order_relaxed), total = g_boot_total.load(std::memory_order_relaxed); char step[160]; static const UiTextId steps[] = {UI_BOOT_SHADERS, UI_BOOT_PIPELINES, UI_BOOT_COMPUTE, UI_BOOT_DRIVER_CACHE}; if (phase >= 1 && phase <= 4) snprintf(step, sizeof step, ui_text(steps[phase - 1]), done, total); else snprintf(step, sizeof step, "%s", ui_text(UI_BOOT_STARTING)); const char *title = ui_text(UI_BOOT_TITLE); // the box in the menu's look (ui/menu_skin.cpp): a filled bar while there is a count, else a sweeping segment ImFont *body = ImGui::GetFont(); float body_px = ImGui::GetFontSize(); ImFont *head = g_title_font ? g_title_font : body; float head_px = g_title_font ? g_title_font->FontSize * io.FontGlobalScale : body_px * 1.2f; // as GetFontSize scales float frac = -1; if (phase >= 1 && phase <= 4 && total > 0) frac = done <= 0 ? 0 : done >= total ? 1 : (float)done / (float)total; float s = (float)fmod((now - g_boot_t0) / 1e9 * 0.8, 2.0); skin_boot_panel(ImGui::GetForegroundDrawList(), head, head_px, body, body_px, title, step, frac, s < 1 ? s : 2 - s, (float)t->width, (float)t->height, scale); ImGui::Render(); return ImGui::GetDrawData()->TotalVtxCount > 0; } extern "C" void menu_overlay_draw(VkCommandBuffer cb) { uint64_t t0 = plat_now_ns(); ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), cb); g_cost_ns += plat_now_ns() - t0; if (++g_cost_frames == 600) { rt_log("menu: %.3f ms CPU per open frame (layout and recording, mean of 600)", g_cost_ns / 600 / 1e6); g_cost_ns = g_cost_frames = 0; } }