Files
Valiantsin DzerakhandClaude Opus 5.5 61e8fd6bb9 Keep the middle and side mouse buttons bound while a game menu has the pointer (#908)
The bindings dropped every mouse button while the cursor was not captured, and the menu pointer releases the capture,
so side buttons bound to L and R turned the menu's tabs only in the frames before the pointer appeared. While the menu
pointer is shown and over the window, the middle and side buttons now count; the left and right buttons stay the
menu's A and B.

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

924 lines
45 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 volatile int32_t g_block, g_menu; // text entry / host menu: 0 off, 1 open, 2 waiting for release
// The device the player used last (input_kbm_last): 1 keyboard and mouse, 0 the gamepad. A pad counts once a button is
// down or a stick is past half way (a drifting stick never switches), the mouse once it moved MOUSE_USED raw counts
// within one poll (a slow deliberate move does; a desk bump does not), the keyboard with any key or mouse button.
enum { PAD_USED = 16384, MOUSE_USED = 6 };
static volatile int32_t g_kbm_last = 1;
int input_kbm_last(void) { return __atomic_load_n(&g_kbm_last, __ATOMIC_RELAXED); }
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 int32_t script_mouse[2]; // one-shot raw counts, consumed by the camera hook; under lock
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, "MOUSE") || !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] == 'M' ? 3 : name[0] == 'L' ? 1 : 2; e.x = (int32_t)a; e.y = (int32_t)b;
if (e.stick == 3) e.hold = 1; // a displacement, never a held speed
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->stick == 3 && e->logged) 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 == 3) {
int64_t x = (int64_t)script_mouse[0] + e->x, y = (int64_t)script_mouse[1] + e->y;
script_mouse[0] = (int32_t)(x < INT32_MIN ? INT32_MIN : x > INT32_MAX ? INT32_MAX : x);
script_mouse[1] = (int32_t)(y < INT32_MIN ? INT32_MIN : y > INT32_MAX ? INT32_MAX : y);
} else 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);
}
void input_direct_mouse_sample(int enabled, int32_t *dx, int32_t *dy) {
plat_mutex_lock(&lock);
enabled = enabled && !g_block && !g_menu;
*dx = script_mouse[0]; *dy = script_mouse[1];
script_mouse[0] = script_mouse[1] = 0;
if (!enabled) { *dx = *dy = 0; }
else if (ikb_captured() && ikb_foreground() && input_cursor_allowed()) {
int32_t x, y;
ikb_mouse_take(&x, &y);
int64_t sx = (int64_t)*dx + x, sy = (int64_t)*dy + y;
*dx = (int32_t)(sx < INT32_MIN ? INT32_MIN : sx > INT32_MAX ? INT32_MAX : sx);
*dy = (int32_t)(sy < INT32_MIN ? INT32_MIN : sy > INT32_MAX ? INT32_MAX : sy);
}
if (enabled && (*dx || *dy) && rec_state == 1) {
char what[80];
snprintf(what, sizeof what, "MOUSE %d %d", *dx, *dy);
int64_t w = frame_step_world_us();
uint64_t tick = frame_step_ticks();
rec_line(tick, w, tick + 1, w, what);
fflush(rec);
}
plat_mutex_unlock(&lock);
}
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;
}
// ---- walk key: the keyboard's stick samples scaled down while a key is held (TOTK_WALK_KEY, TOTK_WALK_SCALE)
// A keyboard key is all or nothing, so every keyboard-driven stick sample is +/-32767 and the game only ever sees a
// full tilt. Holding this key scales those two samples, which gives the half tilt a gamepad gets from its analog range,
// so the keyboard can walk as well as run. yuzu and eden do the same with their modifier and modifier_scale settings.
// Only the keyboard is scaled: the gamepad's axes and the mouse camera sample their own values.
static int walk_key = KEY_NONE; // TOTK_WALK_KEY as a KEY_*, KEY_NONE = off; live (cfg_version)
static float walk_scale = 0.51f; // TOTK_WALK_SCALE, 0.2 to 0.9; live (cfg_version)
static unsigned walk_seen = ~0u;
// The ini takes the key names the Controls tab writes; a hand-written line may spell them in any case.
static int name_matches(const char *name, const char *key) {
for (; *name && *key; name++, key++) {
unsigned char a = (unsigned char)*name, b = (unsigned char)*key;
if (a >= 'A' && a <= 'Z') a = (unsigned char)(a - 'A' + 'a');
if (b >= 'A' && b <= 'Z') b = (unsigned char)(b - 'A' + 'a');
if (a != b) return 0;
}
return *name == *key;
}
static void walk_get(void) {
unsigned ver = cfg_version();
if (ver == walk_seen) return;
walk_seen = ver;
const char *name = cfg_value("TOTK_WALK_KEY"); // "none" (the default) turns the key off
int key = KEY_NONE;
if (*name && !name_matches(name, "none")) {
for (int k = KEY_A; k < KEY_COUNT && key == KEY_NONE; k++)
if (input_key_name(k) && name_matches(name, input_key_name(k))) key = k;
if (key == KEY_NONE) rt_log("input: walk_key: unknown key '%s', the keyboard sticks keep their full tilt", name);
}
// 0.51 rather than 0.5: a keyboard sample is 32767, and 32767 * 0.5 = 16383 falls one short of stick_bits'
// STICK_DIR (16384), which would leave the game's stick-direction bits clear while the walk key is held. The value
// in the settings table carries that default; this code only rejects what the table's range does not allow.
const char *sv = cfg_value("TOTK_WALK_SCALE");
char *end;
double d = strtod(sv, &end);
double s = end == sv || *end || !(d >= 0.2 && d <= 0.9) ? strtod(cfg_meta("TOTK_WALK_SCALE")->def, NULL) : d;
if (key != walk_key || (float)s != walk_scale) {
walk_key = key;
walk_scale = (float)s;
rt_log("input: walk key %s, keyboard sticks at %.0f%% of their tilt (mouse and gamepad unchanged)",
key != KEY_NONE ? input_key_name(key) : "off", s * 100);
}
}
static int mouse_selector;
int input_direct_mouse_selector_active(void) { return __atomic_load_n(&mouse_selector, __ATOMIC_RELAXED) != 0; }
// Selector 1 is a radial wheel, 2..4 are distinct horizontal quick lists; only a wheel retains a virtual cursor.
// Under `lock`.
static void mouse_camera(int32_t out[2], int selector) {
static uint64_t frame = ~0ull, t0;
static int32_t held[2];
static int was_selector;
static float cursor[2];
static InputMouseRow row;
uint64_t now = plat_now_ns();
int32_t dx, dy;
if (!input_direct_mouse_enabled() || !ikb_captured()) selector = 0;
__atomic_store_n(&mouse_selector, selector, __ATOMIC_RELAXED);
if (selector) {
if (was_selector != selector) cursor[0] = cursor[1] = 0, row = (InputMouseRow){0};
was_selector = selector;
ikb_mouse_take(&dx, &dy);
if (selector == 1) input_direct_mouse_radial(dx, dy, mouse_sens_get(), cursor, out);
else input_direct_mouse_row(dx, mouse_sens_get(), now, &row, out);
held[0] = held[1] = 0; frame = ~0ull;
return;
}
if (was_selector) {
was_selector = 0; cursor[0] = cursor[1] = 0; row = (InputMouseRow){0};
ikb_mouse_take(&dx, &dy); // release the wheel without sending its last movement to the camera
held[0] = held[1] = out[0] = out[1] = 0; frame = ~0ull;
return;
}
if (input_direct_mouse_active()) {
held[0] = held[1] = out[0] = out[1] = 0;
frame = ~0ull;
return; // the camera update takes the raw counts itself, once, outside the HID stick path
}
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
}
}
double k = cam_speed_applied(), x = held[0] / k, y = held[1] / k, len = sqrt(x * x + y * y);
if (len > STICK_MAX) x *= STICK_MAX / len, y *= STICK_MAX / len;
out[0] = (int32_t)lround(x);
out[1] = (int32_t)lround(y);
}
static float stick_dead[2] = {-1, -1}; // input_pad_deadzone of the left and right stick; live (cfg_version)
static unsigned stick_dead_seen;
static float stick_dead_get(int s) {
unsigned ver = cfg_version();
if (stick_dead[0] < 0 || ver != stick_dead_seen) {
stick_dead_seen = ver;
stick_dead[0] = input_pad_deadzone(0);
stick_dead[1] = input_pad_deadzone(1);
}
return stick_dead[s];
}
// 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; }
}
// The shake key (B_SHAKE): the game detaches a part fused to what Ultrahand holds when the right stick swings left and
// right a few times. A press swings it for SHAKE_NS, holding keeps it swinging; each side lasts SHAKE_SIDE_NS, two
// frames at the game's 30 fps, so every side is read even at 20 fps. Returns the stick x, 0 while not shaking.
// Under lock.
#define SHAKE_NS 800000000ull
#define SHAKE_SIDE_NS 66666667ull
static int32_t shake_stick(int held) {
static int was;
static uint64_t from, until;
uint64_t now = plat_now_ns();
if (held && !was && now >= until) from = now;
if (held && !was) until = now + SHAKE_NS;
if (held && until < now) until = now;
was = held;
if (now >= until && !held) return 0;
return (now - from) / SHAKE_SIDE_NS % 2 ? STICK_MAX : -STICK_MAX;
}
// The scroll wheel on the Purah Pad's map, which zooms with the right stick's y (up: in): each notch is a full-tilt
// pulse of MAP_ZOOM_PULSE_NS on that stick with MAP_ZOOM_GAP_NS of neutral after it, so a step of the zoom and a
// held zoom both see one notch as one push. A fast scroll's notches wait their turn (at most MAP_ZOOM_QUEUE), a turn the
// other way drops them. Returns the stick y for this poll, 0 between pulses and off the map. Under lock.
#define MAP_ZOOM_PULSE_NS 100000000ull
#define MAP_ZOOM_GAP_NS 50000000ull
#define MAP_ZOOM_QUEUE 4
static int32_t map_zoom_stick(int32_t notches, int on_map) {
static int32_t queued, dir;
static uint64_t until; // the end of the pulse (dir != 0) or of the neutral after it
uint64_t now = plat_now_ns();
if (!on_map) { queued = dir = 0, until = 0; return 0; }
if (notches) {
if (queued && (notches > 0) != (queued > 0)) queued = 0;
queued += notches;
queued = queued > MAP_ZOOM_QUEUE ? MAP_ZOOM_QUEUE : queued < -MAP_ZOOM_QUEUE ? -MAP_ZOOM_QUEUE : queued;
}
if (dir) {
if (now < until) return dir * STICK_MAX;
dir = 0, until = now + MAP_ZOOM_GAP_NS;
return 0;
}
if (now < until || !queued) return 0;
dir = queued > 0 ? 1 : -1;
queued -= dir;
until = now + MAP_ZOOM_PULSE_NS;
return dir * STICK_MAX;
}
// 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());
stick(SDL_GAMEPAD_AXIS_LEFTX, SDL_GAMEPAD_AXIS_LEFTY, stick_dead_get(0), sl);
stick(SDL_GAMEPAD_AXIS_RIGHTX, SDL_GAMEPAD_AXIS_RIGHTY, stick_dead_get(1), sr);
return 1;
}
int input_pad_stick_percent(double pct[2]) {
int ok = 0;
pct[0] = pct[1] = 0;
plat_mutex_lock(&lock);
if (state == 1 && pad && SDL_GamepadConnected(pad)) { // the game's poll keeps the state fresh (SDL_UpdateGamepads)
pct[0] = input_stick_percent(SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTX),
SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTY));
pct[1] = input_stick_percent(SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_RIGHTX),
SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_RIGHTY));
ok = 1;
}
plat_mutex_unlock(&lock);
return ok;
}
static uint64_t g_buttons_last; // the merged buttons of the last poll (input_buttons_last)
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);
int pad_used = b || abs(sl[0]) > PAD_USED || abs(sl[1]) > PAD_USED || abs(sr[0]) > PAD_USED ||
abs(sr[1]) > PAD_USED, kbm_used = 0;
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. While a game menu has the pointer
// (menu_cursor.c) and it is over the window, the left and right buttons are that menu's A and B, and the
// middle and side buttons keep their bindings: bound to L and R they turn the menu's tabs.
static int was_captured;
static uint8_t mouse_ignore[KEY_COUNT];
int cap = ikb_captured();
float pointer_x, pointer_y;
int menu_pointer = !cap && menu_cursor_pointer() && ikb_pointer(&pointer_x, &pointer_y);
for (int k = 1; k < KEY_COUNT; k++) {
down[k] = ikb_down(k);
kbm_used |= 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;
int counts = cap || (menu_pointer && k != KEY_MOUSE_L && k != KEY_MOUSE_R);
if (!counts || mouse_ignore[k]) down[k] = 0;
}
was_captured = cap;
b |= input_map_buttons(down);
int32_t k[2];
walk_get();
int walk = walk_key != KEY_NONE && down[walk_key];
input_map_stick(down, 0, k);
if (walk) k[0] = (int32_t)(k[0] * walk_scale), k[1] = (int32_t)(k[1] * walk_scale);
pick(&sl[0], &sl[1], k[0], k[1]);
input_map_stick(down, 1, k);
if (walk) k[0] = (int32_t)(k[0] * walk_scale), k[1] = (int32_t)(k[1] * walk_scale);
pick(&sr[0], &sr[1], k[0], k[1]);
int selector = b & (1ull << B_L) ? 1 : b & (1ull << B_UP) ? 2 :
b & (1ull << B_LEFT) ? 3 : b & (1ull << B_RIGHT) ? 4 : 0;
// Only while the wheel or a strip is on screen (menu_cursor.c reads the focused screen): L also soothes the
// horse and turns menu tabs, the D-pad does other things too, and then the mouse stays on the camera.
// Before the screens were read (the menu cursor off) the buttons alone decide, as before.
if (selector && menu_cursor_shortcut() == 0) selector = 0;
mouse_camera(k, selector);
// On the map the left stick moves the cursor and the right one zooms: the mouse moves the cursor there,
// the scroll wheel zooms (map_zoom_stick). Elsewhere the wheel is unbound; its notches are dropped.
int map = menu_cursor_map();
if (map) pick(&sl[0], &sl[1], k[0], k[1]);
else pick(&sr[0], &sr[1], k[0], k[1]);
int32_t zoom = map_zoom_stick(ikb_wheel_take(), map);
if (zoom) pick(&sr[0], &sr[1], 0, zoom);
int32_t shake = shake_stick(input_map_held(down, B_SHAKE));
if (shake) sr[0] = shake, sr[1] = 0;
// A mouse shake (mouse_direct.c) moves the stick with the mouse, side by side, and lets go when it rests
else if (input_direct_mouse_shake()) sr[0] = input_direct_mouse_shake() * STICK_MAX, sr[1] = 0;
static uint32_t travel_seen;
uint32_t travel = ikb_mouse_travel();
if (travel - travel_seen >= MOUSE_USED) kbm_used = 1;
travel_seen = travel;
}
// A press or a full-way stick on the pad wins over keys held in the same poll: the player picked it up
if (pad_used) __atomic_store_n(&g_kbm_last, 0, __ATOMIC_RELAXED);
else if (kbm_used) __atomic_store_n(&g_kbm_last, 1, __ATOMIC_RELAXED);
}
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);
__atomic_store_n(&g_buttons_last, b, __ATOMIC_RELAXED);
*buttons = b; *lx = sl[0]; *ly = sl[1]; *rx = sr[0]; *ry = sr[1];
}
uint64_t input_buttons_last(void) { return __atomic_load_n(&g_buttons_last, __ATOMIC_RELAXED); }
// 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.
// When g_block and g_menu last went to 2 (plat_now_ns). The wait for the release ends after RELEASE_WAIT_NS even with
// something held: an input that never reads as released (a button another program holds down, a key whose release
// went to another window, a stick that rests outside its dead zone) would otherwise keep the game deaf until restart
// (#462, #494). A press that closed the menu is let go well within the wait; one still held after it is meant.
static volatile uint64_t g_block_closed, g_menu_closed;
#define RELEASE_WAIT_NS 1000000000ull
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")) && !menu_cursor_pointer();
}
void input_block(int on) {
if (!on) __atomic_store_n(&g_block_closed, plat_now_ns(), __ATOMIC_RELEASE);
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) {
if (!open) __atomic_store_n(&g_menu_closed, plat_now_ns(), __ATOMIC_RELEASE);
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, or with
// something still held RELEASE_WAIT_NS after closed. A stick axis counts as held as well: with the walk key the
// keyboard's sticks move part way without setting a direction bit, so a gate that watched the buttons alone would let
// that movement through the moment the menu or the text entry closes.
static int blocked(volatile int32_t *flag, volatile uint64_t *closed, const char *what, uint64_t buttons,
const int32_t *axes) {
int32_t k = *flag;
if (k != 2) return k != 0;
int held = buttons != 0;
for (int i = 0; i < 4; i++) held |= axes[i] != 0;
if (held && plat_now_ns() - __atomic_load_n(closed, __ATOMIC_ACQUIRE) < RELEASE_WAIT_NS) return 1;
if (plat_cas32(flag, 0, 2) != 2) return *flag != 0; // reopened meanwhile
if (held)
rt_log("input: the %s closed 1 s ago and input is still held (buttons %#llx, sticks %d %d %d %d); the game "
"gets it from now on", what, (unsigned long long)buttons, axes[0], axes[1], axes[2], axes[3]);
return 0;
}
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);
if (!g_block && !g_menu) menu_cursor_poll(buttons);
const int32_t axes[4] = {*lx, *ly, *rx, *ry};
int text = blocked(&g_block, &g_block_closed, "text entry", *buttons, axes),
menu = blocked(&g_menu, &g_menu_closed, "menu", *buttons, axes);
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);
}