Files
totk-pc-recomp/runtime/ui/menu.cpp
T
Valiantsin DzerakhandClaude Opus 5.5 beb93276eb Offer every game language the build ships in the F1 menu (#580)
The game language row listed eleven languages. 1.4.3 also ships Brazilian Portuguese (Mals USpt), and every build
ships British English, Canadian French and Latin American Spanish (EUen, USfr, USes), which ov_language already
accepted. ui_text_game_langs lists them per game build: pt-BR from 1.4.3 on, since 1.2.1 has no USpt. A row selects
only its own code, so en-GB and es-419 no longer show as English and Spanish.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 20:09:22 +03:00

1556 lines
78 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 follows LAS Recomp's runtime/nvn/vulkan/imgui_glue.cpp (https://github.com/peiikai/LASRecomp @
// da35b83a9611d046a9a681aae668d7085954d37f, MIT, Copyright (c) 2026 Peiikai): 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) follows
// SavageWindRecomp's runtime/ui/menu.mm (https://github.com/Revivedx/SavageWindRecomp @
// 575ad88e02f9a89a9a885d90039d48af33c982f4, MIT, Copyright (c) 2026 Revivedx and the Savage Wind Recomp contributors,
// Copyright (c) 2026 Peiikai); the pages are rows of option_rows.cpp.
#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"
#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]);
double frame_step_limit_hz(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);
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;
}
// SavageWindRecomp's colours: dark green panels, gold accents.
static void style_init(ImGuiStyle &s) {
ImGui::StyleColorsDark(&s);
s.WindowRounding = 14; s.FrameRounding = 8; s.GrabRounding = 8; s.TabRounding = 8; s.ChildRounding = 10;
s.WindowPadding = ImVec2(28, 22); s.FramePadding = ImVec2(14, 8); s.ItemSpacing = ImVec2(14, 12);
s.WindowBorderSize = 0; s.ScrollbarSize = 18;
ImVec4 *c = s.Colors;
ImVec4 gold(0.93f, 0.76f, 0.33f, 1), green(0.15f, 0.36f, 0.25f, 1);
c[ImGuiCol_WindowBg] = ImVec4(0.06f, 0.09f, 0.08f, 0.94f);
c[ImGuiCol_ChildBg] = ImVec4(0, 0, 0, 0);
c[ImGuiCol_Text] = ImVec4(0.96f, 0.94f, 0.88f, 1);
c[ImGuiCol_TextDisabled] = ImVec4(0.62f, 0.64f, 0.58f, 1);
c[ImGuiCol_Button] = ImVec4(0.14f, 0.22f, 0.18f, 1);
c[ImGuiCol_ButtonHovered] = green;
c[ImGuiCol_ButtonActive] = ImVec4(0.21f, 0.50f, 0.34f, 1);
c[ImGuiCol_Tab] = ImVec4(0.12f, 0.18f, 0.15f, 1);
c[ImGuiCol_TabHovered] = green;
c[ImGuiCol_TabSelected] = ImVec4(0.21f, 0.44f, 0.31f, 1);
c[ImGuiCol_TabSelectedOverline] = gold;
c[ImGuiCol_NavCursor] = gold;
c[ImGuiCol_Separator] = ImVec4(0.30f, 0.36f, 0.30f, 1);
}
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 = "totk_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;
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);
}
static int fonts_up(float scale) {
enum { N = sizeof kCjk / sizeof *kCjk };
ImFontAtlas *fa = ImGui::GetIO().Fonts;
fa->Clear();
ImFontConfig fc;
fc.FontDataOwnedByAtlas = false;
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);
}
vk_queue_lock(); // the upload submits on the queue and waits for it
int ok = ImGui_ImplVulkan_CreateFontsTexture();
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
// TOTK_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 TOTK_MENU_TEST_DIR (default: the
// working directory) as menu-test-<frame>-<page>.png; after the last tab the menu closes.
// TOTK_MENU_TEST_SET=<frame>:<key>=<value>[,<key>=<value>...][;<frame>:...]: cfg_set of each key (TOTK_ prefix
// optional) at that presented frame; changes live settings with no input, menu open or not. totk.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_AMIIBO, TAB_MENU, TAB_ADVANCED, TAB_MODS, TAB_CHEATS, TAB_COUNT };
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; // TOTK_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; // TOTK_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("TOTK_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("TOTK_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, "TOTK_", 5) ? "TOTK_" : "", 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, "TOTK_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 TOTK_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("TOTK_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("TOTK_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("TOTK_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("TOTK_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("TOTK_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("TOTK_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("TOTK_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("TOTK_GPU_INDEX", UI_MENU_GPU, vals, names, g_gpu_n + 1, UI_MENU_GPU_DESC);
}
// The game language: one entry per Mals language file the running game build ships (ui_text_game_langs), 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];
int n = 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("TOTK_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("TOTK_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
static const char *const vals[] = {"30", "40", "45", "60", "90", "120", "144", "display"};
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_DISPLAY};
enum { N = sizeof vals / sizeof *vals };
const char *names[N];
for (int i = 0; i < N; i++) names[i] = menu_text(ids[i]);
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[N - 1] = display;
}
const char *set = cfg_get("TOTK_FPS_LIMIT"), *old = cfg_get("TOTK_FPS60");
int cur = (!set || !*set) && old && !strcmp(old, "0") ? 0 : -1;
opt_choice_list("TOTK_FPS_LIMIT", UI_MENU_FPS_LIMIT, vals, names, N, UI_MENU_FPS_LIMIT_DESC, cur);
}
// The menu's name of a TOTK_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("TOTK_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
// Colour adjustment (vk_color.c): a profile writes the four values (STS78721's presets, issue #136); moving one of them
// makes the profile Custom.
static void rows_color(void) {
static const struct { const char *name, *b, *c, *s, *g; } presets[] = {
{"default", "0", "100", "100", "1"}, {"vivid", "0", "115", "135", "0.95"}, {"clarity", "2", "110", "115", "1"}};
static const UiTextId names[] = {UI_MENU_COLOR_DEFAULT, UI_MENU_COLOR_VIVID, UI_MENU_COLOR_CLARITY,
UI_MENU_COLOR_CUSTOM};
opt_group(UI_MENU_COLOR);
if (opt_choice("TOTK_COLOR_PRESET", UI_MENU_COLOR_PRESET, names, 4, UI_MENU_COLOR_PRESET_DESC)) {
const char *p = cfg_value("TOTK_COLOR_PRESET");
for (const auto &e : presets)
if (!strcmp(p, e.name)) {
cfg_set("TOTK_COLOR_BRIGHTNESS", e.b), cfg_set("TOTK_COLOR_CONTRAST", e.c);
cfg_set("TOTK_COLOR_SATURATION", e.s), cfg_set("TOTK_COLOR_GAMMA", e.g);
cfg_save();
}
}
bool moved = opt_slider("TOTK_COLOR_BRIGHTNESS", UI_MENU_COLOR_BRIGHTNESS, -30, 30, UI_MENU_COLOR_BRIGHTNESS_DESC, 1,
"%+.0f %%");
moved |= opt_slider("TOTK_COLOR_CONTRAST", UI_MENU_COLOR_CONTRAST, 70, 150, UI_MENU_COLOR_CONTRAST_DESC, 5, "%.0f %%");
moved |= opt_slider("TOTK_COLOR_SATURATION", UI_MENU_COLOR_SATURATION, 50, 200, UI_MENU_COLOR_SATURATION_DESC, 5,
"%.0f %%");
moved |= opt_slider("TOTK_COLOR_GAMMA", UI_MENU_COLOR_GAMMA, 0.8f, 1.2f, UI_MENU_COLOR_GAMMA_DESC, 0.02f, "%.2f");
if (moved && strcmp(cfg_value("TOTK_COLOR_PRESET"), "custom")) {
cfg_set("TOTK_COLOR_PRESET", "custom");
cfg_save();
}
}
static void tab_screen(float scale) {
opt_begin("##screen", scale);
opt_toggle("TOTK_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("TOTK_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("TOTK_PERF_OVERLAY", UI_MENU_PERF_OVERLAY, perf, 3, UI_MENU_PERF_OVERLAY_DESC);
opt_group(UI_MENU_GRAPHICS);
row_output_res();
row_render_scale();
row_shadow_res();
static const UiTextId up[] = {UI_MENU_UPSCALER_FSR, UI_MENU_UPSCALER_BILINEAR};
opt_choice("TOTK_UPSCALER", UI_MENU_UPSCALER, up, 2, UI_MENU_UPSCALER_DESC);
opt_slider("TOTK_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("TOTK_ASPECT", UI_MENU_ASPECT, aspect, 2, UI_MENU_ASPECT_DESC);
opt_slider("TOTK_FOV", UI_MENU_FOV, 70, 140, UI_MENU_FOV_DESC, 5, "%.0f %%");
static const char *const aniso[] = {"game", "2", "4", "8", "16"};
const char *aniso_names[] = {menu_text(UI_MENU_ANISO_GAME), "2x", "4x", "8x", "16x"};
opt_choice_list("TOTK_ANISO", UI_MENU_ANISO, aniso, aniso_names, 5, UI_MENU_ANISO_DESC);
row_shadow_scale();
row_shadow_range();
opt_slider("TOTK_PCSS", UI_MENU_PCSS, 0, 200, UI_MENU_PCSS_DESC, 10, "%.0f %%");
opt_slider("TOTK_CONTACT_SHADOWS", UI_MENU_CONTACT_SHADOWS, 0, 100, UI_MENU_CONTACT_SHADOWS_DESC, 10, "%.0f %%");
opt_toggle("TOTK_DLAA", UI_MENU_DLAA, UI_MENU_DLAA_DESC);
static const UiTextId dlss[] = {UI_MENU_DLSS_DLAA, UI_MENU_DLSS_QUALITY, UI_MENU_DLSS_BALANCED,
UI_MENU_DLSS_PERFORMANCE};
opt_choice("TOTK_DLSS", UI_MENU_DLSS, dlss, 4, UI_MENU_DLSS_DESC);
opt_toggle("TOTK_FXAA", UI_MENU_FXAA, UI_MENU_FXAA_DESC);
rows_color();
opt_group(UI_MENU_HDR_GROUP);
opt_toggle("TOTK_HDR", UI_MENU_HDR, UI_MENU_HDR_DESC);
opt_slider("TOTK_HDR_PEAK_NITS", UI_MENU_HDR_PEAK, 400, 10000, UI_MENU_HDR_PEAK_DESC, 50, "%.0f");
opt_slider("TOTK_HDR_GAME_NITS", UI_MENU_HDR_GAME, 80, 500, UI_MENU_HDR_GAME_DESC, 5, "%.0f");
opt_slider("TOTK_HDR_UI_NITS", UI_MENU_HDR_UI, 80, 500, UI_MENU_HDR_UI_DESC, 5, "%.0f");
opt_slider("TOTK_HDR_HUE", UI_MENU_HDR_HUE, 0, 100, UI_MENU_HDR_HUE_DESC, 5, "%.0f %%");
opt_end();
}
static void tab_game(float scale) {
opt_begin("##game", scale);
row_fps_limit();
row_game_lang();
static const UiTextId mode[] = {UI_MENU_MODE_DOCKED, UI_MENU_MODE_HANDHELD};
opt_choice("TOTK_OPERATION_MODE", UI_MENU_OPERATION_MODE, mode, 2, UI_MENU_OPERATION_MODE_DESC);
opt_toggle("TOTK_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("TOTK_VOLUME", UI_MENU_VOLUME, 0, 100, UI_MENU_VOLUME_DESC, 5, "%.0f %%");
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("TOTK_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 B 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. totk.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("TOTK_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("TOTK_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, TOTK_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("TOTK_PAD_LAYOUT"), *used = romfs_prompts();
const char *want = input_prompts_folder(input_prompts_pick(cfg_get("TOTK_PROMPTS"), layout), layout);
if (want == used || (want && used && !strcmp(want, used))) return nullptr;
if (want && !romfs_prompts_ready(want)) return menu_text(UI_MENU_PROMPTS_MISSING);
switch (romfs_prompts_off(want)) {
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 totk.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 totk.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;
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("TOTK_PAD_LAYOUT", UI_MENU_PAD_LAYOUT, layout, 3, UI_MENU_PAD_LAYOUT_DESC, pad_layout_note());
static const UiTextId prompts[] = {UI_MENU_PROMPTS_NINTENDO, UI_MENU_PROMPTS_XBOX};
bool restart;
const char *note = prompts_note(&restart);
opt_choice("TOTK_PROMPTS", UI_MENU_PROMPTS, prompts, 2, UI_MENU_PROMPTS_DESC, note);
bool ask = restart && opt_button("##restart", UI_MENU_RESTART, UI_MENU_RESTART_DESC);
opt_toggle("TOTK_MOTION", UI_MENU_MOTION, UI_MENU_MOTION_DESC);
opt_slider("TOTK_RUMBLE_STRENGTH", UI_MENU_RUMBLE_STRENGTH, 0, 300, UI_MENU_RUMBLE_STRENGTH_DESC, 10, "%.0f %%");
opt_slider("TOTK_RUMBLE_CURVE", UI_MENU_RUMBLE_CURVE, 0.2f, 1.5f, UI_MENU_RUMBLE_CURVE_DESC, 0.05f, "%.2f");
opt_toggle("TOTK_RUMBLE_TRIGGERS", UI_MENU_RUMBLE_TRIGGERS, UI_MENU_RUMBLE_TRIGGERS_DESC);
opt_slider("TOTK_MOUSE_SENS", UI_MENU_MOUSE_SENS, 0, 10, UI_MENU_MOUSE_SENS_DESC, 0.1f, "%.1f");
row_walk_key();
opt_slider("TOTK_WALK_SCALE", UI_MENU_WALK_SCALE, 0.2f, 0.9f, UI_MENU_WALK_SCALE_DESC, 0.01f, "%.2f");
// Analog sticks: one dead zone row per stick, falling back to TOTK_STICK_DEADZONE (both sticks) while unset. With
// a gamepad connected each row shows its stick's deflection now, before the dead zone: a drifting stick's resting
// value, to set it above.
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("TOTK_LSTICK_DEADZONE", UI_MENU_LSTICK_DEADZONE, 0, 50, UI_MENU_STICK_DEADZONE_DESC, 1, "%.0f %%",
pad ? stick_note[0] : nullptr, "TOTK_STICK_DEADZONE");
opt_slider("TOTK_RSTICK_DEADZONE", UI_MENU_RSTICK_DEADZONE, 0, 50, UI_MENU_STICK_DEADZONE_DESC, 1, "%.0f %%",
pad ? stick_note[1] : nullptr, "TOTK_STICK_DEADZONE");
opt_slider("TOTK_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();
if (ask) ImGui::OpenPopup("##restart_confirm"); // at the level BeginPopupModal looks it up
restart_confirm();
}
static void tab_menu(float scale) {
opt_begin("##menu", scale);
static const UiTextId langs[] = {UI_MENU_MENU_LANG_AUTO, UI_MENU_LANGNAME_EN, UI_MENU_LANGNAME_RU,
UI_MENU_LANGNAME_PT_BR, UI_MENU_LANGNAME_ZH_HANS};
opt_choice("TOTK_MENU_LANG", UI_MENU_MENU_LANG, langs, sizeof langs / sizeof *langs, UI_MENU_MENU_LANG_DESC);
opt_end();
}
// ---- Advanced: the testing and troubleshooting keys, the bug report helpers and the reset of every setting
// A thread count with no fixed default: Automatic (unset), then the common counts.
static void row_threads(const char *key, UiTextId label, UiTextId desc) {
static const char *const vals[] = {"", "1", "2", "4", "6", "8", "12", "16", "24", "32", "48", "64"};
enum { N = sizeof vals / sizeof *vals };
const char *names[N];
names[0] = menu_text(UI_MENU_AUTO);
for (int i = 1; i < N; i++) names[i] = vals[i];
opt_choice_list(key, label, vals, names, N, desc);
}
// A free-text key shown as Off (unset) / On (value set): On writes on_value, any set value reads as On.
static void row_set_or_not(const char *key, UiTextId label, UiTextId desc, const char *on_value) {
const char *vals[] = {"", on_value}, *names[] = {menu_text(UI_MENU_OFF), menu_text(UI_MENU_ON)};
opt_choice_list(key, label, vals, names, 2, desc, *cfg_value(key) ? 1 : 0);
}
static bool validation_layer_present(void) {
static int have = -1;
if (have < 0) {
uint32_t n = 0;
vkEnumerateInstanceLayerProperties(&n, nullptr);
VkLayerProperties *l = (VkLayerProperties *)calloc(n ? n : 1, sizeof *l);
if (l && vkEnumerateInstanceLayerProperties(&n, l) >= 0)
for (uint32_t i = 0; i < n && have <= 0; i++) have = !strcmp(l[i].layerName, "VK_LAYER_KHRONOS_validation");
free(l);
if (have < 0) have = 0;
}
return have != 0;
}
// Every setting the menu shows back to its default (the environment's keys stay), written to totk.ini.
static void reset_all(void) {
const char *keys[128];
int n = 0;
for (const CfgMeta *m; n < 128 && (m = cfg_meta_at(n)); n++) keys[n] = m->key;
int done = cfg_reset(keys, n);
opt_save_soon();
rt_log("menu: reset %d settings to their defaults", done);
}
// A button's result shown in its value for a few seconds: 1 done (done_text), -1 failed.
struct ActionNote { uint64_t until; int result; };
static const char *action_value(const ActionNote &a, UiTextId done_text) {
if (!a.result || plat_now_ns() > a.until) return "";
return menu_text(a.result > 0 ? done_text : UI_MENU_ACTION_FAILED);
}
static void action_done(ActionNote &a, bool ok) { a = {plat_now_ns() + 3000000000ull, ok ? 1 : -1}; }
static void tab_advanced(float scale) {
static ActionNote copy_note, log_note;
opt_begin("##advanced", scale);
opt_text(UI_MENU_ADV_WARNING, true);
opt_group(UI_MENU_GROUP_SCHED);
static const UiTextId preempt[] = {UI_MENU_PREEMPT_OFF, UI_MENU_PREEMPT_ON, UI_MENU_PREEMPT_INHERIT}; // 0|1|2
opt_choice("TOTK_ASYNC_PREEMPT", UI_MENU_ASYNC_PREEMPT, preempt, 3, UI_MENU_ASYNC_PREEMPT_DESC);
opt_toggle("TOTK_REPLAY_ASYNC", UI_MENU_REPLAY_ASYNC, UI_MENU_REPLAY_ASYNC_DESC);
opt_toggle("TOTK_TIMER_1MS", UI_MENU_TIMER_1MS, UI_MENU_TIMER_1MS_DESC);
opt_group(UI_MENU_GROUP_SHADERS);
static const UiTextId wait[] = {UI_MENU_PIPE_WAIT_DEFAULT, UI_MENU_PIPE_WAIT_DRAW};
opt_choice("TOTK_PIPE_WAIT", UI_MENU_PIPE_WAIT, wait, 2, UI_MENU_PIPE_WAIT_DESC);
static const UiTextId skip[] = {UI_MENU_PIPE_SKIP_STRICT, UI_MENU_PIPE_SKIP_ANY, UI_MENU_PIPE_SKIP_FULLSCREEN};
opt_choice("TOTK_PIPE_SKIP", UI_MENU_PIPE_SKIP, skip, 3, UI_MENU_PIPE_SKIP_DESC);
opt_toggle("TOTK_LIB_PREBUILD", UI_MENU_LIB_PREBUILD, UI_MENU_LIB_PREBUILD_DESC);
row_threads("TOTK_LIB_PREBUILD_THREADS", UI_MENU_LIB_PREBUILD_THREADS, UI_MENU_LIB_PREBUILD_THREADS_DESC);
opt_toggle("TOTK_PIPE_PREBUILD", UI_MENU_PIPE_PREBUILD, UI_MENU_PIPE_PREBUILD_DESC);
row_threads("TOTK_PREWARM_XLATE", UI_MENU_PREWARM_XLATE, UI_MENU_PREWARM_XLATE_DESC);
opt_toggle("TOTK_GPL", UI_MENU_GPL, UI_MENU_GPL_DESC);
opt_group(UI_MENU_GROUP_TEXMEM);
static const UiTextId astc[] = {UI_MENU_ASTC_AUTO, UI_MENU_ASTC_GPU, UI_MENU_ASTC_NATIVE, UI_MENU_ASTC_CPU};
opt_choice("TOTK_ASTC", UI_MENU_ASTC, astc, 4, UI_MENU_ASTC_DESC);
static const UiTextId mem[] = {UI_MENU_MEMORY_AUTO, UI_MENU_MEMORY_IMPORT, UI_MENU_MEMORY_MIRROR,
UI_MENU_MEMORY_VERIFY};
opt_choice("TOTK_MEMORY", UI_MENU_MEMORY, mem, 4, UI_MENU_MEMORY_DESC);
opt_toggle("TOTK_TEX_ALIAS", UI_MENU_TEX_ALIAS, UI_MENU_TEX_ALIAS_DESC);
opt_slider("TOTK_TEX_VRAM_PRESSURE", UI_MENU_TEX_VRAM_PRESSURE, 0, 100, UI_MENU_TEX_VRAM_PRESSURE_DESC, 1, "%.0f%%");
static const UiTextId astc_bc[] = {UI_MENU_AUTO, UI_MENU_OFF, UI_MENU_ON}; // unset, 0, 1
opt_choice("TOTK_ASTC_BC", UI_MENU_ASTC_BC, astc_bc, 3, UI_MENU_ASTC_BC_DESC);
opt_group(UI_MENU_GROUP_FIXES);
opt_toggle("TOTK_LOD_GUARD", UI_MENU_LOD_GUARD, UI_MENU_LOD_GUARD_DESC);
opt_toggle("TOTK_FIX_C0", UI_MENU_FIX_C0, UI_MENU_FIX_C0_DESC);
opt_toggle("TOTK_FIX_ZERO", UI_MENU_FIX_ZERO, UI_MENU_FIX_ZERO_DESC);
opt_toggle("TOTK_BIND_SKIP", UI_MENU_BIND_SKIP, UI_MENU_BIND_SKIP_DESC);
opt_toggle("TOTK_SHADER_MEMO", UI_MENU_SHADER_MEMO, UI_MENU_SHADER_MEMO_DESC);
opt_toggle("TOTK_PIPE_MEMO", UI_MENU_PIPE_MEMO, UI_MENU_PIPE_MEMO_DESC);
opt_toggle("TOTK_TEX_MEMO", UI_MENU_TEX_MEMO, UI_MENU_TEX_MEMO_DESC);
opt_toggle("TOTK_DESC_TEMPLATE", UI_MENU_DESC_TEMPLATE, UI_MENU_DESC_TEMPLATE_DESC);
opt_toggle("TOTK_PUSH_SKIP", UI_MENU_PUSH_SKIP, UI_MENU_PUSH_SKIP_DESC);
opt_slider("TOTK_SPLIT_DRAWS", UI_MENU_SPLIT_DRAWS, 0, 1000, UI_MENU_SPLIT_DRAWS_DESC, 8, "%.0f");
opt_group(UI_MENU_GROUP_VULKAN);
if (!validation_layer_present() && !cfg_bool("TOTK_VK_VALIDATION")) opt_disable_next(UI_MENU_VK_VALIDATION_MISSING);
opt_toggle("TOTK_VK_VALIDATION", UI_MENU_VK_VALIDATION, UI_MENU_VK_VALIDATION_DESC);
static const UiTextId dyn[] = {UI_MENU_AUTO, UI_MENU_OFF, UI_MENU_ON}; // unset, 0, 1
opt_choice("TOTK_VK_DYN_VERTEX", UI_MENU_VK_DYN_VERTEX, dyn, 3, UI_MENU_VK_DYN_VERTEX_DESC);
opt_choice("TOTK_VK_DYN_A2C", UI_MENU_VK_DYN_A2C, dyn, 3, UI_MENU_VK_DYN_A2C_DESC);
opt_toggle("TOTK_VK_SUBGROUP_PIN", UI_MENU_VK_SUBGROUP_PIN, UI_MENU_VK_SUBGROUP_PIN_DESC);
opt_group(UI_MENU_GROUP_DIAG);
row_set_or_not("TOTK_TRACE", UI_MENU_TRACE, UI_MENU_TRACE_DESC, "perf");
opt_toggle("TOTK_FLIGHT", UI_MENU_FLIGHT, UI_MENU_FLIGHT_DESC);
opt_slider("TOTK_FLIGHT_SPIKE_MS", UI_MENU_FLIGHT_SPIKE_MS, 20, 1000, UI_MENU_FLIGHT_SPIKE_MS_DESC, 10, "%.0f");
opt_choice("TOTK_PASS_CRUMBS", UI_MENU_PASS_CRUMBS, dyn, 3, UI_MENU_PASS_CRUMBS_DESC); // vk_crumbs.c pass marks
static char rec_path[600]; // the recording goes beside the log
if (!rec_path[0]) {
char dir[512];
snprintf(rec_path, sizeof rec_path, "%s/input-record.txt", sysinfo_log_folder(dir, sizeof dir) ? dir : ".");
}
row_set_or_not("TOTK_INPUT_RECORD", UI_MENU_INPUT_RECORD, UI_MENU_INPUT_RECORD_DESC, rec_path);
if (opt_button("##open_log", UI_MENU_OPEN_LOG, UI_MENU_OPEN_LOG_DESC, action_value(log_note, UI_MENU_OPEN_LOG))) {
char dir[512];
bool ok = sysinfo_log_folder(dir, sizeof dir) && plat_open_folder(dir);
if (!ok) action_done(log_note, false); // success shows as the opened folder
}
if (opt_button("##copy_info", UI_MENU_COPY_INFO, UI_MENU_COPY_INFO_DESC,
action_value(copy_note, UI_MENU_COPY_INFO_DONE))) {
static char text[16384];
action_done(copy_note, sysinfo_text(text, sizeof text) > 0 && plat_clipboard_set(text));
}
bool ask = opt_button("##reset", UI_MENU_RESET, UI_MENU_RESET_DESC);
opt_end();
if (ask) ImGui::OpenPopup("##reset_confirm"); // at the level BeginPopupModal looks it up
// the confirmation, over the menu; Cancel has the cursor first
g_modal = false;
ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(0.5f, 0.5f));
if (ImGui::BeginPopupModal("##reset_confirm", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize)) {
g_modal = true;
ImGui::TextUnformatted(menu_text(UI_MENU_RESET_CONFIRM));
ImGui::Spacing();
const char *yes = menu_text(UI_MENU_RESET_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 = false;
if (ImGui::Button(no, ImVec2(bw, 0))) close = true;
if (ImGui::IsWindowAppearing()) ImGui::SetItemDefaultFocus();
ImGui::SameLine();
if (ImGui::Button(yes, ImVec2(bw, 0))) {
reset_all();
close = true;
}
if (ImGui::IsKeyPressed(ImGuiKey_Escape, false) || ImGui::IsKeyPressed(ImGuiKey_GamepadFaceRight, false))
close = true;
if (close) {
ImGui::CloseCurrentPopup();
opt_focus_key("##reset"); // back on the row it came from
}
ImGui::EndPopup();
}
}
static const UiTextId kTabNames[TAB_COUNT] = {UI_MENU_TAB_SCREEN, UI_MENU_TAB_GAME, UI_MENU_TAB_SOUND,
UI_MENU_TAB_CONTROLS, UI_MENU_TAB_AMIIBO, UI_MENU_TAB_MENU,
UI_MENU_TAB_ADVANCED, UI_MENU_TAB_MODS, UI_MENU_TAB_CHEATS};
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);
}
// ---- Mods: the mods folder (mods.c) with switches and the load order, then the port's own patches. The folder was
// read at start; the rows change the order and switches for the next start.
static void mods_builtin_rows(void) {
opt_group(UI_MENU_MODS_BUILTIN);
char fps[64], res[64];
const char *lim = cfg_value("TOTK_FPS_LIMIT");
if (!strcmp(lim, "display")) snprintf(fps, sizeof fps, "%s", menu_text(UI_MENU_FPS_DISPLAY));
else snprintf(fps, sizeof fps, "%s FPS", lim);
if (opt_button("##mods_fps60", UI_MENU_MODS_FPS60, UI_MENU_MODS_FPS60_DESC, fps)) go_to(TAB_GAME, "TOTK_FPS_LIMIT");
int w = 0, h = 0;
sscanf(cfg_value("TOTK_OUTPUT_RES"), "%dx%d", &w, &h);
snprintf(res, sizeof res, "%d × %d", w, h); // U+00D7, as row_output_res shows it
if (opt_button("##mods_res", UI_MENU_MODS_RES, UI_MENU_MODS_RES_DESC, res)) go_to(TAB_SCREEN, "TOTK_OUTPUT_RES");
opt_toggle("TOTK_REALTIME", UI_MENU_REALTIME, UI_MENU_REALTIME_DESC);
opt_toggle("TOTK_CLOTH_REALTIME", UI_MENU_CLOTH_REALTIME, UI_MENU_CLOTH_REALTIME_DESC);
opt_toggle("TOTK_HIGHFPS_FIX", UI_MENU_HIGHFPS_FIX, UI_MENU_HIGHFPS_FIX_DESC);
opt_button("##mods_quickmenu", UI_MENU_MODS_QUICKMENU, UI_MENU_MODS_QUICKMENU_DESC,
menu_text(romfs_patches_loaded() ? UI_MENU_ON : UI_MENU_OFF));
}
static void tab_mods(float scale) {
opt_begin("##mods", scale);
opt_group(UI_MENU_MODS_INSTALLED);
int n = mods_count();
if (!n) opt_text(UI_MENU_MODS_NONE);
const char *after = mods_changed() ? menu_text(UI_MENU_AFTER_RESTART) : nullptr;
for (int i = 0; i < n; i++) {
const ModInfo *m = mods_at(i);
char id[32], desc[1536];
snprintf(id, sizeof id, "##mod%d", i);
char over[512] = "", other[512] = "";
if (m->overridden) snprintf(over, sizeof over, menu_text(UI_MENU_MODS_OVERRIDDEN), m->overridden, m->overridden_by);
if (m->other_version) snprintf(other, sizeof other, menu_text(UI_MENU_MODS_OTHER_VERSION), m->other_version);
char cheat[256] = "";
if (m->kind & MOD_CHEATS) { // its cheats as read at start, or the file they come from
int count = 0;
for (int j = 0; j < cheats_count(); j++)
count += !strcmp(cheats_info(j)->mod, m->name) && cheats_info(j)->kind != CHEAT_HEADING;
if (count) snprintf(cheat, sizeof cheat, menu_text(UI_MENU_MODS_CHEATS), count);
else snprintf(cheat, sizeof cheat, menu_text(UI_MENU_MODS_CHEATS_FILE), cheats_file());
}
char code[512] = "", problem[512] = "";
if (m->nsets && m->overrides[0])
snprintf(code, sizeof code, menu_text(UI_MENU_MODS_REPLACES_BUILTIN), m->nsets, m->overrides);
else if (m->nsets)
snprintf(code, sizeof code, menu_text(UI_MENU_MODS_EXEFS_ON), m->nsets);
switch (m->problem) {
case MOD_EMPTY: snprintf(problem, sizeof problem, "%s", menu_text(UI_MENU_MODS_EMPTY)); break;
case MOD_PLUGIN: snprintf(problem, sizeof problem, "%s", menu_text(UI_MENU_MODS_SUBSDK)); break;
case MOD_EXEFS_LATER: snprintf(problem, sizeof problem, "%s", menu_text(UI_MENU_MODS_EXEFS_LATER)); break;
case MOD_EXEFS_UNSUPPORTED:
snprintf(problem, sizeof problem, menu_text(UI_MENU_MODS_EXEFS_UNSUPPORTED), m->note);
break;
case MOD_EXEFS_OTHER: snprintf(problem, sizeof problem, "%s", menu_text(UI_MENU_MODS_EXEFS_OTHER)); break;
case MOD_BAD_NAME: snprintf(problem, sizeof problem, "%s", menu_text(UI_MENU_MODS_BAD_NAME)); break;
default: break;
}
int k = snprintf(desc, sizeof desc, menu_text(UI_MENU_MODS_ROW_DESC), m->replaced, m->added);
const char *parts[] = {over, other, cheat, code, problem, menu_text(UI_MENU_MODS_MOVE_HINT)};
for (const char *part : parts)
if (*part && k >= 0 && k < (int)sizeof desc) k += snprintf(desc + k, sizeof desc - k, "\n\n%s", part);
const char *note = m->enabled && m->problem != MOD_OK ? menu_text(UI_MENU_MODS_UNUSED) : after;
int s = opt_text_row(id, m->name, menu_text(m->enabled ? UI_MENU_ON : UI_MENU_OFF), note, desc, true);
if (s == 2 || s == -1 || s == 1) {
mods_set_enabled(i, !m->enabled);
opt_save_soon();
} else if ((s == 3 && i > 0) || (s == 4 && i + 1 < n)) {
int to = s == 3 ? i - 1 : i + 1;
mods_move(i, to - i);
snprintf(id, sizeof id, "##mod%d", to);
opt_focus_key(id); // the cursor follows the mod
opt_save_soon();
}
}
bool rebuild =
cheats_rebuild_pending() && opt_button("##restart", UI_MENU_CHEATS_REBUILD, UI_MENU_CHEATS_REBUILD_DESC);
static uint64_t open_failed; // plat_now_ns until which the button says it failed
if (opt_button("##mods_open", UI_MENU_MODS_OPEN, UI_MENU_MODS_OPEN_DESC,
plat_now_ns() < open_failed ? menu_text(UI_MENU_ACTION_FAILED) : "")) {
bool ok = mods_dir()[0] != 0;
if (ok) plat_file_mkdir_p(mods_dir());
if (!ok || !plat_open_folder(mods_dir())) open_failed = plat_now_ns() + 3000000000ull;
}
mods_builtin_rows();
opt_end();
if (rebuild) {
restart_rebuild = true;
ImGui::OpenPopup("##restart_confirm");
}
restart_confirm();
}
// ---- the window: title, tabs (L / R or Q / E switch), the page, and the bottom row with "Continue" and "Quit game".
// B on the gamepad and Esc close it.
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(0, 0, 0, 120));
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);
ImGui::Begin("##menu", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoSavedSettings);
ImGui::TextColored(ImVec4(0.93f, 0.76f, 0.33f, 1), "%s", menu_text(UI_MENU_TITLE));
// 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, menu_text(UI_MENU_VERSION), 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 (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 (!ImGui::BeginTabItem(label, nullptr, g_tab_request == k ? ImGuiTabItemFlags_SetSelected : 0)) 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_MODS: tab_mods(scale); break;
case TAB_SOUND: tab_sound(scale); break;
case TAB_CONTROLS: tab_controls(scale); break;
case TAB_AMIIBO: tab_amiibo(scale); break;
case TAB_MENU: tab_menu(scale); break;
case TAB_CHEATS:
if (tab_cheats(scale)) {
restart_rebuild = true;
ImGui::OpenPopup("##restart_confirm");
}
restart_confirm();
break;
case TAB_ADVANCED: tab_advanced(scale); break;
}
ImGui::EndTabItem();
}
g_tab_request = -1;
ImGui::EndTabBar();
}
// bottom row: the save status on the left, the two buttons on the right
ImGui::Spacing();
float y = ImGui::GetCursorPosY();
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();
bool was_modal = g_modal || binds_modal();
if (g_tab != TAB_ADVANCED && 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 totk.ini (opt_save_tick)
// ---- the performance overlay (TOTK_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, in the style of SavageWindRecomp's perf_overlay_draw
// (menu.mm): 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: totk.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("TOTK_PERF_OVERLAY", modes[(perf_mode_of(cfg_value("TOTK_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("TOTK_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]);
if (t->scaler == 2) {
int s = cfg_int("TOTK_SHARPNESS");
snprintf(v[3], sizeof v[3], menu_text(UI_MENU_OV_FMT_FSR), s < 0 ? 0 : s > 100 ? 100 : s);
} else {
// FSR enlarges only (vk_fsr_record): an image larger than the window is shrunk by the blit even with FSR selected
int down = t->scaler == 1 && !strcmp(cfg_value("TOTK_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};
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++) {
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);
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};
static const PerfCol c0[] = {{UI_MENU_OV_FPS, 888}};
static const PerfCol c1[] = {{UI_MENU_OV_FMT_PCT, 100}, {UI_MENU_OV_FMT_GB, 888.8f}, {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_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}};
perf_column("##c0", c0, 1, labels, 4);
perf_column("##c1", c1, 3);
perf_column("##c2", c2, 3);
perf_column("##c3", c3, 4);
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::TableNextColumn(); // the frame row: no label, the rate in its place
char b[64];
snprintf(b, sizeof b, menu_text(UI_MENU_OV_FPS), s.fps);
ImGui::TextColored(fps_c, "%s", b);
perf_cell(UI_MENU_OV_FRAME_MS, s.frame_ms_avg);
perf_cell(UI_MENU_OV_MAX_MS, s.frame_ms_max);
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;
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 totk.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("TOTK_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;
}
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;
}
}