mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
The game sizes its scene each frame from a GPU-time controller. The new patch set res_fixed takes the controller's fixed path and reads the scale from the patch page, so DLSS Quality, Balanced, Performance and Ultra Performance render at 67, 58, 50 and 33 percent of the picture and DLAA at all of it. The set goes on once the controller has run for a few frames, and the menu's Upscaling choice lists the modes and switches them live. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1539 lines
77 KiB
C++
1539 lines
77 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};
|
||
static char dl[5][96];
|
||
const char *names[7] = {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]));
|
||
names[2 + i] = dl[i];
|
||
}
|
||
int cur = strcmp(cfg_value("NX2NAVI_UPSCALER"), "fsr") ? 0 : 1;
|
||
if (cfg_bool("NX2NAVI_DLAA")) {
|
||
cur = 2;
|
||
for (int i = 0; i < 5; i++)
|
||
if (!strcmp(cfg_value("NX2NAVI_DLSS"), modes[i])) cur = 2 + i;
|
||
}
|
||
int nv = opt_choice_index("NX2NAVI_UPSCALER", UI_MENU_UPSCALING, names, 7, 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]);
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
}
|