mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
2365 lines
132 KiB
C++
2365 lines
132 KiB
C++
// 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 <atomic>
|
||
#include <limits.h>
|
||
#include <math.h>
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
|
||
#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<int> g_open; // the menu is open
|
||
static std::atomic<int> 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<int> 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<int> 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<int> 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<int> 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<ImFontGlyphRangesBuilder *>(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<ImWchar> 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=<frame>[,<frame>...][:<frames per page>]: at each such presented frame (up to 8) the menu opens on
|
||
// its first tab; each tab stays <frames per page> frames (default 30) and its last frame's swapchain image goes to
|
||
// NX2NAVI_MENU_TEST_DIR (default: the working directory) as menu-test-<frame>-<page>.png; after the last tab the menu
|
||
// closes.
|
||
// NX2NAVI_MENU_TEST_CLICK=<key>:<part>[,...]: mouse clicks on the rows of the page shown (ui/option_rows.cpp click_test).
|
||
// NX2NAVI_MENU_TEST_SET=<frame>:<key>=<value>[,<key>=<value>...][;<frame>:...]: 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=<key> (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 "<frame>:<key>=<value>,..." 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/<region>.Product.<n>.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<uint32_t> 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<int> 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 (vk_exec.c vk_exec_replay_workers_auto), named with the count it gives on this PC; 0 is the default
|
||
{
|
||
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-<w>x<h>), 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<int> 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<int> g_boot_phase;
|
||
static std::atomic<long long> 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;
|
||
}
|
||
}
|