diff --git a/gpu/vk/vk_api.h b/gpu/vk/vk_api.h index 9d1a6b53..f90dece3 100644 --- a/gpu/vk/vk_api.h +++ b/gpu/vk/vk_api.h @@ -26,6 +26,9 @@ enum { HOS_PAD_SOUTH, HOS_PAD_EAST, HOS_PAD_WEST, HOS_PAD_NORTH, HOS_PAD_BACK, H HOS_PAD_LSTICK, HOS_PAD_RSTICK, HOS_PAD_LSHOULDER, HOS_PAD_RSHOULDER, HOS_PAD_UP, HOS_PAD_DOWN, HOS_PAD_LEFT, HOS_PAD_RIGHT, HOS_PAD_LTRIGGER, HOS_PAD_RTRIGGER, HOS_PAD_N }; enum { HOS_BIND_PAD, HOS_BIND_KEY }; +/* (hookshot) keys only (HOS_BIND_KEY): the left stick and the camera from the keyboard, after the Switch's buttons */ +enum { HOS_KEY_MOVE_UP = HOS_SW_N, HOS_KEY_MOVE_DOWN, HOS_KEY_MOVE_LEFT, HOS_KEY_MOVE_RIGHT, HOS_KEY_CAM_UP, + HOS_KEY_CAM_DOWN, HOS_KEY_CAM_LEFT, HOS_KEY_CAM_RIGHT, HOS_KEY_N }; /* what the executable gives the back end */ typedef struct MkwVkHost { @@ -73,7 +76,8 @@ typedef struct MkwVkHost { uint32_t (*pad_raw)(void); /* (hookshot) the controls, for the Insert menu's Controls page. button: the Switch's, HOS_SW_*; set HOS_BIND_PAD: the gamepad input that presses it (a HOS_PAD_* position, -1 none; slot 0 only); set HOS_BIND_KEY: the keys that - do (Windows key codes, 0 none; slots 0 and 1). bind with button -1: all back to their defaults */ + do (Windows key codes, 0 none; slots 0 and 1; a mouse button's too, and HOS_KEY_* past the Switch's buttons). + bind with button -1: all back to their defaults; -2: only the set's (the gamepad's or the keys') */ int (*binding)(int set, int button, int slot); void (*bind)(int set, int button, int slot, int code); } MkwVkHost; @@ -113,9 +117,13 @@ typedef struct MkwVkRenderer { void (*nvn_texture_created)(const nvn_tex *t); /* the CPU set an event's value: in order with the submits (the GPU's waits on it) */ void (*nvn_event_signal)(uint64_t gpu, uint32_t value); - int (*option)(int id); /* (hookshot, the Insert menu) 0: game time follows the clock, 1: the menu is open, 2: dynamic resolution, 3: its fixed scale in percent, 4: the controllers count behind other windows, 5: text speed (0 normal to 3 instant) */ + int (*option)(int id); /* (hookshot, the Insert menu) 0: game time follows the clock, 1: the menu is open, 2: dynamic resolution, 3: its fixed scale in percent, 4: the controllers count behind other windows, 5: text speed (0 normal to 3 instant), + 20: the mouse's sensitivity in percent, 21: its vertical inverted, 22: native mouse look (host/nn_hid.c, mouse_native.c) */ /* a submit with its queue */ void (*nvn_submit_q)(const uint64_t *handles, int n, uint64_t queue); + /* (hookshot, the menu) from the game's side: 20 the mouse's sensitivity in percent (the - and = keys; kept in the file), + 29 the mouse wheel while the menu is open (host/display.c, in WHEEL_DELTA units) */ + void (*option_set)(int id, int value); } MkwVkRenderer; /* the DLL's one export: null when Vulkan is not usable (the executable then falls back to WebGPU) */ diff --git a/gpu/vk/vk_menu.cpp b/gpu/vk/vk_menu.cpp index c33335ce..580f0a2c 100644 --- a/gpu/vk/vk_menu.cpp +++ b/gpu/vk/vk_menu.cpp @@ -2,8 +2,9 @@ // so it costs nothing while closed and it never touches the game's picture, nor what frame interpolation sees. Keys: Insert // opens and closes it, Up and Down choose, Left and Right change, Esc closes; the game gets no keyboard input while it is open. // (hookshot) A gamepad's Home button, or Select and Start together, opens it too; the D-pad chooses and changes, the bottom -// face button changes too, the right one closes. The game gets no gamepad input while it is open either. -// Its Controls page binds the Switch's buttons to gamepad inputs and keys. +// face button changes too, the right one closes, the shoulder buttons change tabs. The game gets no gamepad input while it is +// open either. The mouse works it too. Its Keyboard and Controller tabs bind the Switch's buttons to keys and gamepad inputs, +// which the host keeps (host/nn_hid.c: host->binding and bind). #include #include #include @@ -30,6 +31,7 @@ struct Row { int lo, hi, step; const char* (*text)(int v); const char* help; // (shown under the list while the row is selected) + int tab; // (hookshot: the tab it is on, tab_names) }; const char* tx_framegen(int v) { static const char* t[] = {"Off", "", "2x", "3x", "4x", "5x", "6x", "7x", "8x"}; @@ -58,6 +60,7 @@ const char* tx_text_speed(int v) { } const char* tx_handheld(int v) { return v ? "On (restart)" : "Off (restart)"; } const char* tx_ultrawide(int v) { return v ? "Monitor's shape (restart)" : "16:9 (restart)"; } + const char* tx_preset(int v) { static const char* t[] = {"Custom", "Minimum (4GB)", "Low (6GB)", "Balanced (8GB)", "High (12GB)", "Ultra (16GB+)"}; return t[std::min(std::max(v, 0), 5)]; @@ -99,49 +102,182 @@ void apply_preset(int p) { } sync_preset_budgets(p); } +/// (after a row changed) the preset applied, or, when a setting it sets was changed by hand, Custom +void preset_follow(const int* value) { + if (value == &opts.preset) apply_preset(opts.preset); + else if (value == &opts.resolution || value == &opts.upscale || value == &opts.sharpness || value == &opts.framegen || + value == &opts.pace || value == &opts.realtime || value == &opts.handheld) + opts.preset = 0, sync_preset_budgets(0); +} +const char* tx_sens(int v) { + static char b[12]; + snprintf(b, sizeof b, "%.2f", v / 100.0); + return b; +} +const char* tx_mousecam(int v) { return v ? "Native" : "As the stick"; } +// (the controls' rows: the text function says which kind they are, kind_of; they have no value of their own, their lo is the +// control's number, HOS_SW_* or HOS_KEY_*, and the host keeps what it is bound to) +const char* tx_keybind(int) { return ""; } +const char* tx_padbind(int) { return ""; } +const char* tx_reset(int) { return "Reset to defaults"; } + +/// a key's or a mouse button's name, as the Keyboard tab shows it +const char* vk_name(int vk) { + static char b[32]; + if (vk <= 0) return "-"; + if ((vk >= '0' && vk <= '9') || (vk >= 'A' && vk <= 'Z')) { snprintf(b, sizeof b, "%c", vk); return b; } + if (vk >= 0x70 && vk <= 0x87) { snprintf(b, sizeof b, "F%d", vk - 0x70 + 1); return b; } // (F1 to F24) + if (vk >= 0x60 && vk <= 0x69) { snprintf(b, sizeof b, "Num %d", vk - 0x60); return b; } // (the number pad) + switch (vk) { + case VK_LBUTTON: return "Left click"; + case VK_RBUTTON: return "Right click"; + case VK_MBUTTON: return "Middle click"; + case 0x05: return "Mouse 4"; + case 0x06: return "Mouse 5"; + case VK_SPACE: return "Space"; + case VK_SHIFT: return "Shift"; + case VK_CONTROL: return "Ctrl"; + case VK_MENU: return "Alt"; + case VK_TAB: return "Tab"; + case VK_RETURN: return "Enter"; + case VK_CAPITAL: return "Caps Lock"; + case VK_UP: return "Up"; + case VK_DOWN: return "Down"; + case VK_LEFT: return "Left"; + case VK_RIGHT: return "Right"; + case VK_HOME: return "Home"; + case VK_END: return "End"; + case VK_PRIOR: return "Page Up"; + case VK_NEXT: return "Page Down"; + case VK_DELETE: return "Delete"; + case 0xBA: return ";"; + case 0xBB: return "="; + case 0xBC: return ","; + case 0xBD: return "-"; + case 0xBE: return "."; + case 0xBF: return "/"; + case 0xC0: return "`"; + case 0xDB: return "["; + case 0xDC: return "\\"; + case 0xDD: return "]"; + case 0xDE: return "'"; + } + snprintf(b, sizeof b, "Key %d", vk); + return b; +} +/// a gamepad input by where it is (HOS_PAD_*, SDL's order): the same place on any pad +const char* pad_name(int p) { + static const char* t[HOS_PAD_N] = {"Bottom face (A / Cross)", "Right face (B / Circle)", "Left face (X / Square)", + "Top face (Y / Triangle)", "Back / Select / Share", "Home / Guide", "Start / Options", + "Left stick press", "Right stick press", "LB / L1", "RB / R1", "D-pad up", + "D-pad down", "D-pad left", "D-pad right", "LT / L2", "RT / R2"}; + return p >= 0 && p < HOS_PAD_N ? t[p] : "-"; +} +// (the controls, kept by the host: host/nn_hid.c reads them back from the file, pad_ and key_) +const char* const bind_names[HOS_KEY_N] = {"a", "b", "x", "y", "ls", "rs", "l", "r", "zl", "zr", "plus", "minus", + "left", "up", "right", "down", "move_up", "move_down", "move_left", + "move_right", "cam_up", "cam_down", "cam_left", "cam_right"}; +bool can_bind() { return host && host->binding && host->bind && host->pad_raw; } +int key_of(int control, int slot) { return can_bind() ? host->binding(HOS_BIND_KEY, control, slot) : 0; } +int pad_of(int control) { return can_bind() ? host->binding(HOS_BIND_PAD, control, 0) : -1; } + +const char* tab_names[] = {"Display", "Performance", "Camera", "Keyboard", "Controller"}; +constexpr int kTabs = 5; +#define KEY_HELP "Enter, the bottom face button or a click on a key: press the new key or mouse button (Esc keeps the old one). Backspace clears it. " \ + "Left and right: the first or second key." +#define PAD_HELP "Enter, the bottom face button or a click: press the controller button that should do this (Home is the menu's). " \ + "Taking another button's input swaps the two. Delete clears it." Row rows[] = { - {"Quality preset", &opts.preset, 0, 5, 1, tx_preset, - "Automatically configures resolution, upscaling, frame generation, and dynamic VRAM limits tailored to your GPU tier."}, {"Fullscreen", &opts.fullscreen, 0, 1, 1, tx_onoff, - "Fills the whole screen with the game, without the window's border. The same as F11."}, - {"Frame generation", &opts.framegen, 0, 8, 1, tx_framegen, - "Makes extra frames between the game's own (2x up to 8x), for smoother motion. Adds a little input delay, and fast motion or camera cuts " - "can show artifacts."}, - {"Frame pacing", &opts.pace, 0, 1, 1, tx_onoff, - "Shows each frame at an even interval instead of as soon as it is ready. Less stutter, at up to one frame of delay."}, + "Fills the whole screen with the game, without the window's border. The same as F11.", 0}, {"Resolution", &opts.resolution, 0, 4, 1, tx_resolution, "How sharp the 3D is drawn. Auto lets the game lower it when the GPU is busy. A fixed percentage stays steady: lower is " - "faster."}, + "faster.", 0}, {"Upscaling", &opts.upscale, 0, 2, 1, tx_upscale, - "The filter that scales the picture to your window. Off is the plain copy. Catmull-Rom is sharper, Lanczos sharper still."}, + "The filter that scales the picture to your window. Off is the plain copy. Catmull-Rom is sharper, Lanczos sharper still.", 0}, {"Sharpness", &opts.sharpness, 0, 100, 10, tx_percent, - "Sharpens the picture as it goes to the window, with or without an upscaling filter. Too much shows halos around edges."}, + "Sharpens the picture as it goes to the window, with or without an upscaling filter. Too much shows halos around edges.", 0}, + {"Ultrawide", &opts.ultrawide, 0, 1, 1, tx_ultrawide, + "Monitor's shape widens the camera to fill a screen wider than 16:9. The HUD is still stretched. Applies after a restart.", 0}, + {"FPS counter", &opts.show_fps, 0, 1, 1, tx_onoff, + "Shows frames per second in the top right corner. With frame generation on, it also shows the game's own rate.", 0}, + {"Quality preset", &opts.preset, 0, 5, 1, tx_preset, + "Sets resolution, upscaling, frame generation and the GPU memory limits for a tier of graphics card (its video memory). " + "Changing one of those settings by hand makes it Custom.", 1}, + {"Frame generation", &opts.framegen, 0, 8, 1, tx_framegen, + "Makes extra frames between the game's own (2x up to 8x), for smoother motion. Adds a little input delay, and fast motion or camera cuts " + "can show artifacts.", 1}, + {"Frame pacing", &opts.pace, 0, 1, 1, tx_onoff, + "Shows each frame at an even interval instead of as soon as it is ready. Less stutter, at up to one frame of delay.", 1}, {"Game speed", &opts.realtime, 0, 1, 1, tx_speed, "Per frame: the game slows down whenever it runs below 60 fps. Real time: it keeps normal speed down to 15 fps by taking " - "longer steps."}, + "longer steps.", 1}, + {"Text speed", &opts.text_speed, 0, 3, 1, tx_text_speed, + "How fast the letters of dialogue and other messages appear. Normal is the game's own pace, with its pauses. Instant shows " + "each page at once, as pressing A does.", 1}, {"Handheld profile", &opts.handheld, 0, 1, 1, tx_handheld, - "Runs the game as if the console were undocked: lower resolutions, lighter on the GPU. Applies after a restart."}, - {"Ultrawide", &opts.ultrawide, 0, 1, 1, tx_ultrawide, - "Monitor's shape widens the camera to fill a screen wider than 16:9. The HUD is still stretched. Applies after a restart."}, - {"FPS counter", &opts.show_fps, 0, 1, 1, tx_onoff, - "Shows frames per second in the top right corner. With frame generation on, it also shows the game's own rate."}, + "Runs the game as if the console were undocked: lower resolutions, lighter on the GPU. Applies after a restart.", 1}, + {"Mouse camera", &opts.mouse_native, 0, 1, 1, tx_mousecam, + "Native: the mouse turns the camera itself, one to one, like a PC game. As the stick: the mouse stands in for the right stick " + "(the game's own stick curve and top speed). Click in the game's window to use the mouse; Esc lets it go.", 2}, + {"Mouse sensitivity", &opts.mouse_sens, 10, 500, 5, tx_sens, + "How far the camera turns for a given mouse movement: at 1.00, a tenth of a degree a count. One setting for every camera. " + "The - and = keys change it in the game too.", 2}, + {"Invert vertical", &opts.mouse_invert, 0, 1, 1, tx_onoff, + "Swaps up and down for the mouse: moving it forward tilts the camera down.", 2}, + {"Button A", nullptr, HOS_SW_A, HOS_SW_A, 1, tx_keybind, KEY_HELP, 3}, + {"Button B", nullptr, HOS_SW_B, HOS_SW_B, 1, tx_keybind, KEY_HELP, 3}, + {"Button X", nullptr, HOS_SW_X, HOS_SW_X, 1, tx_keybind, KEY_HELP, 3}, + {"Button Y", nullptr, HOS_SW_Y, HOS_SW_Y, 1, tx_keybind, KEY_HELP, 3}, + {"Button L", nullptr, HOS_SW_L, HOS_SW_L, 1, tx_keybind, KEY_HELP, 3}, + {"Button R", nullptr, HOS_SW_R, HOS_SW_R, 1, tx_keybind, KEY_HELP, 3}, + {"Button ZL", nullptr, HOS_SW_ZL, HOS_SW_ZL, 1, tx_keybind, KEY_HELP, 3}, + {"Button ZR", nullptr, HOS_SW_ZR, HOS_SW_ZR, 1, tx_keybind, KEY_HELP, 3}, + {"Plus", nullptr, HOS_SW_PLUS, HOS_SW_PLUS, 1, tx_keybind, KEY_HELP, 3}, + {"Minus", nullptr, HOS_SW_MINUS, HOS_SW_MINUS, 1, tx_keybind, KEY_HELP, 3}, + {"Left stick press", nullptr, HOS_SW_LS, HOS_SW_LS, 1, tx_keybind, KEY_HELP, 3}, + {"Right stick press", nullptr, HOS_SW_RS, HOS_SW_RS, 1, tx_keybind, KEY_HELP, 3}, + {"Move forward", nullptr, HOS_KEY_MOVE_UP, HOS_KEY_MOVE_UP, 1, tx_keybind, KEY_HELP, 3}, + {"Move back", nullptr, HOS_KEY_MOVE_DOWN, HOS_KEY_MOVE_DOWN, 1, tx_keybind, KEY_HELP, 3}, + {"Move left", nullptr, HOS_KEY_MOVE_LEFT, HOS_KEY_MOVE_LEFT, 1, tx_keybind, KEY_HELP, 3}, + {"Move right", nullptr, HOS_KEY_MOVE_RIGHT, HOS_KEY_MOVE_RIGHT, 1, tx_keybind, KEY_HELP, 3}, + {"Camera up", nullptr, HOS_KEY_CAM_UP, HOS_KEY_CAM_UP, 1, tx_keybind, KEY_HELP, 3}, + {"Camera down", nullptr, HOS_KEY_CAM_DOWN, HOS_KEY_CAM_DOWN, 1, tx_keybind, KEY_HELP, 3}, + {"Camera left", nullptr, HOS_KEY_CAM_LEFT, HOS_KEY_CAM_LEFT, 1, tx_keybind, KEY_HELP, 3}, + {"Camera right", nullptr, HOS_KEY_CAM_RIGHT, HOS_KEY_CAM_RIGHT, 1, tx_keybind, KEY_HELP, 3}, + {"D-pad left", nullptr, HOS_SW_LEFT, HOS_SW_LEFT, 1, tx_keybind, KEY_HELP, 3}, + {"D-pad up", nullptr, HOS_SW_UP, HOS_SW_UP, 1, tx_keybind, KEY_HELP, 3}, + {"D-pad right", nullptr, HOS_SW_RIGHT, HOS_SW_RIGHT, 1, tx_keybind, KEY_HELP, 3}, + {"D-pad down", nullptr, HOS_SW_DOWN, HOS_SW_DOWN, 1, tx_keybind, KEY_HELP, 3}, + {"Reset keys", nullptr, 0, 0, 1, tx_reset, "Puts every key and mouse button back as they came.", 3}, {"Controller in background", &opts.pad_background, 0, 1, 1, tx_onoff, "On: the controller keeps playing the game while another window has the focus, for streaming or playing beside other " - "apps. The keyboard and mouse still need the game's window."}, - {"Text speed", &opts.text_speed, 0, 3, 1, tx_text_speed, - "How fast the letters of dialogue and other messages appear. Normal is the game's own pace, with its pauses. Instant shows " - "each page at once, as pressing A does."}, - - {"Controls", nullptr, 0, 0, 0, nullptr, // (hookshot: opens its page) - "Which gamepad button and which keys press each of the Switch's buttons."}, + "apps. The keyboard and mouse still need the game's window.", 4}, + {"Button A", nullptr, HOS_SW_A, HOS_SW_A, 1, tx_padbind, PAD_HELP, 4}, + {"Button B", nullptr, HOS_SW_B, HOS_SW_B, 1, tx_padbind, PAD_HELP, 4}, + {"Button X", nullptr, HOS_SW_X, HOS_SW_X, 1, tx_padbind, PAD_HELP, 4}, + {"Button Y", nullptr, HOS_SW_Y, HOS_SW_Y, 1, tx_padbind, PAD_HELP, 4}, + {"Button L", nullptr, HOS_SW_L, HOS_SW_L, 1, tx_padbind, PAD_HELP, 4}, + {"Button R", nullptr, HOS_SW_R, HOS_SW_R, 1, tx_padbind, PAD_HELP, 4}, + {"Button ZL", nullptr, HOS_SW_ZL, HOS_SW_ZL, 1, tx_padbind, PAD_HELP, 4}, + {"Button ZR", nullptr, HOS_SW_ZR, HOS_SW_ZR, 1, tx_padbind, PAD_HELP, 4}, + {"Plus", nullptr, HOS_SW_PLUS, HOS_SW_PLUS, 1, tx_padbind, PAD_HELP, 4}, + {"Minus", nullptr, HOS_SW_MINUS, HOS_SW_MINUS, 1, tx_padbind, PAD_HELP, 4}, + {"Left stick press", nullptr, HOS_SW_LS, HOS_SW_LS, 1, tx_padbind, PAD_HELP, 4}, + {"Right stick press", nullptr, HOS_SW_RS, HOS_SW_RS, 1, tx_padbind, PAD_HELP, 4}, + {"D-pad left", nullptr, HOS_SW_LEFT, HOS_SW_LEFT, 1, tx_padbind, PAD_HELP, 4}, + {"D-pad up", nullptr, HOS_SW_UP, HOS_SW_UP, 1, tx_padbind, PAD_HELP, 4}, + {"D-pad right", nullptr, HOS_SW_RIGHT, HOS_SW_RIGHT, 1, tx_padbind, PAD_HELP, 4}, + {"D-pad down", nullptr, HOS_SW_DOWN, HOS_SW_DOWN, 1, tx_padbind, PAD_HELP, 4}, + {"Reset buttons", nullptr, 0, 0, 1, tx_reset, "Puts every controller button back as it came.", 4}, }; constexpr int kRows = sizeof rows / sizeof rows[0]; -constexpr int kMenuWidth = 360; // (at a scale of 1: 720 lines) +constexpr int kMenuWidth = 540; // (at a scale of 1: 720 lines) std::atomic open_now{false}; -int selected = 0; -bool menu_refresh = false; +int cur_tab = 0, selected = 0; // (selected: the row's index in rows, on cur_tab) +std::atomic menu_refresh{false}; std::string ini_path() { char exe[MAX_PATH]; const DWORD n = GetModuleFileNameA(nullptr, exe, MAX_PATH); @@ -155,117 +291,30 @@ const char* ini_key(const Row& r) { k[n] = 0; return k; } -// (hookshot) the controls, kept by the host (host/nn_hid.c reads them back from here): pad_ + + + + + + + + +
Settings +
+
+ + + +
+

+ +""" + + +def main(): + try: + server = ThreadingHTTPServer(("127.0.0.1", PORT), Handler) + except OSError: # (one is running already: its page is opened) + webbrowser.open("http://127.0.0.1:%d/" % PORT) + print("The launcher is already running: its page was opened in your browser.") + return + if not os.path.exists(CONFIG): + write_json(CONFIG, load_config()) + threading.Thread(target=checker.loop, daemon=True).start() + url = "http://127.0.0.1:%d/" % PORT + print("Hookshot Launcher is running at %s\nIts page is open in your browser. Close this window to stop the launcher." % url, flush=True) + if not os.environ.get("HOOKSHOT_NO_BROWSER"): + webbrowser.open(url) + try: + server.serve_forever() + except KeyboardInterrupt: + job.cancel() + + +if __name__ == "__main__": + main()