mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-12 18:31:05 +00:00
The branch forked 79 commits before main and 98 before dev. Resolutions: 1. Aspect ratio: dev's implementation stays (camera patch at page +56 on 1.4.3, tools/ultrawide_layout.py layouts per size in romfs-patches-<w>x<h>, the aspect = fit|stretch setting). The PR's second implementation is dropped: the aspect sets of patches-v121.txt and patches-v143.txt (the 1.4.3 one patched the same viewport sites as dev's), tools/aspect_layouts.py, the resolution.c shapes that limited game resolutions to 16:9, 16:10 and 21:9, and the picture_fit setting. Its letterbox code (gfx_fit_rect) is kept and reads TOTK_ASPECT. 2. Code dev changed where the PR moved it: the stick dead zone, the test gamepad and the paddles go to input.c; the DPI awareness, keep-awake and IME detach of gfx_window.c go to gfx_window_win.c; device fault reports and GPU breadcrumbs to gfx_present.c. gfx_window_dpi gets an SDL version. 3. dev's Win32 calls in shared code go through platform.h: the shader cache pack reader (PlatFile), the romfs overlay checks, the audio pacing waits, the motion, ASTC and device fault locks. plat_cpu_usable and plat_commit_info get Linux versions; plat_cpu_count counts the machine's processors on both. dpi_probe.c becomes dpi_probe_win.c. 4. assume_focus defaults to 1 as the PR has it (a load interrupted by a focus loss can stop for good) and stays live, with dev's Game tab row. 5. The frame limits 40 and 45 join dev's list. The BC 3D storage fallback for Vega only runs with TOTK_BLOCK_VIEWS_3D=1, since dev makes 3D BC images without block views by default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
686 lines
32 KiB
C
686 lines
32 KiB
C
// Host input for nn::hid (runtime/ov_input.c): an SDL3 gamepad (no SDL window needed), the keyboard and the mouse,
|
|
// merged into Switch npad button bits and stick axes. Button order follows libnx HidNpadButton (ISC). The key and pad
|
|
// bindings are the tables in input_map.c (totk.ini overrides them). The keyboard and the mouse come from the window
|
|
// system's back end (input_wsys.h: input_win.c with Win32, input_sdl.c with SDL), which also owns the cursor capture.
|
|
// Keys and their map: docs/controls.md.
|
|
// The gamepad also rumbles (input_rumble) and, when it has a gyro and an accelerometer, feeds the six-axis sensor from a
|
|
// thread of its own (input_motion_start). Every SDL call on the gamepad is made under `lock`.
|
|
#include <math.h>
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <SDL3/SDL.h>
|
|
|
|
#include "cfg.h"
|
|
#include "host.h"
|
|
#include "input.h"
|
|
#include "input_map.h"
|
|
#include "input_wsys.h"
|
|
#include "nvn_api.h"
|
|
#include "ui/menu.h"
|
|
#include "vk.h"
|
|
#include "platform/platform.h"
|
|
|
|
enum { STICK_MAX = 32767, STICK_DIR = 16384, TRIGGER_ON = 16384, RESCAN_MS = 1000 };
|
|
|
|
static PlatMutex lock = PLAT_MUTEX_INIT;
|
|
static int state; // 0 not started, 1 running, -1 disabled or SDL failed
|
|
static SDL_Gamepad *pad;
|
|
static uint64_t next_scan;
|
|
static int pad_rumble; // the open gamepad has rumble motors
|
|
static volatile int32_t pad_labels[4]; // ... and the labels of its south, east, west, north buttons (PAD_LABEL_*), any thread
|
|
static int pad_east_a; // ... and labels its east face button A (Nintendo layout under TOTK_PAD_LAYOUT=auto)
|
|
static int pad_layout = -1; // PAD_LAYOUT_* in effect for the open gamepad, -1 to pick again; live (cfg_version)
|
|
static unsigned pad_layout_seen;
|
|
static volatile int32_t pad_layout_shown = -1; // pad_layout for the menu (any thread), -1 without a gamepad
|
|
static SDL_JoystickID sensor_pad; // the gamepad whose sensors the motion thread set up, 0 none
|
|
static int sensor_ok; // ... and it has both sensors, enabled
|
|
static int motion_thread; // 0 not started, 1 running, -1 off (TOTK_MOTION=0, no SDL)
|
|
|
|
// SDL buttons of the gamepad inputs (input_map.h PAD_*), by position; PAD_ZL and PAD_ZR are the triggers (axes).
|
|
static const SDL_GamepadButton pad_sdl[PAD_COUNT] = {
|
|
[PAD_SOUTH] = SDL_GAMEPAD_BUTTON_SOUTH, [PAD_EAST] = SDL_GAMEPAD_BUTTON_EAST, [PAD_WEST] = SDL_GAMEPAD_BUTTON_WEST,
|
|
[PAD_NORTH] = SDL_GAMEPAD_BUTTON_NORTH, [PAD_LSTICK] = SDL_GAMEPAD_BUTTON_LEFT_STICK,
|
|
[PAD_RSTICK] = SDL_GAMEPAD_BUTTON_RIGHT_STICK, [PAD_L] = SDL_GAMEPAD_BUTTON_LEFT_SHOULDER,
|
|
[PAD_R] = SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, [PAD_START] = SDL_GAMEPAD_BUTTON_START, [PAD_BACK] = SDL_GAMEPAD_BUTTON_BACK,
|
|
[PAD_DPAD_LEFT] = SDL_GAMEPAD_BUTTON_DPAD_LEFT, [PAD_DPAD_UP] = SDL_GAMEPAD_BUTTON_DPAD_UP,
|
|
[PAD_DPAD_RIGHT] = SDL_GAMEPAD_BUTTON_DPAD_RIGHT, [PAD_DPAD_DOWN] = SDL_GAMEPAD_BUTTON_DPAD_DOWN,
|
|
[PAD_LPADDLE1] = SDL_GAMEPAD_BUTTON_LEFT_PADDLE1, [PAD_RPADDLE1] = SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1,
|
|
[PAD_LPADDLE2] = SDL_GAMEPAD_BUTTON_LEFT_PADDLE2, [PAD_RPADDLE2] = SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2,
|
|
};
|
|
|
|
// TOTK_INPUT_SCRIPT=<file>: lines "<frame> <button> [hold]" or "<frame> LSTICK|RSTICK <x> <y> [hold]", ORed into the real input.
|
|
// <frame> and its hold are 60 Hz ticks of game time since the first present (frame_step_ticks: presents at 60 fps), so
|
|
// the title menus get the presses at the same game time at any frame limit. A line "@<ms> ..." is timed instead: <ms>
|
|
// and its hold are milliseconds of the world clock (frame_step_world_us, game time since the loaded save's first
|
|
// frame), so it lands at the same game time at any frame rate and load length. A timed event shorter than a frame is
|
|
// applied once. In a script with timed lines (a recording) the untimed ones belong before the world clock: they stop
|
|
// when it starts, as the load ends at another tick in each run.
|
|
// at, hold: ticks, or microseconds of the world clock when timed.
|
|
typedef struct { uint64_t at, hold; int bit, stick, timed; int32_t x, y; int logged; char text[48]; } ScriptEv;
|
|
static const struct { const char *name; int bit; } script_buttons[] = {
|
|
{"A", B_A}, {"B", B_B}, {"X", B_X}, {"Y", B_Y}, {"LCLICK", B_STICK_L}, {"RCLICK", B_STICK_R}, {"L", B_L}, {"R", B_R},
|
|
{"ZL", B_ZL}, {"ZR", B_ZR}, {"PLUS", B_PLUS}, {"MINUS", B_MINUS}, {"LEFT", B_LEFT}, {"UP", B_UP}, {"RIGHT", B_RIGHT}, {"DOWN", B_DOWN},
|
|
};
|
|
static ScriptEv *script_ev;
|
|
static int script_n, script_state; // 0 not loaded, 1 loaded
|
|
static int script_mixed; // the script has timed lines: its untimed ones end where the world clock starts
|
|
|
|
static void script_load(void) {
|
|
script_state = 1;
|
|
const char *path = cfg_get("TOTK_INPUT_SCRIPT");
|
|
if (!path || !*path) return;
|
|
FILE *f = fopen(path, "r");
|
|
if (!f) { rt_log("input: cannot open TOTK_INPUT_SCRIPT %s", path); return; }
|
|
char line[256];
|
|
int cap = 0, ln = 0;
|
|
while (fgets(line, sizeof line, f)) {
|
|
ln++;
|
|
char *h = strchr(line, '#');
|
|
if (h) *h = 0;
|
|
char name[32];
|
|
double at;
|
|
int off = 0, timed = line[0] == '@';
|
|
if (sscanf(line + timed, "%lf %31s%n", &at, name, &off) < 2 || at < 0) continue;
|
|
off += timed;
|
|
double unit = timed ? 1000.0 : 1.0; // ms to us, or presents
|
|
ScriptEv e = {.at = (uint64_t)(at * unit + 0.5), .hold = timed ? 66667 : 4, .bit = -1, .timed = timed};
|
|
long a, b;
|
|
double c;
|
|
if (!strcmp(name, "LSTICK") || !strcmp(name, "RSTICK")) {
|
|
int n = sscanf(line + off, "%ld %ld %lf", &a, &b, &c);
|
|
if (n < 2) { rt_log("input: script line %d: %s needs x y [hold]", ln, name); continue; }
|
|
e.stick = name[0] == 'L' ? 1 : 2; e.x = (int32_t)a; e.y = (int32_t)b;
|
|
if (n >= 3 && c > 0) e.hold = (uint64_t)(c * unit + 0.5);
|
|
snprintf(e.text, sizeof e.text, "%s %d %d", name, e.x, e.y);
|
|
} else {
|
|
for (size_t i = 0; i < sizeof script_buttons / sizeof *script_buttons; i++)
|
|
if (!strcmp(name, script_buttons[i].name)) e.bit = script_buttons[i].bit;
|
|
if (e.bit < 0) { rt_log("input: script line %d: unknown button %s", ln, name); continue; }
|
|
if (sscanf(line + off, "%lf", &c) == 1 && c > 0) e.hold = (uint64_t)(c * unit + 0.5);
|
|
snprintf(e.text, sizeof e.text, "%s", name);
|
|
}
|
|
if (script_n == cap) {
|
|
cap = cap ? cap * 2 : 32;
|
|
script_ev = realloc(script_ev, (size_t)cap * sizeof *script_ev);
|
|
if (!script_ev) { script_n = 0; fclose(f); return; }
|
|
}
|
|
script_ev[script_n++] = e;
|
|
script_mixed |= timed;
|
|
}
|
|
fclose(f);
|
|
rt_log("input: script %s, %d events", path, script_n);
|
|
}
|
|
|
|
static void script_apply(uint64_t *b, int32_t sl[2], int32_t sr[2]) {
|
|
if (!script_state) script_load();
|
|
if (!script_n) return;
|
|
uint64_t fr = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED), tick = frame_step_ticks();
|
|
int64_t w = frame_step_world_us();
|
|
for (int i = 0; i < script_n; i++) {
|
|
ScriptEv *e = &script_ev[i];
|
|
if (e->timed ? w < 0 : script_mixed && w >= 0) continue;
|
|
uint64_t now = e->timed ? (uint64_t)w : tick;
|
|
if (now < e->at || (now >= e->at + e->hold && (e->logged || !e->timed))) continue;
|
|
if (!e->logged) {
|
|
e->logged = 1;
|
|
if (e->timed) rt_log("input: script frame %llu at %.1f ms: %s", (unsigned long long)fr, w / 1000.0, e->text);
|
|
else rt_log("input: script frame %llu tick %llu: %s", (unsigned long long)fr, (unsigned long long)tick, e->text);
|
|
}
|
|
if (e->stick) { int32_t *s = e->stick == 1 ? sl : sr; s[0] = e->x; s[1] = e->y; }
|
|
else *b |= 1ull << e->bit;
|
|
}
|
|
}
|
|
|
|
// TOTK_INPUT_RECORD=<file>: writes the merged input as TOTK_INPUT_SCRIPT lines, one per button press and per stick
|
|
// segment, so a played session replays as a script. A stick segment ends when an axis moves by more than REC_STICK_STEP.
|
|
// An event that begins on the world clock is written timed ("@<ms>", hold in ms), one before it in ticks
|
|
// (frame_step_ticks). When the world clock starts, held buttons and sticks are cut there and go on as timed events.
|
|
enum { REC_STICK_STEP = 2048 };
|
|
static FILE *rec;
|
|
static int rec_state; // 0 not opened, 1 recording, -1 off
|
|
static uint64_t rec_down[16]; // tick + 1 at which each button went down, 0 while up
|
|
static int64_t rec_down_w[16]; // the world clock then, -1 before it started
|
|
static uint64_t rec_stick_from[2]; // tick + 1 at which the current non-zero stick segment began
|
|
static int64_t rec_stick_w[2];
|
|
static int32_t rec_stick[2][2];
|
|
static int64_t rec_last_w = -1; // the world clock at the previous apply
|
|
|
|
// "<tick> <what> <hold>" or "@<ms> <what> <hold ms>": from tick from and world clock from_w to now.
|
|
static void rec_line(uint64_t from, int64_t from_w, uint64_t fr, int64_t w, const char *what) {
|
|
if (from_w >= 0 && w >= from_w)
|
|
fprintf(rec, "@%.1f %s %.1f\n", from_w / 1000.0, what, (w > from_w ? w - from_w : 1000) / 1000.0);
|
|
else
|
|
fprintf(rec, "%llu %s %llu\n", (unsigned long long)from, what, (unsigned long long)(fr > from ? fr - from : 1));
|
|
}
|
|
|
|
static void rec_close_stick(int s, uint64_t fr, int64_t w) {
|
|
if (!rec_stick_from[s]) return;
|
|
char what[48];
|
|
snprintf(what, sizeof what, "%s %d %d", s ? "RSTICK" : "LSTICK", rec_stick[s][0], rec_stick[s][1]);
|
|
rec_line(rec_stick_from[s] - 1, rec_stick_w[s], fr, w, what);
|
|
rec_stick_from[s] = 0;
|
|
}
|
|
|
|
static void rec_flush_all(void) {
|
|
if (rec_state != 1) return;
|
|
uint64_t fr = frame_step_ticks() + 1;
|
|
int64_t w = frame_step_world_us();
|
|
for (int i = 0; i < 16; i++)
|
|
if (rec_down[i]) { rec_line(rec_down[i] - 1, rec_down_w[i], fr, w, script_buttons[i].name); rec_down[i] = 0; }
|
|
rec_close_stick(0, fr, w);
|
|
rec_close_stick(1, fr, w);
|
|
fflush(rec);
|
|
}
|
|
|
|
static void rec_apply(uint64_t b, const int32_t sl[2], const int32_t sr[2]) {
|
|
if (!rec_state) {
|
|
const char *path = cfg_get("TOTK_INPUT_RECORD"); // no default: not recording when unset
|
|
rec = path && *path ? fopen(path, "w") : NULL;
|
|
rec_state = rec ? 1 : -1;
|
|
if (path && *path && !rec) rt_log("input: cannot open TOTK_INPUT_RECORD %s", path);
|
|
if (rec) { fprintf(rec, "# Recorded input (TOTK_INPUT_RECORD); frame = 60 Hz ticks of game time, @ms = world clock.\n"); atexit(rec_flush_all);
|
|
rt_log("input: recording to %s", path); }
|
|
}
|
|
if (rec_state != 1) return;
|
|
uint64_t fr = frame_step_ticks();
|
|
int64_t w = frame_step_world_us();
|
|
int wrote = 0;
|
|
if (w >= 0 && rec_last_w < 0) { // the world clock starts: what is held goes on as timed events from @0
|
|
for (int i = 0; i < 16; i++)
|
|
if (rec_down[i]) {
|
|
rec_line(rec_down[i] - 1, -1, fr, -1, script_buttons[i].name);
|
|
rec_down[i] = fr + 1, rec_down_w[i] = w;
|
|
wrote = 1;
|
|
}
|
|
for (int s = 0; s < 2; s++)
|
|
if (rec_stick_from[s]) {
|
|
rec_close_stick(s, fr, -1);
|
|
rec_stick_from[s] = fr + 1, rec_stick_w[s] = w;
|
|
wrote = 1;
|
|
}
|
|
}
|
|
rec_last_w = w;
|
|
for (int i = 0; i < 16; i++) {
|
|
int bit = script_buttons[i].bit, down = (b >> bit & 1) != 0;
|
|
if (down && !rec_down[i]) rec_down[i] = fr + 1, rec_down_w[i] = w;
|
|
else if (!down && rec_down[i]) {
|
|
rec_line(rec_down[i] - 1, rec_down_w[i], fr, w, script_buttons[i].name);
|
|
rec_down[i] = 0;
|
|
wrote = 1;
|
|
}
|
|
}
|
|
const int32_t *st[2] = {sl, sr};
|
|
for (int s = 0; s < 2; s++) {
|
|
int moved = abs(st[s][0] - rec_stick[s][0]) > REC_STICK_STEP || abs(st[s][1] - rec_stick[s][1]) > REC_STICK_STEP ||
|
|
((st[s][0] || st[s][1]) != (rec_stick[s][0] || rec_stick[s][1]));
|
|
if (!moved) continue;
|
|
if (rec_stick_from[s]) { rec_close_stick(s, fr, w); wrote = 1; }
|
|
rec_stick[s][0] = st[s][0]; rec_stick[s][1] = st[s][1];
|
|
if (st[s][0] || st[s][1]) rec_stick_from[s] = fr + 1, rec_stick_w[s] = w;
|
|
}
|
|
if (wrote) fflush(rec);
|
|
}
|
|
|
|
static float mouse_sens = -1; // TOTK_MOUSE_SENS, 0 = mouse camera off; live (cfg_version)
|
|
static unsigned mouse_sens_seen;
|
|
|
|
static float mouse_sens_get(void) {
|
|
unsigned ver = cfg_version();
|
|
if (mouse_sens >= 0 && ver == mouse_sens_seen) return mouse_sens;
|
|
mouse_sens_seen = ver;
|
|
double v = cfg_double("TOTK_MOUSE_SENS");
|
|
mouse_sens = (float)(v < 0 ? 0 : v > 10 ? 10 : v);
|
|
return mouse_sens;
|
|
}
|
|
|
|
|
|
// The mouse camera as a right-stick sample, held for the whole presented frame. Under `lock`.
|
|
static void mouse_camera(int32_t out[2]) {
|
|
static uint64_t frame = ~0ull, t0;
|
|
static int32_t held[2];
|
|
uint64_t now = plat_now_ns();
|
|
int32_t dx, dy;
|
|
if (!ikb_captured()) {
|
|
ikb_mouse_take(&dx, &dy);
|
|
held[0] = held[1] = 0;
|
|
frame = ~0ull;
|
|
} else {
|
|
uint64_t fr = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED);
|
|
double age = frame == ~0ull ? 0 : (double)(now - t0) / 1e9;
|
|
if (fr != frame) {
|
|
double dt = age < 0.004 ? 0.004 : age > 0.1 ? 0.1 : age;
|
|
ikb_mouse_take(&dx, &dy);
|
|
input_mouse_stick(dx, dy, dt, mouse_sens_get(), held);
|
|
frame = fr;
|
|
t0 = now;
|
|
} else if (age > 0.1) {
|
|
held[0] = held[1] = 0; // frames stopped: do not hold a deflection
|
|
}
|
|
}
|
|
out[0] = held[0];
|
|
out[1] = held[1];
|
|
}
|
|
|
|
static float stick_dead = -1; // TOTK_STICK_DEADZONE as a fraction of full deflection; live (cfg_version)
|
|
static unsigned stick_dead_seen;
|
|
|
|
static float stick_dead_get(void) {
|
|
unsigned ver = cfg_version();
|
|
if (stick_dead >= 0 && ver == stick_dead_seen) return stick_dead;
|
|
stick_dead_seen = ver;
|
|
double v = cfg_double("TOTK_STICK_DEADZONE");
|
|
stick_dead = (float)(v < 0 ? 0 : v > 50 ? 50 : v) / 100.f;
|
|
return stick_dead;
|
|
}
|
|
|
|
// One gamepad stick through the dead zone; SDL's Y grows downwards, the Switch's upwards.
|
|
static void stick(SDL_GamepadAxis ax, SDL_GamepadAxis ay, float dead, int32_t out[2]) {
|
|
input_pad_stick(SDL_GetGamepadAxis(pad, ax), -(int32_t)SDL_GetGamepadAxis(pad, ay), dead, out);
|
|
}
|
|
|
|
// Open the first gamepad when none is open or the open one went away; logs once per change.
|
|
static void scan_pads(void) {
|
|
if (pad && SDL_GamepadConnected(pad)) return;
|
|
if (pad) {
|
|
SDL_CloseGamepad(pad); pad = NULL; sensor_pad = 0; sensor_ok = 0;
|
|
plat_xchg32(&pad_layout_shown, -1);
|
|
for (int i = 0; i < 4; i++) plat_xchg32(&pad_labels[i], PAD_LABEL_NONE);
|
|
rt_log("input: gamepad disconnected");
|
|
}
|
|
int n = 0;
|
|
SDL_JoystickID *ids = SDL_GetGamepads(&n);
|
|
for (int i = 0; ids && i < n && !pad; i++) pad = SDL_OpenGamepad(ids[i]);
|
|
SDL_free(ids);
|
|
if (!pad) return;
|
|
pad_rumble = SDL_GetBooleanProperty(SDL_GetGamepadProperties(pad), SDL_PROP_GAMEPAD_CAP_RUMBLE_BOOLEAN, false);
|
|
pad_east_a = SDL_GetGamepadButtonLabel(pad, SDL_GAMEPAD_BUTTON_EAST) == SDL_GAMEPAD_BUTTON_LABEL_A;
|
|
for (int i = 0; i < 4; i++) {
|
|
SDL_GamepadButtonLabel lb = SDL_GetGamepadButtonLabel(pad, pad_sdl[PAD_SOUTH + i]);
|
|
plat_xchg32(&pad_labels[i], lb == SDL_GAMEPAD_BUTTON_LABEL_A ? PAD_LABEL_A
|
|
: lb == SDL_GAMEPAD_BUTTON_LABEL_B ? PAD_LABEL_B
|
|
: lb == SDL_GAMEPAD_BUTTON_LABEL_X ? PAD_LABEL_X
|
|
: lb == SDL_GAMEPAD_BUTTON_LABEL_Y ? PAD_LABEL_Y
|
|
: lb == SDL_GAMEPAD_BUTTON_LABEL_CROSS ? PAD_LABEL_CROSS
|
|
: lb == SDL_GAMEPAD_BUTTON_LABEL_CIRCLE ? PAD_LABEL_CIRCLE
|
|
: lb == SDL_GAMEPAD_BUTTON_LABEL_SQUARE ? PAD_LABEL_SQUARE
|
|
: lb == SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE ? PAD_LABEL_TRIANGLE
|
|
: PAD_LABEL_NONE);
|
|
}
|
|
pad_layout = -1;
|
|
int motion = SDL_GamepadHasSensor(pad, SDL_SENSOR_GYRO) && SDL_GamepadHasSensor(pad, SDL_SENSOR_ACCEL);
|
|
rt_log("input: gamepad \"%s\" (%s, %s, east button %s)", SDL_GetGamepadName(pad), pad_rumble ? "rumble" : "no rumble",
|
|
motion ? "gyro and accelerometer" : "no motion sensors", pad_east_a ? "labelled A" : "not labelled A");
|
|
}
|
|
|
|
// Under lock: the layout of the open gamepad, picked again when a setting or the gamepad changed; logs each change.
|
|
static int pad_layout_get(void) {
|
|
unsigned ver = cfg_version();
|
|
if (pad_layout >= 0 && ver == pad_layout_seen) return pad_layout;
|
|
pad_layout_seen = ver;
|
|
int was = pad_layout;
|
|
const char *set = cfg_value("TOTK_PAD_LAYOUT");
|
|
pad_layout = input_pad_layout_pick(set, pad_east_a);
|
|
if (pad_layout != was)
|
|
rt_log("input: gamepad layout %s (pad_layout = %s)", pad_layout == PAD_LAYOUT_XBOX ? "xbox" : "nintendo", set);
|
|
plat_xchg32(&pad_layout_shown, pad_layout);
|
|
return pad_layout;
|
|
}
|
|
|
|
// The TOTK_BIND_* values last applied, one per line; the tables are built again when one changes (the menu's remapping rows write
|
|
// them while the game runs). Under lock.
|
|
static unsigned binds_seen = ~0u;
|
|
static char binds_applied[4096];
|
|
|
|
static void binds_refresh(void) {
|
|
unsigned v = cfg_version();
|
|
if (v == binds_seen) return;
|
|
binds_seen = v;
|
|
char now[sizeof binds_applied];
|
|
size_t len = 0;
|
|
for (int a = 0; a < ACTION_COUNT; a++) {
|
|
char var[32];
|
|
snprintf(var, sizeof var, "TOTK_BIND_%s", input_action_name(a));
|
|
const char *s = cfg_get(var);
|
|
len += (size_t)snprintf(now + len, len < sizeof now ? sizeof now - len : 0, "%s\n", s ? s : "");
|
|
}
|
|
if (!strcmp(now, binds_applied)) return;
|
|
memcpy(binds_applied, now, sizeof now);
|
|
input_map_configure();
|
|
}
|
|
|
|
// TOTK_TEST_PAD=<lx>,<ly>,<rx>,<ry> (test runs): an SDL virtual gamepad whose sticks rest at these raw SDL axis values
|
|
// (-32768..32767, down is +y), attached before the first scan so it is the gamepad the game reads: drift and full
|
|
// deflection go through the same mapping and dead zone as a real pad.
|
|
static void test_pad_attach(void) {
|
|
static SDL_Joystick *joy; // kept open: a virtual joystick's axes are set through an open handle
|
|
const char *e = cfg_get("TOTK_TEST_PAD");
|
|
int v[4];
|
|
if (!e || !*e) return;
|
|
if (sscanf(e, "%d,%d,%d,%d", &v[0], &v[1], &v[2], &v[3]) != 4) {
|
|
rt_log("input: TOTK_TEST_PAD=%s is not <lx>,<ly>,<rx>,<ry>; no test gamepad", e);
|
|
return;
|
|
}
|
|
SDL_VirtualJoystickDesc d;
|
|
SDL_INIT_INTERFACE(&d);
|
|
d.type = SDL_JOYSTICK_TYPE_GAMEPAD;
|
|
d.naxes = SDL_GAMEPAD_AXIS_COUNT;
|
|
d.nbuttons = SDL_GAMEPAD_BUTTON_COUNT;
|
|
d.name = "TotK test gamepad";
|
|
SDL_JoystickID id = SDL_AttachVirtualJoystick(&d);
|
|
joy = id ? SDL_OpenJoystick(id) : NULL;
|
|
if (!joy) {
|
|
rt_log("input: test gamepad not attached (%s)", SDL_GetError());
|
|
return;
|
|
}
|
|
static const SDL_GamepadAxis axes[4] = {SDL_GAMEPAD_AXIS_LEFTX, SDL_GAMEPAD_AXIS_LEFTY, SDL_GAMEPAD_AXIS_RIGHTX,
|
|
SDL_GAMEPAD_AXIS_RIGHTY};
|
|
for (int i = 0; i < 4; i++)
|
|
SDL_SetJoystickVirtualAxis(joy, axes[i], (Sint16)(v[i] < -32768 ? -32768 : v[i] > 32767 ? 32767 : v[i]));
|
|
rt_log("input: test gamepad attached, sticks at %d,%d %d,%d", v[0], v[1], v[2], v[3]);
|
|
}
|
|
|
|
static void start(void) {
|
|
binds_refresh();
|
|
const char *e = cfg_get("TOTK_INPUT");
|
|
if (e && !strcmp(e, "none")) { state = -1; rt_log("input: disabled (TOTK_INPUT=none)"); return; }
|
|
if (!SDL_Init(SDL_INIT_GAMEPAD)) { state = -1; rt_log("input: SDL gamepad init failed (%s), keyboard only", SDL_GetError()); }
|
|
else state = 1;
|
|
if (state == 1) test_pad_attach();
|
|
if (state == 1) {
|
|
scan_pads();
|
|
if (!pad) rt_log("input: no gamepad (keyboard only until one is connected)");
|
|
}
|
|
if (state == -1) state = 2; // keyboard only
|
|
}
|
|
|
|
static void stick_bits(uint64_t *b, int32_t x, int32_t y, int first) {
|
|
if (x < -STICK_DIR) *b |= 1ull << (first + 0);
|
|
if (y > STICK_DIR) *b |= 1ull << (first + 1);
|
|
if (x > STICK_DIR) *b |= 1ull << (first + 2);
|
|
if (y < -STICK_DIR) *b |= 1ull << (first + 3);
|
|
}
|
|
|
|
static void pick(int32_t *x, int32_t *y, int32_t kx, int32_t ky) {
|
|
int64_t a = (int64_t)*x * *x + (int64_t)*y * *y, b = (int64_t)kx * kx + (int64_t)ky * ky;
|
|
if (b > a) { *x = kx; *y = ky; }
|
|
}
|
|
|
|
// Under lock, state 1: the open gamepad's inputs by position (pd[PAD_*] != 0 held); 0 without a connected gamepad.
|
|
static int pad_physical(uint8_t pd[PAD_COUNT]) {
|
|
if ((!pad || !SDL_GamepadConnected(pad)) && plat_now_ns() / 1000000u >= next_scan) { next_scan = plat_now_ns() / 1000000u + RESCAN_MS; scan_pads(); }
|
|
SDL_UpdateGamepads();
|
|
if (!pad || !SDL_GamepadConnected(pad)) return 0;
|
|
for (int p = PAD_SOUTH; p < PAD_ZL; p++) pd[p] = SDL_GetGamepadButton(pad, pad_sdl[p]);
|
|
pd[PAD_ZL] = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > TRIGGER_ON;
|
|
pd[PAD_ZR] = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > TRIGGER_ON;
|
|
for (int p = PAD_ZR + 1; p < PAD_COUNT; p++) pd[p] = SDL_GetGamepadButton(pad, pad_sdl[p]); // false without paddles
|
|
return 1;
|
|
}
|
|
|
|
// Under lock, state 1: the open gamepad's buttons (Switch bits after TOTK_PAD_LAYOUT and the TOTK_BIND_* lines) and
|
|
// sticks; 0 without a connected gamepad.
|
|
static int pad_read(uint64_t *b, int32_t sl[2], int32_t sr[2]) {
|
|
uint8_t pd[PAD_COUNT] = {0};
|
|
if (!pad_physical(pd)) return 0;
|
|
*b |= input_map_pad_buttons(pd, pad_layout_get());
|
|
float dead = stick_dead_get();
|
|
stick(SDL_GAMEPAD_AXIS_LEFTX, SDL_GAMEPAD_AXIS_LEFTY, dead, sl);
|
|
stick(SDL_GAMEPAD_AXIS_RIGHTX, SDL_GAMEPAD_AXIS_RIGHTY, dead, sr);
|
|
return 1;
|
|
}
|
|
|
|
static void poll_merged(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *ry) {
|
|
uint64_t b = 0;
|
|
int32_t sl[2] = {0, 0}, sr[2] = {0, 0};
|
|
plat_mutex_lock(&lock);
|
|
if (!state) start();
|
|
if (state > 0) {
|
|
binds_refresh();
|
|
if (state == 1) pad_read(&b, sl, sr);
|
|
if (ikb_foreground()) {
|
|
uint8_t down[KEY_COUNT] = {0};
|
|
// Mouse buttons count only while the cursor is captured, and a button already held when the capture began
|
|
// (the click that refocused the window) is ignored until it is released.
|
|
static int was_captured;
|
|
static uint8_t mouse_ignore[KEY_COUNT];
|
|
int cap = ikb_captured();
|
|
for (int k = 1; k < KEY_COUNT; k++) {
|
|
down[k] = ikb_down(k);
|
|
if (k < KEY_MOUSE_L || k > KEY_MOUSE_X2) continue;
|
|
if (cap && !was_captured) mouse_ignore[k] = down[k];
|
|
if (!down[k]) mouse_ignore[k] = 0;
|
|
if (!cap || mouse_ignore[k]) down[k] = 0;
|
|
}
|
|
was_captured = cap;
|
|
b |= input_map_buttons(down);
|
|
int32_t k[2];
|
|
input_map_stick(down, 0, k);
|
|
pick(&sl[0], &sl[1], k[0], k[1]);
|
|
input_map_stick(down, 1, k);
|
|
pick(&sr[0], &sr[1], k[0], k[1]);
|
|
mouse_camera(k);
|
|
pick(&sr[0], &sr[1], k[0], k[1]);
|
|
}
|
|
}
|
|
script_apply(&b, sl, sr);
|
|
rec_apply(b, sl, sr);
|
|
// Minus and Plus held together (gamepad or keys) open or close the menu and never reach the game
|
|
static int combo_held;
|
|
int32_t st[4] = {sl[0], sl[1], sr[0], sr[1]};
|
|
int toggle = input_menu_gate(&combo_held, menu_is_open(), &b, st);
|
|
static int overlay_held; // L3+R3 step the performance overlay (F10)
|
|
int overlay = input_overlay_gate(&overlay_held, &b);
|
|
sl[0] = st[0], sl[1] = st[1], sr[0] = st[2], sr[1] = st[3];
|
|
plat_mutex_unlock(&lock);
|
|
if (toggle) menu_toggle();
|
|
if (overlay) menu_perf_cycle();
|
|
stick_bits(&b, sl[0], sl[1], B_SL_LEFT);
|
|
stick_bits(&b, sr[0], sr[1], B_SR_LEFT);
|
|
*buttons = b; *lx = sl[0]; *ly = sl[1]; *rx = sr[0]; *ry = sr[1];
|
|
}
|
|
|
|
// Text entry (swkbd_*.c): while it is open the game reads no input, as behind the keyboard applet; after it closes
|
|
// the game reads none until every button and stick direction is released, so the press that closed it stays there.
|
|
// g_block: 0 off, 1 text entry open, 2 closed, waiting for the release. The cursor is released while it is open.
|
|
static volatile int32_t g_block;
|
|
static volatile int32_t g_menu; // the in-game menu, the same way (input_menu)
|
|
|
|
int input_cursor_allowed(void) {
|
|
const char *in = cfg_get("TOTK_INPUT");
|
|
return g_block != 1 && g_menu != 1 && mouse_sens_get() > 0 && !(in && !strcmp(in, "none"));
|
|
}
|
|
|
|
void input_block(int on) {
|
|
plat_xchg32(&g_block, on ? 1 : 2);
|
|
ikb_cursor_update();
|
|
}
|
|
|
|
// The in-game menu (ui/menu.cpp) blocks the game's input the same way: g_menu 0 closed, 1 open, 2 closed, waiting for
|
|
// the release. The cursor is released while it is open, for the menu's mouse.
|
|
void input_menu(int open) {
|
|
plat_xchg32(&g_menu, open ? 1 : 2);
|
|
if (!open) ikb_menu_closed(); // the capture before Esc comes back
|
|
ikb_cursor_update();
|
|
}
|
|
|
|
// 1 while flag (g_block, g_menu) blocks the game's input; state 2 ends at the first poll with nothing held.
|
|
static int blocked(volatile int32_t *flag, uint64_t buttons) {
|
|
int32_t k = *flag;
|
|
return k && !(k == 2 && !buttons && plat_cas32(flag, 0, 2) == 2);
|
|
}
|
|
|
|
void input_poll(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *ry) {
|
|
poll_merged(buttons, lx, ly, rx, ry);
|
|
int text = blocked(&g_block, *buttons), menu = blocked(&g_menu, *buttons);
|
|
if (!text && !menu) return;
|
|
*buttons = 0; *lx = *ly = *rx = *ry = 0;
|
|
}
|
|
|
|
void input_poll_ui(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *ry) {
|
|
poll_merged(buttons, lx, ly, rx, ry);
|
|
}
|
|
|
|
int input_pad_read(uint64_t *buttons, int32_t *lx, int32_t *ly) {
|
|
uint64_t b = 0;
|
|
int32_t sl[2] = {0, 0}, sr[2] = {0, 0};
|
|
plat_mutex_lock(&lock);
|
|
if (!state) start();
|
|
if (state > 0) binds_refresh();
|
|
int ok = state == 1 && pad_read(&b, sl, sr);
|
|
plat_mutex_unlock(&lock);
|
|
*buttons = b;
|
|
*lx = sl[0];
|
|
*ly = sl[1];
|
|
return ok;
|
|
}
|
|
|
|
uint16_t rumble_motor(float a, float curve, float strength) {
|
|
if (!(a > 0) || !(strength > 0)) return 0;
|
|
float v = powf(a > 1 ? 1 : a, curve) * strength * 65535.0f + 0.5f;
|
|
return v >= 65535.0f ? 65535 : (uint16_t)v;
|
|
}
|
|
|
|
int input_pad_layout(void) {
|
|
return (int)plat_cas32(&pad_layout_shown, 0, 0);
|
|
}
|
|
|
|
int input_pad_label(int pad_id) {
|
|
if (pad_id < PAD_SOUTH || pad_id > PAD_NORTH) return PAD_LABEL_NONE;
|
|
return (int)plat_cas32(&pad_labels[pad_id - PAD_SOUTH], 0, 0);
|
|
}
|
|
|
|
void input_map_snapshot(InputMap *m) {
|
|
plat_mutex_lock(&lock);
|
|
if (!state) start();
|
|
if (state > 0) binds_refresh();
|
|
*m = input_map;
|
|
plat_mutex_unlock(&lock);
|
|
}
|
|
|
|
int input_capture(int reset, int *id) {
|
|
static uint8_t prev_k[KEY_COUNT], prev_p[PAD_COUNT];
|
|
static int prev_esc;
|
|
uint8_t k[KEY_COUNT] = {0}, p[PAD_COUNT] = {0};
|
|
int fg = ikb_foreground();
|
|
for (int i = 1; i < KEY_COUNT; i++) k[i] = fg && ikb_down(i);
|
|
int esc = fg && ikb_escape();
|
|
plat_mutex_lock(&lock);
|
|
if (!state) start();
|
|
if (state == 1) pad_physical(p);
|
|
plat_mutex_unlock(&lock);
|
|
int got = INPUT_CAPTURE_NONE;
|
|
if (!reset) {
|
|
if (esc && !prev_esc) got = INPUT_CAPTURE_CANCEL;
|
|
for (int i = 1; i < KEY_COUNT && !got; i++)
|
|
if (k[i] && !prev_k[i]) got = INPUT_CAPTURE_KEY, *id = i;
|
|
for (int i = 1; i < PAD_COUNT && !got; i++)
|
|
if (p[i] && !prev_p[i]) got = INPUT_CAPTURE_PAD, *id = i;
|
|
}
|
|
memcpy(prev_k, k, sizeof k);
|
|
memcpy(prev_p, p, sizeof p);
|
|
prev_esc = esc;
|
|
return got;
|
|
}
|
|
|
|
int input_rumble_triggers_enabled(void) {
|
|
return cfg_bool("TOTK_RUMBLE_TRIGGERS");
|
|
}
|
|
|
|
int input_rumble(uint16_t low, uint16_t high, uint32_t ms) {
|
|
int ok = 0;
|
|
static int triggers = -1;
|
|
static unsigned seen;
|
|
if (triggers < 0 || seen != cfg_version()) seen = cfg_version(), triggers = input_rumble_triggers_enabled();
|
|
plat_mutex_lock(&lock);
|
|
if (state == 1 && pad && pad_rumble) {
|
|
ok = SDL_RumbleGamepad(pad, low, high, ms);
|
|
if (triggers) SDL_RumbleGamepadTriggers(pad, high, high, ms); // unsupported pads return false: ignored
|
|
}
|
|
plat_mutex_unlock(&lock);
|
|
return ok;
|
|
}
|
|
|
|
// SDL gamepad sensor axes (SDL_sensor.h, a gamepad held in front of you): x to the right, y up out of the face (lying
|
|
// flat face up, +y points at the ceiling), z toward the player (out of the bottom edge). The accelerometer reads the
|
|
// push against gravity in m/s^2 (lying flat: y = +9.81); the gyro reads rad/s, counter-clockwise positive (right hand).
|
|
// Switch six-axis axes (nn::hid, as MacNativeGames' GCMotion feed and its Pro Controller HID path use them): x to the
|
|
// right, y toward the top edge (away from the player), z out of the face; acceleration as gravity reads (lying flat:
|
|
// z = -1 G). So Switch x = SDL x, Switch y = -SDL z, Switch z = SDL y: a rotation (right-handed to right-handed), so the
|
|
// gyro keeps its sense, while the acceleration is also negated (push -> gravity) and scaled to G.
|
|
void input_motion_axes(const float a[3], const float g[3], double acc[3], double gyro[3]) {
|
|
acc[0] = -a[0] / SDL_STANDARD_GRAVITY;
|
|
acc[1] = a[2] / SDL_STANDARD_GRAVITY;
|
|
acc[2] = -a[1] / SDL_STANDARD_GRAVITY;
|
|
gyro[0] = g[0];
|
|
gyro[1] = -g[2];
|
|
gyro[2] = g[1];
|
|
}
|
|
|
|
// Under lock: (re)enable the sensors when the open gamepad changed; 1 if it has both and they are on.
|
|
static int sensors_ready(void) {
|
|
SDL_JoystickID id = SDL_GetGamepadID(pad);
|
|
if (id == sensor_pad) return sensor_ok;
|
|
sensor_pad = id;
|
|
// Nothing drains the SDL event queue: no event per sample (the data still updates, SDL_SendJoystickSensor)
|
|
SDL_SetEventEnabled(SDL_EVENT_GAMEPAD_SENSOR_UPDATE, false);
|
|
sensor_ok =SDL_GamepadHasSensor(pad, SDL_SENSOR_GYRO) && SDL_GamepadHasSensor(pad, SDL_SENSOR_ACCEL) &&
|
|
SDL_SetGamepadSensorEnabled(pad, SDL_SENSOR_GYRO, true) &&
|
|
SDL_SetGamepadSensorEnabled(pad, SDL_SENSOR_ACCEL, true);
|
|
if (sensor_ok) rt_log("input: motion sensors on (gyro %.0f Hz, accelerometer %.0f Hz)",
|
|
(double)SDL_GetGamepadSensorDataRate(pad, SDL_SENSOR_GYRO),
|
|
(double)SDL_GetGamepadSensorDataRate(pad, SDL_SENSOR_ACCEL));
|
|
else rt_log("input: \"%s\" has no gyro and accelerometer: the six-axis sensor reads a controller at rest",
|
|
SDL_GetGamepadName(pad));
|
|
return sensor_ok;
|
|
}
|
|
|
|
int input_motion_enabled(void) {
|
|
static int on = -1;
|
|
static unsigned seen;
|
|
unsigned v = cfg_version();
|
|
if (on < 0 || v != seen) {
|
|
seen = v;
|
|
on = cfg_bool("TOTK_MOTION");
|
|
}
|
|
return on;
|
|
}
|
|
|
|
// 250 Hz: the gamepads update (reports read, rumble expiry) and the newest gyro and accelerometer sample goes into the
|
|
// fusion. Without a gamepad with sensors, or with motion off (TOTK_MOTION=0, live), nothing is fed and the six-axis
|
|
// sensor reads at rest.
|
|
static void motion_main(void *arg) {
|
|
(void)arg;
|
|
for (;;) {
|
|
plat_wait_ns(4000000);
|
|
float a[3], g[3];
|
|
int have = 0;
|
|
plat_mutex_lock(&lock);
|
|
if (pad && SDL_GamepadConnected(pad)) {
|
|
SDL_UpdateGamepads();
|
|
have = sensors_ready() && SDL_GetGamepadSensorData(pad, SDL_SENSOR_ACCEL, a, 3) &&
|
|
SDL_GetGamepadSensorData(pad, SDL_SENSOR_GYRO, g, 3);
|
|
}
|
|
plat_mutex_unlock(&lock);
|
|
if (!have || !input_motion_enabled()) continue;
|
|
double acc[3], gyro[3];
|
|
input_motion_axes(a, g, acc, gyro);
|
|
motion_feed(motion_global(), acc, gyro, kern_now_ns());
|
|
}
|
|
}
|
|
|
|
void input_motion_start(void) {
|
|
plat_mutex_lock(&lock);
|
|
if (!state) start();
|
|
if (!motion_thread) {
|
|
if (!input_motion_enabled()) rt_log("input: motion off (TOTK_MOTION=0); it can be turned on while the game runs");
|
|
if (state != 1) motion_thread = -1;
|
|
else {
|
|
if (!plat_thread_spawn(motion_main, NULL, "motion", 0)) rt_fatal("input: cannot start the motion thread");
|
|
motion_thread = 1;
|
|
}
|
|
}
|
|
plat_mutex_unlock(&lock);
|
|
}
|