mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
The Upscaling choice gains FSR 3.1 Native AA, Quality, Balanced, Performance and Ultra Performance. FSR 3.1 runs through AMD's signed amd_fidelityfx_vk.dll (loaded at run time; the build kit ships it) in DLSS's place, on the same scene light, depth and camera motion vectors and at the same render scales. Its Vulkan back end needs VK_KHR_get_memory_requirements2, which the device now enables when FSR 3.1 is chosen. A half-size effect pass with three targets and a half-size depth was taken for a G-buffer, which switched every later frame to the deferred frame shape: there the output pass of a smaller scene was never found, so DLSS Quality, Balanced, Performance and Ultra Performance did nothing on a 16:9 picture. Such a pass smaller than the scene is now left alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1545 lines
78 KiB
C++
1545 lines
78 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"
|
||
#pragma clang diagnostic pop
|
||
#include "ui/amiibo_page.h"
|
||
#include "ui/binds_page.h"
|
||
#include "ui/option_rows.h"
|
||
|
||
extern "C" {
|
||
#include "amiibo.h"
|
||
#include "cheats.h"
|
||
#include "cfg.h"
|
||
#include "fs.h"
|
||
#include "input.h"
|
||
#include "input_map.h"
|
||
#include "mods.h"
|
||
#include "perfmon.h"
|
||
#include "platform/platform.h"
|
||
#include "sysinfo.h"
|
||
#include "ui/menu.h"
|
||
#include "ui_text.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);
|
||
const char *const *vk_color_preset(const char *name);
|
||
double frame_step_limit_hz(void);
|
||
int frame_limit_adjustable(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.
|
||
#pragma clang diagnostic push
|
||
#pragma clang diagnostic ignored "-Wc23-extensions" // #embed in C++
|
||
static const unsigned char roboto_ttf[] = {
|
||
#embed "fonts/Roboto-Medium.ttf"
|
||
};
|
||
#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;
|
||
|
||
// want 0 or 1 sets the state; -1 flips it. Returns the new state.
|
||
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);
|
||
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); }
|
||
|
||
static int g_failed_flag();
|
||
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()) 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_MAX = 256 };
|
||
struct Ev { int type, a, b; float x, y; };
|
||
static PlatMutex ev_lock = PLAT_MUTEX_INIT;
|
||
static Ev ev_q[EV_MAX];
|
||
static int ev_n;
|
||
|
||
static void push(Ev e) {
|
||
if (!menu_is_open()) return;
|
||
plat_mutex_lock(&ev_lock);
|
||
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}); }
|
||
|
||
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); 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;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---- 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. 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 h) {
|
||
float least = 720.0f * (float)gfx_window_dpi() / 96.0f;
|
||
return ((float)h >= least ? (float)h : least) / 1080.0f;
|
||
}
|
||
|
||
// The game's menu colours: a dark umber panel, warm browns for frames and what is selected, gold for the ornaments,
|
||
// accents and the cursor, parchment text. kGreen and kOcarina keep their names for the selected and active controls.
|
||
static const ImVec4 kGold(0.93f, 0.76f, 0.38f, 1), kGreen(0.36f, 0.26f, 0.13f, 1), kNavy(0.075f, 0.058f, 0.042f, 0.95f),
|
||
kOcarina(0.62f, 0.47f, 0.22f, 1);
|
||
static void style_init(ImGuiStyle &s) {
|
||
ImGui::StyleColorsDark(&s);
|
||
s.WindowRounding = 10; s.FrameRounding = 6; s.GrabRounding = 6; s.TabRounding = 6; s.ChildRounding = 8;
|
||
s.WindowPadding = ImVec2(40, 30); s.FramePadding = ImVec2(14, 8); s.ItemSpacing = ImVec2(14, 12);
|
||
s.WindowBorderSize = 2; s.ScrollbarSize = 16;
|
||
ImVec4 *c = s.Colors;
|
||
c[ImGuiCol_WindowBg] = kNavy;
|
||
c[ImGuiCol_Border] = ImVec4(kGold.x, kGold.y, kGold.z, 0.55f);
|
||
c[ImGuiCol_ChildBg] = ImVec4(0, 0, 0, 0);
|
||
c[ImGuiCol_PopupBg] = kNavy;
|
||
c[ImGuiCol_Text] = ImVec4(0.96f, 0.92f, 0.82f, 1);
|
||
c[ImGuiCol_TextDisabled] = ImVec4(0.62f, 0.56f, 0.46f, 1);
|
||
c[ImGuiCol_FrameBg] = ImVec4(0.15f, 0.115f, 0.08f, 1);
|
||
c[ImGuiCol_FrameBgHovered] = ImVec4(0.24f, 0.18f, 0.10f, 1);
|
||
c[ImGuiCol_FrameBgActive] = kGreen;
|
||
c[ImGuiCol_SliderGrab] = kOcarina;
|
||
c[ImGuiCol_SliderGrabActive] = kGold;
|
||
c[ImGuiCol_CheckMark] = kGold;
|
||
c[ImGuiCol_Button] = ImVec4(0.19f, 0.145f, 0.09f, 1);
|
||
c[ImGuiCol_ButtonHovered] = kGreen;
|
||
c[ImGuiCol_ButtonActive] = kOcarina;
|
||
c[ImGuiCol_Header] = ImVec4(0.24f, 0.18f, 0.10f, 1);
|
||
c[ImGuiCol_HeaderHovered] = kGreen;
|
||
c[ImGuiCol_HeaderActive] = kOcarina;
|
||
c[ImGuiCol_Tab] = ImVec4(0.13f, 0.10f, 0.07f, 1);
|
||
c[ImGuiCol_TabHovered] = kGreen;
|
||
c[ImGuiCol_TabSelected] = ImVec4(0.42f, 0.31f, 0.15f, 1);
|
||
c[ImGuiCol_TabSelectedOverline] = kGold;
|
||
c[ImGuiCol_NavCursor] = kGold;
|
||
c[ImGuiCol_Separator] = ImVec4(kGold.x, kGold.y, kGold.z, 0.30f);
|
||
c[ImGuiCol_ScrollbarGrab] = ImVec4(0.36f, 0.28f, 0.16f, 1);
|
||
c[ImGuiCol_ScrollbarBg] = ImVec4(0, 0, 0, 0.2f);
|
||
}
|
||
|
||
// 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";
|
||
style_init(g_base_style);
|
||
ImGui::GetStyle() = g_base_style;
|
||
g_ctx_ok = 1;
|
||
}
|
||
|
||
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 = 16;
|
||
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 (CreateFontsTexture).
|
||
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] = {};
|
||
ImVector<ImWchar> ranges[N];
|
||
int merged = 0;
|
||
for (int i = 0; i < N; i++) {
|
||
char path[512];
|
||
uint64_t size = 0;
|
||
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);
|
||
}
|
||
vk_queue_lock(); // the upload submits on the queue and waits for it
|
||
int ok = ImGui_ImplVulkan_CreateFontsTexture();
|
||
// 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;
|
||
vk_queue_unlock();
|
||
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;
|
||
}
|
||
|
||
extern "C" void menu_vk_release(void) {
|
||
if (g_ctx_ok) vk_down();
|
||
}
|
||
|
||
// ---- test hooks
|
||
// NX2NAVI_MENU_TEST=<frame>[:<frames per page>]: at that presented frame 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_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).
|
||
enum { TAB_SCREEN, TAB_GAME, TAB_SOUND, TAB_CONTROLS, TAB_COUNT }; // Ocarina of Time: the options this port has
|
||
enum { TEST_PAGE_FRAMES = 30, TEST_SETS = 16 };
|
||
struct TestSet { uint64_t frame; char key[64], value[64]; };
|
||
static struct {
|
||
int parsed;
|
||
uint64_t at; // NX2NAVI_MENU_TEST frame; 0 none
|
||
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, -1, {}, 0, 0};
|
||
|
||
static void test_parse(void) {
|
||
g_test.parsed = 1;
|
||
if (const char *e = cfg_get("NX2NAVI_MENU_TEST")) {
|
||
long long at = atoll(e), per = 0;
|
||
if (const char *c = strchr(e, ':')) per = atoll(c + 1);
|
||
g_test.at = at > 0 ? (uint64_t)at : 0;
|
||
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.at || g_test.n_sets)
|
||
rt_log("menu: test hook: open at frame %llu, %llu frames per page, %d settings changes",
|
||
(unsigned long long)g_test.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 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);
|
||
}
|
||
|
||
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.
|
||
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;
|
||
opt_choice_list("NX2NAVI_FPS_LIMIT", UI_MENU_FPS_LIMIT, vals, names, n, UI_MENU_FPS_LIMIT_DESC, cur);
|
||
}
|
||
|
||
// The menu's name of a NX2NAVI_VK_PRESENT mode (gfx_window_present_mode); NULL for another.
|
||
static const char *present_name(const char *mode) {
|
||
static const struct { const char *mode; UiTextId name; } names[] = {
|
||
{"mailbox", UI_MENU_PRESENT_MAILBOX}, {"fifo", UI_MENU_PRESENT_VSYNC}, {"immediate", UI_MENU_PRESENT_IMMEDIATE},
|
||
};
|
||
for (const auto &n : names)
|
||
if (!strcmp(n.mode, mode)) return menu_text(n.name);
|
||
return nullptr;
|
||
}
|
||
|
||
// "in use: VSync" under the Presentation value when the swapchain got another mode than the setting (a driver without
|
||
// mailbox gets fifo); NULL when they agree or before the first swapchain.
|
||
static const char *present_note(void) {
|
||
const char *got = gfx_window_present_mode(), *name = present_name(got);
|
||
if (!*got || !strcmp(got, cfg_value("NX2NAVI_VK_PRESENT")) || !name) return nullptr;
|
||
static char note[96];
|
||
snprintf(note, sizeof note, menu_text(UI_MENU_PRESENT_IN_USE), name);
|
||
return note;
|
||
}
|
||
|
||
// ---- the tabs
|
||
|
||
// One upscaling choice over three settings: NX2NAVI_UPSCALER (how the picture is fitted to the window), NX2NAVI_DLAA
|
||
// (DLSS on) and NX2NAVI_DLSS (its mode, the render scale: widescreen.c wide_present). A DLSS mode fits the picture with
|
||
// bilinear, DLSS having made it; the model is NVIDIA's own for the mode (vk_dlaa.c evaluate, dlss_preset at its default).
|
||
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};
|
||
static char dl[10][96];
|
||
const char *names[12] = {menu_text(UI_MENU_UPSCALER_BILINEAR), menu_text(UI_MENU_UPSCALER_FSR)};
|
||
for (int i = 0; i < 5; i++) {
|
||
snprintf(dl[i], sizeof dl[i], i ? "DLSS %s" : "NVIDIA %s", menu_text(mode_names[i]));
|
||
snprintf(dl[5 + i], sizeof dl[5 + i], "AMD FSR 3.1 %s", menu_text(fsr_names[i]));
|
||
names[2 + i] = dl[i], names[7 + i] = dl[5 + i];
|
||
}
|
||
int cur = strcmp(cfg_value("NX2NAVI_UPSCALER"), "fsr") ? 0 : 1;
|
||
if (cfg_bool("NX2NAVI_DLAA")) {
|
||
int base = strcmp(cfg_value("NX2NAVI_TAA"), "fsr3") ? 2 : 7;
|
||
cur = base;
|
||
for (int i = 0; i < 5; i++)
|
||
if (!strcmp(cfg_value("NX2NAVI_DLSS"), modes[i])) cur = base + i;
|
||
}
|
||
int nv = opt_choice_index("NX2NAVI_UPSCALER", UI_MENU_UPSCALING, names, 12, cur, 1, UI_MENU_UPSCALING_DESC,
|
||
cur >= 2 ? menu_text(UI_MENU_UPSCALING_DLSS_NOTE) : nullptr);
|
||
if (nv < 0) return;
|
||
opt_apply("NX2NAVI_UPSCALER", nv == 1 ? "fsr" : "bilinear");
|
||
opt_apply("NX2NAVI_DLAA", nv >= 2 ? "1" : "0");
|
||
if (nv >= 2) opt_apply("NX2NAVI_DLSS", modes[(nv - 2) % 5]);
|
||
if (nv >= 2) opt_apply("NX2NAVI_TAA", nv >= 7 ? "fsr3" : "dlss");
|
||
}
|
||
|
||
// The picture's height (widescreen.c picture_height): the game's 1440 or more.
|
||
static void row_game_res(void) {
|
||
static const char *const values[] = {"1440", "1800", "2160", "2880", "window"};
|
||
const char *names[] = {"1440p", "1800p", "2160p (4K)", "2880p (5K)", menu_text(UI_MENU_GAME_RES_NATIVE)};
|
||
opt_choice_list("NX2NAVI_GAME_RES", UI_MENU_GAME_RES, values, names, 5, UI_MENU_GAME_RES_DESC);
|
||
}
|
||
|
||
static void tab_screen(float scale) {
|
||
opt_begin("##screen", scale);
|
||
opt_toggle("NX2NAVI_FULLSCREEN", UI_MENU_FULLSCREEN, UI_MENU_FULLSCREEN_DESC);
|
||
row_window_size();
|
||
static const UiTextId present[] = {UI_MENU_PRESENT_MAILBOX, UI_MENU_PRESENT_VSYNC, UI_MENU_PRESENT_IMMEDIATE};
|
||
opt_choice("NX2NAVI_VK_PRESENT", UI_MENU_PRESENT, present, 3, UI_MENU_PRESENT_DESC, present_note());
|
||
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_game_res();
|
||
row_upscaling();
|
||
// RCAS runs only under FSR (vk_fsr.c vk_fsr_record): with bilinear or DLSS the slider would do nothing
|
||
if (strcmp(cfg_value("NX2NAVI_UPSCALER"), "fsr") || cfg_bool("NX2NAVI_DLAA")) opt_disable_next(UI_MENU_SHARPNESS_NEEDS_FSR);
|
||
opt_slider("NX2NAVI_SHARPNESS", UI_MENU_SHARPNESS, 0, 100, UI_MENU_SHARPNESS_DESC, 5, "%.0f %%");
|
||
static const UiTextId aspect[] = {UI_MENU_ASPECT_FIT, UI_MENU_ASPECT_STRETCH};
|
||
opt_choice("NX2NAVI_ASPECT", UI_MENU_ASPECT, aspect, 2, UI_MENU_ASPECT_DESC);
|
||
opt_end();
|
||
}
|
||
|
||
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);
|
||
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"). 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];
|
||
snprintf(note, sizeof note, menu_text(UI_MENU_PAD_LAYOUT_IN_USE),
|
||
menu_text(got == PAD_LAYOUT_XBOX ? UI_MENU_PAD_LAYOUT_XBOX : UI_MENU_PAD_LAYOUT_NINTENDO));
|
||
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", and only then
|
||
// *restart is set: a restart would bring the prompts asked for. NULL when they match.
|
||
static const char *prompts_note(bool *restart) {
|
||
*restart = false;
|
||
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);
|
||
if (want == used || (want && used && !strcmp(want, used))) return 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);
|
||
}
|
||
}
|
||
|
||
// The confirmation of "Restart now", 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 new prompts 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).
|
||
static bool restart_rebuild;
|
||
static void restart_confirm(void) {
|
||
g_modal = false;
|
||
ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(0.5f, 0.5f));
|
||
if (!ImGui::BeginPopupModal("##restart_confirm", nullptr,
|
||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize))
|
||
return;
|
||
g_modal = true;
|
||
ImGui::TextUnformatted(menu_text(UI_MENU_RESTART_CONFIRM));
|
||
bool saving = fs_save_busy() != 0;
|
||
if (saving || !cfg_last_save_ok())
|
||
ImGui::TextColored(ImVec4(0.95f, 0.45f, 0.35f, 1), "%s",
|
||
menu_text(saving ? UI_MENU_RESTART_SAVING : UI_MENU_SAVE_FAILED));
|
||
ImGui::Spacing();
|
||
const char *yes = menu_text(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 = ImGui::Button(no, ImVec2(bw, 0));
|
||
if (ImGui::IsWindowAppearing()) ImGui::SetItemDefaultFocus();
|
||
ImGui::SameLine();
|
||
ImGui::BeginDisabled(saving);
|
||
if (ImGui::Button(yes, ImVec2(bw, 0)) && 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;
|
||
opt_focus_key("##restart"); // back on the row it came from
|
||
}
|
||
ImGui::EndPopup();
|
||
}
|
||
|
||
static void tab_controls(float scale) {
|
||
opt_begin("##controls", scale);
|
||
static const UiTextId layout[] = {UI_MENU_PAD_LAYOUT_AUTO, UI_MENU_PAD_LAYOUT_NINTENDO, UI_MENU_PAD_LAYOUT_XBOX};
|
||
opt_choice("NX2NAVI_PAD_LAYOUT", UI_MENU_PAD_LAYOUT, layout, 3, UI_MENU_PAD_LAYOUT_DESC, pad_layout_note());
|
||
opt_toggle("NX2NAVI_MOTION", UI_MENU_MOTION, UI_MENU_MOTION_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);
|
||
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");
|
||
binds_rows(scale);
|
||
opt_end();
|
||
binds_after_page();
|
||
}
|
||
|
||
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 and R play theirs where they are read). The first frame after opening only records.
|
||
static void menu_sounds_frame(void) {
|
||
ImGuiContext &g = *GImGui;
|
||
static int last_frame = -2, last_tab = -1;
|
||
static ImGuiID last_nav;
|
||
bool fresh = g.FrameCount != last_frame + 1;
|
||
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) menu_sound_play(g_tab > last_tab ? MENU_SOUND_TAB_RIGHT : MENU_SOUND_TAB_LEFT);
|
||
}
|
||
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();
|
||
const float inset = 7 * scale;
|
||
// 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));
|
||
}
|
||
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
|
||
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, "Ocarina Recomp %s - game %s", rt_port_version, game);
|
||
ImGui::SameLine(ImGui::GetWindowWidth() - ImGui::GetStyle().WindowPadding.x - ImGui::CalcTextSize(version).x);
|
||
ImGui::TextDisabled("%s", version);
|
||
|
||
int step = g_modal || binds_modal() ? 0
|
||
: ImGui::IsKeyPressed(ImGuiKey_GamepadR1, false) || ImGui::IsKeyPressed(ImGuiKey_E, false) ? 1
|
||
: ImGui::IsKeyPressed(ImGuiKey_GamepadL1, false) || ImGui::IsKeyPressed(ImGuiKey_Q, false) ? -1
|
||
: 0;
|
||
if (step) g_tab_request = (g_tab + step + TAB_COUNT) % TAB_COUNT;
|
||
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 < TAB_COUNT; k++) {
|
||
char label[128];
|
||
snprintf(label, sizeof label, " %s ###tab%d", menu_text(kTabNames[k]), k); // the id stays per language
|
||
if (g_title_font) ImGui::PushFont(g_title_font); // the tab labels in the title face
|
||
bool shown = ImGui::BeginTabItem(label, nullptr, g_tab_request == k ? ImGuiTabItemFlags_SetSelected : 0);
|
||
if (g_title_font) ImGui::PopFont();
|
||
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));
|
||
}
|
||
const char *cont = menu_text(UI_MENU_CONTINUE), *quit = menu_text(UI_MENU_QUIT);
|
||
float bw = fmaxf(ImGui::CalcTextSize(cont).x, ImGui::CalcTextSize(quit).x) + st.FramePadding.x * 4;
|
||
ImGui::SetCursorPos(ImVec2(ImGui::GetWindowWidth() - st.WindowPadding.x - bw * 2 - st.ItemSpacing.x, y));
|
||
if (ImGui::Button(cont, ImVec2(bw, 0))) set_open(0);
|
||
ImGui::SameLine();
|
||
if (ImGui::Button(quit, ImVec2(bw, 0))) gfx_window_request_close();
|
||
menu_sounds_frame();
|
||
bool was_modal = g_modal || binds_modal();
|
||
if (g_tab != TAB_CONTROLS) g_modal = false;
|
||
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) {
|
||
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 panel keeps its width while the numbers change: every column is as wide as the widest text it can show, measured
|
||
// on samples of 8s (Roboto's digits share one width), and the numbers are right-aligned in it.
|
||
struct PerfCol { UiTextId fmt; float sample; };
|
||
|
||
static float perf_text_w(UiTextId fmt, float v) {
|
||
char b[64];
|
||
snprintf(b, sizeof b, menu_text(fmt), v);
|
||
return ImGui::CalcTextSize(b).x;
|
||
}
|
||
|
||
// A fixed column as wide as the widest of the formats (with their samples) and labels given.
|
||
static void perf_column(const char *id, const PerfCol *cols, int n, const UiTextId *labels = nullptr, int nl = 0) {
|
||
float w = ImGui::CalcTextSize(menu_text(UI_MENU_OV_NONE)).x;
|
||
for (int i = 0; i < n; i++) w = fmaxf(w, perf_text_w(cols[i].fmt, cols[i].sample));
|
||
for (int i = 0; i < nl; i++) w = fmaxf(w, ImGui::CalcTextSize(menu_text(labels[i])).x);
|
||
ImGui::TableSetupColumn(id, ImGuiTableColumnFlags_WidthFixed, w);
|
||
}
|
||
|
||
static void perf_right(const char *s, bool dim) {
|
||
float pad = ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(s).x;
|
||
if (pad > 0) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + pad);
|
||
if (dim) ImGui::TextDisabled("%s", s);
|
||
else ImGui::TextUnformatted(s);
|
||
}
|
||
|
||
// One cell of the full panel's table: fmt (a catalog format, one number) with v, or "-" when the machine gives none.
|
||
static void perf_cell(UiTextId fmt, float v) {
|
||
ImGui::TableNextColumn();
|
||
if (v < 0) return perf_right(menu_text(UI_MENU_OV_NONE), true);
|
||
char b[64];
|
||
snprintf(b, sizeof b, menu_text(fmt), v);
|
||
perf_right(b, false);
|
||
}
|
||
|
||
static void perf_label(UiTextId label, const ImVec4 &c) {
|
||
ImGui::TableNextColumn();
|
||
ImGui::TextColored(c, "%s", menu_text(label));
|
||
}
|
||
|
||
static float perf_gb(float mb) { return mb < 0 ? -1 : mb / 1024; }
|
||
|
||
// The picture lines: the window, the game image the present shows and the 3D scene it holds (resolution.c), how the
|
||
// present scales the image to the window (gfx_present.c, the last present), the frame limit and the presentation mode
|
||
// in use.
|
||
static void perf_picture(const MenuTarget *t, const ImVec4 &c) {
|
||
unsigned out[2], scene[2];
|
||
res_sizes(out, scene);
|
||
char v[6][64];
|
||
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]);
|
||
int sharp = cfg_int("NX2NAVI_SHARPNESS");
|
||
sharp = sharp < 0 ? 0 : sharp > 100 ? 100 : sharp;
|
||
if (t->scaler == 2) {
|
||
snprintf(v[3], sizeof v[3], menu_text(UI_MENU_OV_FMT_FSR), sharp);
|
||
} else if (t->scaler == 3) { // shrunk by the blit, then RCAS (vk_fsr.c shrink_sharpen)
|
||
snprintf(v[3], sizeof v[3], menu_text(UI_MENU_OV_FMT_SHRINK), sharp);
|
||
} else {
|
||
// FSR with sharpness 0 and an image larger than the window: the plain shrink blit (vk_fsr_record)
|
||
int down = t->scaler == 1 && !strcmp(cfg_value("NX2NAVI_UPSCALER"), "fsr") &&
|
||
(t->image_w > (int)t->width || t->image_h > (int)t->height);
|
||
snprintf(v[3], sizeof v[3], "%s",
|
||
menu_text(down ? UI_MENU_OV_SCALE_DOWN : t->scaler ? UI_MENU_OV_SCALE_BILINEAR : UI_MENU_OV_SCALE_NONE));
|
||
}
|
||
double hz = frame_step_limit_hz();
|
||
snprintf(v[4], sizeof v[4], "%s", menu_text(UI_MENU_OV_NONE));
|
||
if (hz > 0) snprintf(v[4], sizeof v[4], menu_text(UI_MENU_OV_FPS), hz);
|
||
const char *pm = present_name(gfx_window_present_mode());
|
||
snprintf(v[5], sizeof v[5], "%s", pm ? pm : menu_text(UI_MENU_OV_NONE));
|
||
static const UiTextId labels[6] = {UI_MENU_OV_WINDOW, UI_MENU_OV_OUTPUT, UI_MENU_OV_SCENE,
|
||
UI_MENU_OV_SCALING, UI_MENU_OV_LIMIT, UI_MENU_OV_PRESENT};
|
||
// 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
|
||
bool scene_known = res_adjustable() != 0;
|
||
if (!ImGui::BeginTable("##perfp", 2, ImGuiTableFlags_SizingFixedFit)) return;
|
||
float vw = 0;
|
||
for (auto &s : v) vw = fmaxf(vw, ImGui::CalcTextSize(s).x);
|
||
ImGui::TableSetupColumn("##p0", ImGuiTableColumnFlags_WidthStretch);
|
||
ImGui::TableSetupColumn("##p1", ImGuiTableColumnFlags_WidthFixed, vw);
|
||
for (int i = 0; i < 6; i++) {
|
||
if (i == 2 && !scene_known) continue;
|
||
perf_label(labels[i], c);
|
||
ImGui::TableNextColumn();
|
||
perf_right(v[i], false);
|
||
}
|
||
ImGui::EndTable();
|
||
}
|
||
|
||
static void perf_full_draw(const MenuTarget *t, float scale) {
|
||
float w = (float)t->width;
|
||
PerfStats s;
|
||
perfmon_snapshot(&s);
|
||
ImGui::SetNextWindowPos(ImVec2(w - 12 * scale, 12 * scale), ImGuiCond_Always, ImVec2(1, 0));
|
||
ImGui::SetNextWindowBgAlpha(0.6f);
|
||
ImGui::Begin("##perf", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize |
|
||
ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav |
|
||
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoSavedSettings);
|
||
// under the menu while it is open (the settings stay readable), else over everything
|
||
if (g_open.load(std::memory_order_relaxed)) ImGui::BringWindowToDisplayBack(ImGui::GetCurrentWindow());
|
||
else ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow());
|
||
ImGui::SetWindowFontScale(0.72f);
|
||
const ImVec4 gpu_c(0.55f, 0.83f, 0.31f, 1), cpu_c(0.18f, 0.59f, 0.80f, 1), ram_c(0.80f, 0.45f, 0.70f, 1),
|
||
fps_c(0.95f, 0.95f, 0.95f, 1), wait_c(0.93f, 0.76f, 0.33f, 1), pic_c(0.62f, 0.70f, 0.95f, 1);
|
||
// the port version and the game update first, so a screenshot in a bug report carries both; 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);
|
||
}
|
||
ImGui::TextDisabled("%s", ver);
|
||
if (ImGui::BeginTable("##perft", 4, ImGuiTableFlags_SizingFixedFit)) {
|
||
static const UiTextId labels[] = {UI_MENU_OV_GPU, UI_MENU_OV_VRAM, UI_MENU_OV_CPU, UI_MENU_OV_RAM,
|
||
UI_MENU_OV_FPS_LABEL, UI_MENU_OV_FRAME_LABEL};
|
||
static const PerfCol c1[] = {{UI_MENU_OV_FMT_PCT, 100}, {UI_MENU_OV_FMT_GB, 888.8f}, {UI_MENU_OV_FMT_FPS, 888},
|
||
{UI_MENU_OV_FRAME_MS, 888.8f}};
|
||
static const PerfCol c2[] = {{UI_MENU_OV_FMT_TEMP, 188}, {UI_MENU_OV_FMT_GB_OF, 888.8f}, {UI_MENU_OV_LOW1, 888},
|
||
{UI_MENU_OV_MAX_MS, 8888.8f}};
|
||
static const PerfCol c3[] = {{UI_MENU_OV_FMT_POWER, 888}, {UI_MENU_OV_FMT_MHZ, 8888}, {UI_MENU_OV_GAME_PCT, 100},
|
||
{UI_MENU_OV_GAME_GB, 888.8f}, {UI_MENU_OV_LOW01, 888}};
|
||
perf_column("##c0", nullptr, 0, labels, 6);
|
||
perf_column("##c1", c1, 4);
|
||
perf_column("##c2", c2, 4);
|
||
perf_column("##c3", c3, 5);
|
||
perf_label(UI_MENU_OV_GPU, gpu_c);
|
||
perf_cell(UI_MENU_OV_FMT_PCT, s.gpu_pct);
|
||
perf_cell(UI_MENU_OV_FMT_TEMP, s.gpu_temp_c);
|
||
perf_cell(UI_MENU_OV_FMT_POWER, s.gpu_power_w);
|
||
perf_label(UI_MENU_OV_VRAM, gpu_c);
|
||
perf_cell(UI_MENU_OV_FMT_GB, perf_gb(s.vram_used_mb));
|
||
perf_cell(UI_MENU_OV_FMT_GB_OF, perf_gb(s.vram_total_mb));
|
||
perf_cell(UI_MENU_OV_FMT_MHZ, s.gpu_clock_mhz);
|
||
perf_label(UI_MENU_OV_CPU, cpu_c);
|
||
perf_cell(UI_MENU_OV_FMT_PCT, s.cpu_total_pct);
|
||
perf_cell(UI_MENU_OV_FMT_TEMP, s.cpu_temp_c);
|
||
perf_cell(UI_MENU_OV_GAME_PCT, s.cpu_game_pct);
|
||
perf_label(UI_MENU_OV_RAM, ram_c);
|
||
perf_cell(UI_MENU_OV_FMT_GB, perf_gb(s.ram_used_mb));
|
||
perf_cell(UI_MENU_OV_FMT_GB_OF, perf_gb(s.ram_total_mb));
|
||
perf_cell(UI_MENU_OV_GAME_GB, perf_gb(s.ram_game_mb));
|
||
ImGui::TableNextRow(ImGuiTableRowFlags_None, 6 * scale); // a gap between the machine and the frames
|
||
// the frames: the rate over the last second with the lows over the last ten seconds, then the frame times
|
||
perf_label(UI_MENU_OV_FPS_LABEL, fps_c);
|
||
perf_cell(UI_MENU_OV_FMT_FPS, s.fps);
|
||
perf_cell(UI_MENU_OV_LOW1, s.low1_fps);
|
||
perf_cell(UI_MENU_OV_LOW01, s.low01_fps);
|
||
perf_label(UI_MENU_OV_FRAME_LABEL, fps_c);
|
||
perf_cell(UI_MENU_OV_FRAME_MS, s.frame_ms_avg);
|
||
perf_cell(UI_MENU_OV_MAX_MS, s.frame_ms_max);
|
||
ImGui::TableNextColumn();
|
||
ImGui::EndTable();
|
||
}
|
||
float top = s.frame_ms_max > 50 ? s.frame_ms_max : 50;
|
||
// as wide as the panel the tables size; the plot does not count toward that width, so the panel can still shrink
|
||
ImGuiWindow *win = ImGui::GetCurrentWindow();
|
||
float max_x = win->DC.CursorMaxPos.x;
|
||
ImGui::PlotLines("##ft", s.frame_ms, PERF_HISTORY, 0, nullptr, 0, top, ImVec2(-FLT_MIN, 48 * scale));
|
||
win->DC.CursorMaxPos.x = max_x;
|
||
// per frame, averaged over the last half second; the labels' column stretches, so the numbers end at the panel's
|
||
// right edge, and the table still asks for no more than its widest label and number
|
||
if (ImGui::BeginTable("##perfw", 2, ImGuiTableFlags_SizingFixedFit)) {
|
||
static const PerfCol c1[] = {{UI_MENU_OV_FMT_MS, 888.88f}, {UI_MENU_OV_PER_FRAME, 888.8f}};
|
||
ImGui::TableSetupColumn("##w0", ImGuiTableColumnFlags_WidthStretch);
|
||
perf_column("##w1", c1, 2);
|
||
perf_label(UI_MENU_OV_REPLAY, wait_c);
|
||
perf_cell(UI_MENU_OV_FMT_MS, s.replay_ms);
|
||
perf_label(UI_MENU_OV_GPU_TIME, wait_c);
|
||
perf_cell(UI_MENU_OV_FMT_MS, s.gpu_ms);
|
||
perf_label(UI_MENU_OV_PIPE_WAIT, wait_c);
|
||
perf_cell(UI_MENU_OV_FMT_MS, s.pipe_wait_ms);
|
||
perf_label(UI_MENU_OV_SHADER_WAIT, wait_c);
|
||
perf_cell(UI_MENU_OV_FMT_MS, s.xlate_ms);
|
||
perf_label(UI_MENU_OV_TEX_UPLOADS, wait_c);
|
||
perf_cell(UI_MENU_OV_PER_FRAME, s.tex_uploads);
|
||
ImGui::EndTable();
|
||
}
|
||
perf_picture(t, pic_c);
|
||
if (s.gpu_name[0]) ImGui::TextDisabled("%s", s.gpu_name);
|
||
if (s.cpu_name[0]) ImGui::TextDisabled("%s", s.cpu_name);
|
||
ImGui::End();
|
||
}
|
||
|
||
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 pad = 6 * scale;
|
||
ImVec2 p(w - 12 * scale - sz.x - pad * 2, 12 * scale); // the top right corner: the game's HUD leaves it empty
|
||
dl->AddRectFilled(p, ImVec2(p.x + sz.x + pad * 2, p.y + sz.y + pad * 2), IM_COL32(0, 0, 0, 150), 4 * scale);
|
||
dl->AddText(ImVec2(p.x + pad, p.y + pad), IM_COL32(255, 255, 255, 235), g_perf_text);
|
||
}
|
||
|
||
extern "C" int menu_overlay_frame(uint64_t frame, const MenuTarget *t, const char **shot) {
|
||
*shot = NULL;
|
||
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();
|
||
bool open = g_open.load(std::memory_order_relaxed);
|
||
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.at && !g_test.opened && frame >= g_test.at) {
|
||
g_test.opened = frame;
|
||
g_test.page = 0;
|
||
g_tab_request = 0;
|
||
set_open(1);
|
||
open = true;
|
||
} else if (!g_perf_mode) {
|
||
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->height);
|
||
if (!vk_up(t) || (g_font_scale != scale && !fonts_up(scale))) {
|
||
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);
|
||
if (reset) {
|
||
io.ClearInputKeys();
|
||
io.ClearInputMouse();
|
||
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);
|
||
} 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);
|
||
}
|
||
ImGui_ImplVulkan_NewFrame();
|
||
ImGui::NewFrame();
|
||
if (open) layout_menu(t->width, t->height, scale);
|
||
perf_draw(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 == TAB_COUNT) {
|
||
g_test.page = -1;
|
||
set_open(0); // the next frame lays out nothing
|
||
} else {
|
||
g_tab_request = g_test.page;
|
||
}
|
||
}
|
||
return ImGui::GetDrawData()->TotalVtxCount > 0;
|
||
}
|
||
|
||
// ---- 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 panel in the middle of the screen in the menu's colours: the title, the step with its count and a bar, and why it
|
||
// happens. 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->height);
|
||
if (!vk_up(t) || (g_font_scale != scale && !fonts_up(scale))) {
|
||
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;
|
||
}
|
||
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;
|
||
ImGui_ImplVulkan_NewFrame();
|
||
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];
|
||
if (phase == 1 || phase == 2)
|
||
snprintf(step, sizeof step, ui_text(phase == 1 ? UI_BOOT_SHADERS : UI_BOOT_PIPELINES), done, total);
|
||
else
|
||
snprintf(step, sizeof step, "%s", ui_text(UI_BOOT_STARTING));
|
||
const char *title = ui_text(UI_BOOT_TITLE), *hint = ui_text(UI_BOOT_HINT);
|
||
|
||
ImDrawList *dl = ImGui::GetForegroundDrawList();
|
||
float w = (float)t->width, h = (float)t->height, pad = 34 * scale, gap = 16 * scale;
|
||
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 : body_px * 1.2f;
|
||
ImVec2 ts = head->CalcTextSizeA(head_px, FLT_MAX, 0, title), ss = body->CalcTextSizeA(body_px, FLT_MAX, 0, step),
|
||
hs = body->CalcTextSizeA(body_px * 0.8f, FLT_MAX, 0, hint);
|
||
float bar_h = 10 * scale;
|
||
float pw = fmaxf(fmaxf(ts.x, hs.x), 560 * scale) + pad * 2;
|
||
float ph = pad + ts.y + gap * 1.5f + ss.y + gap * 0.6f + bar_h + gap * 1.4f + hs.y + pad;
|
||
ImVec2 a((w - pw) / 2, (h - ph) / 2), b(a.x + pw, a.y + ph);
|
||
ImU32 gold = ImGui::GetColorU32(kGold), panel = ImGui::GetColorU32(kNavy);
|
||
ImU32 text = IM_COL32(236, 224, 196, 255), dim = IM_COL32(236, 224, 196, 150);
|
||
dl->AddRectFilled(a, b, panel, 10 * scale);
|
||
dl->AddRect(a, b, IM_COL32(237, 194, 97, 140), 10 * scale, 0, 1.5f * scale);
|
||
float y = a.y + pad;
|
||
dl->AddText(head, head_px, ImVec2((w - ts.x) / 2, y), gold, title);
|
||
y += ts.y + gap * 1.5f;
|
||
dl->AddText(body, body_px, ImVec2(a.x + pad, y), text, step);
|
||
y += ss.y + gap * 0.6f;
|
||
ImVec2 ba(a.x + pad, y), bb(b.x - pad, y + bar_h);
|
||
dl->AddRectFilled(ba, bb, IM_COL32(255, 255, 255, 28), bar_h / 2);
|
||
if ((phase == 1 || phase == 2) && total > 0) {
|
||
float f = (float)done / (float)total;
|
||
f = f < 0 ? 0 : f > 1 ? 1 : f;
|
||
if (f > 0) dl->AddRectFilled(ba, ImVec2(ba.x + (bb.x - ba.x) * f, bb.y), gold, bar_h / 2);
|
||
} else { // no count yet: a short segment sweeps along the bar
|
||
float span = (bb.x - ba.x) * 0.2f, run = (bb.x - ba.x) - span;
|
||
float s = (float)fmod((now - g_boot_t0) / 1e9 * 0.8, 2.0);
|
||
float x = ba.x + run * (s < 1 ? s : 2 - s);
|
||
dl->AddRectFilled(ImVec2(x, ba.y), ImVec2(x + span, bb.y), gold, bar_h / 2);
|
||
}
|
||
y += bar_h + gap * 1.4f;
|
||
dl->AddText(body, body_px * 0.8f, ImVec2((w - hs.x) / 2, y), dim, hint);
|
||
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;
|
||
}
|
||
}
|