Files
NX2navi/runtime/input.c
chrg6 be8c2620e5 Run the gamepad update on a thread of its own, so a controller that comes or goes no longer stalls the game.
When a device came or went, SDL's gamepad update first found every device again, 100 ms and more, on the game's
polling thread with the input lock held: a controller disconnecting gave five frames of 120 to 240 ms in a row, with
the game's other pollers queued on the lock. The pad thread now makes SDL's initialisation and every update. A poll
asks for an update and waits at most 2 ms for it; an ordinary one is done in microseconds and the poll reads fresh
state as before, and during a device search the poll goes on with the state it read last. Rumble, the motion samples
and the menu's stick reading skip a beat then instead of waiting.
--probe-input checks it on a virtual gamepad, with an update made to take 300 ms.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-09 23:51:58 -05:00

1261 lines
61 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 HidNpadButton. The key and pad
// bindings are the tables in input_map.c (nx2navi.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"
#ifdef _WIN32
#include "input_ds3.h"
#endif
#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 NX2NAVI_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 (NX2NAVI_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,
};
// NX2NAVI_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("NX2NAVI_INPUT_SCRIPT");
if (!path || !*path) return;
FILE *f = fopen(path, "r");
if (!f) { rt_log("input: cannot open NX2NAVI_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;
}
}
// NX2NAVI_INPUT_RECORD=<file>: writes the merged input as NX2NAVI_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("NX2NAVI_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 NX2NAVI_INPUT_RECORD %s", path);
if (rec) { fprintf(rec, "# Recorded input (NX2NAVI_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; // NX2NAVI_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("NX2NAVI_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 (NX2NAVI_WALK_KEY, NX2NAVI_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.
// Only the keyboard is scaled: the gamepad's axes and the mouse camera sample their own values.
static int walk_key = KEY_NONE; // NX2NAVI_WALK_KEY as a KEY_*, KEY_NONE = off; live (cfg_version)
static float walk_scale = 0.51f; // NX2NAVI_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("NX2NAVI_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("NX2NAVI_WALK_SCALE");
char *end;
double d = strtod(sv, &end);
double s = end == sv || *end || !(d >= 0.2 && d <= 0.9) ? strtod(cfg_meta("NX2NAVI_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.
// ---- SDL's gamepad update on a thread of its own
// SDL_UpdateGamepads reads the controllers, and when a device came or went it first finds every device again, 100 ms
// and more inside the system's input libraries. It ran on the game's polling thread with the input lock held: a
// controller that disconnected (a wireless one going to sleep, a cable pulled) gave a burst of 120 to 240 ms frames,
// one per device notification, with the game's other pollers queued on the lock.
// The pad thread (up_main) makes SDL's initialisation and every update. A poll asks for an update and waits for it,
// UP_WAIT_NS at most (sdl_enter): an ordinary update is done in microseconds and the poll reads fresh state, as before;
// one that runs into the device search is not, and the poll goes on with what it read last. sdl_lock is held around
// every SDL gamepad call: by the pad thread for an update, by everyone else through sdl_enter or a try
// (plat_mutex_trylock), never by waiting, so no thread of the game stands behind the device search.
#define UP_WAIT_NS 2000000ull // how long a poll waits for its update
#define UP_FRESH_NS 500000ull // polls this soon after an update share it (the game reads several pads in a row)
static PlatMutex sdl_lock = PLAT_MUTEX_INIT;
static PlatMutex up_lock = PLAT_MUTEX_INIT; // guards the fields below
static PlatCond up_go = PLAT_COND_INIT, up_done = PLAT_COND_INIT;
static int up_started, up_inited, up_init_ok, up_want, up_busy;
static uint64_t up_gen, up_at, up_t0; // updates finished; when the last one finished; when the running one began
static uint64_t up_stale; // polls that went on without an update
static volatile uint64_t up_test_ns; // --probe-input: the next update takes this long, as a device search does
static void up_main(void *arg) {
(void)arg;
// SDL drops gamepad presses while it thinks the program is in the background, and it judges that from windows of
// its own: the Win32 window (gfx_window_win.c) is not one, and keyboard focus is gated here (ikb_foreground)
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
int ok = SDL_Init(SDL_INIT_GAMEPAD);
if (!ok) rt_log("input: SDL gamepad init failed (%s), keyboard only", SDL_GetError());
plat_mutex_lock(&up_lock);
up_inited = 1, up_init_ok = ok;
plat_cond_broadcast(&up_done);
while (ok) {
while (!up_want) plat_cond_wait(&up_go, &up_lock);
up_want = 0, up_busy = 1, up_t0 = plat_now_ns();
plat_mutex_unlock(&up_lock);
plat_mutex_lock(&sdl_lock);
uint64_t slow = __atomic_exchange_n(&up_test_ns, 0, __ATOMIC_RELAXED);
if (slow) plat_wait_ns(slow);
SDL_UpdateGamepads();
#ifdef _WIN32
// The Windows window is our own (gfx_window_win.c), so nothing reads SDL's event queue, yet every axis and
// button change of a gamepad is queued there: at SDL's cap (about 65,000 events, several MB) each further one
// fails and sets SDL's error text. The state the getters read does not come from the queue.
SDL_FlushEvents(SDL_EVENT_FIRST, SDL_EVENT_LAST);
#endif
plat_mutex_unlock(&sdl_lock);
plat_mutex_lock(&up_lock);
uint64_t took = plat_now_ns() - up_t0;
up_busy = 0, up_gen++, up_at = plat_now_ns();
plat_cond_broadcast(&up_done);
static int logged;
if (took >= 20000000ull && logged++ < 16)
rt_log("input: the gamepad update took %.0f ms (devices found again after one came or went); %llu polls so far "
"went on with their last state instead of waiting", took / 1e6, (unsigned long long)up_stale);
}
plat_mutex_unlock(&up_lock);
}
// Under lock: starts the pad thread and waits for SDL's initialisation on it. 1 when SDL's gamepads can be used.
static int sdl_start(void) {
plat_mutex_lock(&up_lock);
if (!up_started) {
up_started = 1;
if (!plat_thread_spawn(up_main, NULL, "pads", 0)) rt_fatal("input: cannot start the gamepad thread");
}
while (!up_inited) plat_cond_wait(&up_done, &up_lock);
int ok = up_init_ok;
plat_mutex_unlock(&up_lock);
return ok;
}
// Under lock: brings SDL's gamepad state up to date and takes sdl_lock for the caller's SDL calls. 1 with it held
// (sdl_leave gives it back); 0 when the update is not done within UP_WAIT_NS, and then nothing of SDL may be called.
static int sdl_enter(void) {
plat_mutex_lock(&up_lock);
uint64_t now = plat_now_ns();
int ok = 1;
if (up_busy && now - up_t0 >= UP_WAIT_NS) {
ok = 0; // the device search: the first poll waited for it, the ones after it do not
} else if (up_busy || now - up_at >= UP_FRESH_NS) {
uint64_t next = up_gen + 1;
if (!up_busy) up_want = 1, plat_cond_signal(&up_go);
while (up_gen < next) {
uint64_t el = plat_now_ns() - now;
if (el >= UP_WAIT_NS) break;
plat_cond_wait_ns(&up_done, &up_lock, UP_WAIT_NS - el);
}
ok = up_gen >= next;
}
if (!ok) up_stale++;
plat_mutex_unlock(&up_lock);
if (ok && !plat_mutex_trylock(&sdl_lock)) ok = 0; // the next update began already, for another poller
return ok;
}
static void sdl_leave(void) { plat_mutex_unlock(&sdl_lock); }
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);
}
// Every connected gamepad is kept open; `pad` is the one the game reads (and the one that rumbles and feeds motion):
// the first one found, then whichever one the player presses (pad_follow). Taking only the first gamepad SDL lists
// left the game deaf when that one was a device nobody holds: a virtual pad of Steam Input or DS4Windows, a wheel, a
// receiver's idle second slot, listed before the controller in the player's hands.
enum { MAX_PADS = 8 };
static SDL_Gamepad *pads[MAX_PADS];
static int npads;
static int pad_pressed_logged; // the primary's first press was logged (once per primary)
// Under lock: makes p the gamepad the game reads, with its labels, rumble and layout; logs it.
static void pad_primary(SDL_Gamepad *p, const char *why) {
pad = p;
sensor_pad = 0, sensor_ok = 0, pad_pressed_logged = 0;
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, %s, east button %s)", SDL_GetGamepadName(pad), why,
pad_rumble ? "rumble" : "no rumble", motion ? "gyro and accelerometer" : "no motion sensors",
pad_east_a ? "labelled A" : "not labelled A");
}
// Under lock: closes the gamepads that went away and opens the ones that came; the primary falls back to the first
// open one when it left. Logs each change.
static void scan_pads(void) {
for (int i = 0; i < npads;) {
if (SDL_GamepadConnected(pads[i])) { i++; continue; }
rt_log("input: gamepad \"%s\" disconnected", SDL_GetGamepadName(pads[i]) ? SDL_GetGamepadName(pads[i]) : "?");
if (pads[i] == pad) {
pad = NULL, sensor_pad = 0, sensor_ok = 0;
plat_xchg32(&pad_layout_shown, -1);
for (int k = 0; k < 4; k++) plat_xchg32(&pad_labels[k], PAD_LABEL_NONE);
}
SDL_CloseGamepad(pads[i]);
pads[i] = pads[--npads];
}
int n = 0;
SDL_JoystickID *ids = SDL_GetGamepads(&n);
for (int i = 0; ids && i < n && npads < MAX_PADS; i++) {
int open = 0;
for (int k = 0; k < npads && !open; k++) open = SDL_GetGamepadID(pads[k]) == ids[i];
if (open) continue;
SDL_Gamepad *p = SDL_OpenGamepad(ids[i]);
if (!p) { rt_log("input: gamepad %u could not be opened (%s)", (unsigned)ids[i], SDL_GetError()); continue; }
pads[npads++] = p;
if (pad) rt_log("input: gamepad \"%s\" connected (the game switches to it at its first press)", SDL_GetGamepadName(p));
}
SDL_free(ids);
if (!pad && npads) pad_primary(pads[0], "in use");
}
// Under lock: 1 when p has a button down, a trigger pulled or a stick past half way (a drifting stick does not count).
static int pad_active(SDL_Gamepad *p) {
for (int b = 0; b < SDL_GAMEPAD_BUTTON_COUNT; b++)
if (SDL_GetGamepadButton(p, (SDL_GamepadButton)b)) return 1;
for (int a = 0; a < SDL_GAMEPAD_AXIS_COUNT; a++)
if (abs(SDL_GetGamepadAxis(p, (SDL_GamepadAxis)a)) > PAD_USED) return 1;
return 0;
}
// Under lock: when the primary is idle and another open gamepad is pressed, that one becomes the primary.
static void pad_follow(void) {
if (npads < 2 || (pad && pad_active(pad))) return;
for (int i = 0; i < npads; i++)
if (pads[i] != pad && pad_active(pads[i])) { pad_primary(pads[i], "in use (pressed)"); return; }
}
// 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("NX2NAVI_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 NX2NAVI_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, "NX2NAVI_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();
}
// NX2NAVI_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. NX2NAVI_TEST_PAD_AT=<ms>: attached that long after
// input starts instead, like a controller switched on in play, and its south button pressed for half a second one
// second later (the log then shows the hot-plug and the press arriving: scan_pads, pad_physical).
static SDL_Joystick *test_joy; // kept open: a virtual joystick's axes and buttons are set through an open handle
static uint64_t test_attach_ns, test_press_ns; // NX2NAVI_TEST_PAD_AT: when to attach, when to press (0: done)
static void test_pad_make(void) {
const char *e = cfg_get("NX2NAVI_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: NX2NAVI_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 = "NX2navi test gamepad";
SDL_JoystickID id = SDL_AttachVirtualJoystick(&d);
test_joy = id ? SDL_OpenJoystick(id) : NULL;
if (!test_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(test_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 test_pad_attach(void) {
const char *at = cfg_get("NX2NAVI_TEST_PAD_AT");
if (at && *at && cfg_get("NX2NAVI_TEST_PAD")) {
test_attach_ns = plat_now_ns() + strtoull(at, NULL, 10) * 1000000ull;
return;
}
test_pad_make();
}
// Under lock, every poll: the delayed attach and the press of NX2NAVI_TEST_PAD_AT.
static void test_pad_tick(void) {
uint64_t now = plat_now_ns();
if (test_attach_ns && now >= test_attach_ns) {
test_attach_ns = 0;
test_pad_make();
if (test_joy) test_press_ns = now + 1000000000ull;
}
if (test_press_ns && test_joy && now >= test_press_ns) {
int down = now < test_press_ns + 500000000ull;
SDL_SetJoystickVirtualButton(test_joy, SDL_GAMEPAD_BUTTON_SOUTH, down);
if (!down) test_press_ns = 0;
}
}
static void start(void) {
binds_refresh();
const char *e = cfg_get("NX2NAVI_INPUT");
if (e && !strcmp(e, "none")) { state = -1; rt_log("input: disabled (NX2NAVI_INPUT=none)"); return; }
state = sdl_start() ? 1 : -1; // SDL's initialisation, on the pad thread (up_main)
if (state == 1) {
plat_mutex_lock(&sdl_lock); // the pad thread updates only when asked, and nobody has asked yet
test_pad_attach();
scan_pads();
plat_mutex_unlock(&sdl_lock);
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 the held object 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;
}
// Under lock and inside sdl_enter (the gamepads' state is up to date), 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]) {
test_pad_tick();
// every RESCAN_MS, not only without a gamepad: a second controller, or one that wakes up after the start, is found
if (plat_now_ns() / 1000000u >= next_scan) { next_scan = plat_now_ns() / 1000000u + RESCAN_MS; scan_pads(); }
pad_follow();
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
if (!pad_pressed_logged) { // once per primary: the log then shows that its presses arrive
for (int p = PAD_SOUTH; p < PAD_COUNT && !pad_pressed_logged; p++)
if (pd[p]) pad_pressed_logged = 1, rt_log("input: gamepad \"%s\": first press (%s)", SDL_GetGamepadName(pad), input_pad_name(p));
}
return 1;
}
// Under lock, state 1: the open gamepad's buttons (Switch bits after NX2NAVI_PAD_LAYOUT and the NX2NAVI_BIND_* lines) and
// sticks; 0 without a connected gamepad.
#ifdef _WIN32
// Without a gamepad SDL can open, Xbox controllers are read through XInput directly (xinput1_4, else xinput9_1_0):
// SDL may miss a pad another program or driver presents only as XInput. Slots 0-3, the first connected one; a slot
// found empty is asked again once a second, as XInputGetState on an empty slot takes milliseconds.
typedef struct { uint16_t buttons; uint8_t lt, rt; int16_t lx, ly, rx, ry; } XiPad;
typedef struct { uint32_t packet; XiPad pad; } XiState;
typedef uint32_t(__stdcall *XiGetState)(uint32_t, XiState *);
__declspec(dllimport) void *__stdcall LoadLibraryA(const char *);
__declspec(dllimport) void *__stdcall GetProcAddress(void *, const char *);
// Under lock: the first connected XInput pad's inputs by position (input_map.h PAD_*) and sticks (y up); 0 without one.
static int xinput_read(uint8_t pd[PAD_COUNT], int32_t *raw) {
static XiGetState get;
static int tried, slot = -1;
static uint64_t next_try;
if (!tried) {
tried = 1;
void *m = LoadLibraryA("xinput1_4.dll");
if (!m) m = LoadLibraryA("xinput9_1_0.dll");
if (m) get = (XiGetState)GetProcAddress(m, "XInputGetState");
}
if (!get) return 0;
XiState st;
if (slot >= 0 && get((uint32_t)slot, &st) != 0) {
rt_log("input: XInput controller %d disconnected", slot);
slot = -1;
}
if (slot < 0) {
uint64_t now = plat_now_ns();
if (now < next_try) return 0;
next_try = now + 1000000000ull;
for (int i = 0; i < 4 && slot < 0; i++)
if (get((uint32_t)i, &st) == 0) slot = i, rt_log("input: XInput controller %d (SDL has no gamepad)", i);
if (slot < 0) return 0;
}
uint16_t w = st.pad.buttons;
static const struct { uint16_t bit; uint8_t pad; } map[] = {
{0x1000, PAD_SOUTH}, {0x2000, PAD_EAST}, {0x4000, PAD_WEST}, {0x8000, PAD_NORTH}, {0x0040, PAD_LSTICK},
{0x0080, PAD_RSTICK}, {0x0100, PAD_L}, {0x0200, PAD_R}, {0x0010, PAD_START}, {0x0020, PAD_BACK},
{0x0004, PAD_DPAD_LEFT}, {0x0001, PAD_DPAD_UP}, {0x0008, PAD_DPAD_RIGHT}, {0x0002, PAD_DPAD_DOWN}};
for (size_t i = 0; i < sizeof map / sizeof *map; i++) pd[map[i].pad] = (w & map[i].bit) != 0;
pd[PAD_ZL] = st.pad.lt > 128, pd[PAD_ZR] = st.pad.rt > 128;
raw[0] = st.pad.lx, raw[1] = st.pad.ly, raw[2] = st.pad.rx, raw[3] = st.pad.ry;
return 1;
}
#endif
#ifdef _WIN32
// 1 when the DualShock 3 reader (input_ds3_win.c) has input beyond rest: a button down or a stick past half way.
static int ds3_pressed(const uint8_t pd[PAD_COUNT], const int32_t raw[4]) {
for (int p = PAD_SOUTH; p < PAD_COUNT; p++)
if (pd[p]) return 1;
for (int i = 0; i < 4; i++)
if (abs(raw[i]) > PAD_USED) return 1;
return 0;
}
// Under lock: the DualShock 3 reader's inputs when it is the controller the game follows, else 0. Next to an SDL
// gamepad the one pressed last is followed, as between two SDL gamepads (pad_follow); the same DualShock 3 that a driver
// also presents to SDL is two sources of one device, and either one gives the same presses.
static int ds3_primary(uint8_t pd[PAD_COUNT], int32_t raw[4], int sdl_pad) {
if (!ds3_read(pd, raw)) return 0;
if (!sdl_pad) return 1;
static int follow = 1;
if (ds3_pressed(pd, raw)) follow = 1;
else if (pad_active(pad)) follow = 0;
return follow;
}
#endif
static int pad_read_now(uint64_t *b, int32_t sl[2], int32_t sr[2]);
// Under lock, state 1: the gamepad's buttons (ORed into *b) and sticks; 0 without one. While SDL is finding devices
// again (sdl_enter) the answer is the last one read: what was held stays held for those moments.
static int pad_read(uint64_t *b, int32_t sl[2], int32_t sr[2]) {
static uint64_t last_b;
static int32_t last_sl[2], last_sr[2];
static int last_ok;
if (sdl_enter()) {
uint64_t pb = 0;
int32_t l[2] = {0, 0}, r[2] = {0, 0};
last_ok = pad_read_now(&pb, l, r);
sdl_leave();
last_b = pb, last_sl[0] = l[0], last_sl[1] = l[1], last_sr[0] = r[0], last_sr[1] = r[1];
}
if (last_ok) *b |= last_b, sl[0] = last_sl[0], sl[1] = last_sl[1], sr[0] = last_sr[0], sr[1] = last_sr[1];
return last_ok;
}
static int pad_read_now(uint64_t *b, int32_t sl[2], int32_t sr[2]) {
uint8_t pd[PAD_COUNT] = {0};
#ifdef _WIN32
int sdl = pad_physical(pd);
uint8_t dd[PAD_COUNT] = {0};
int32_t raw[4];
if (ds3_primary(dd, raw, sdl)) {
*b |= input_map_pad_buttons(dd, pad_layout_get());
input_pad_stick(raw[0], raw[1], stick_dead_get(0), sl);
input_pad_stick(raw[2], raw[3], stick_dead_get(1), sr);
return 1;
}
if (!sdl) {
if (!xinput_read(pd, raw)) return 0;
*b |= input_map_pad_buttons(pd, pad_layout_get());
input_pad_stick(raw[0], raw[1], stick_dead_get(0), sl);
input_pad_stick(raw[2], raw[3], stick_dead_get(1), sr);
return 1;
}
#else
if (!pad_physical(pd)) return 0;
#endif
*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);
int in = state == 1 && plat_mutex_trylock(&sdl_lock); // not while SDL updates: this reading is skipped then
int sdl_pad = in && pad && SDL_GamepadConnected(pad);
#ifdef _WIN32
uint8_t dd[PAD_COUNT];
int32_t raw[4];
if (state == 1 && ds3_primary(dd, raw, sdl_pad)) {
pct[0] = input_stick_percent(raw[0], raw[1]);
pct[1] = input_stick_percent(raw[2], raw[3]);
ok = 1;
} else
#endif
if (sdl_pad) { // the game's poll keeps the state fresh (sdl_enter)
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;
}
if (in) sdl_leave();
plat_mutex_unlock(&lock);
return ok;
}
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.
static int was_captured;
static uint8_t mouse_ignore[KEY_COUNT];
int cap = ikb_captured();
// A scan is one key state system call per key (about 95). The game reads several pads in a row: reads
// within a millisecond of a scan get its answers again.
static uint8_t raw[KEY_COUNT];
static uint64_t raw_at;
uint64_t now = plat_now_ns();
if (!raw_at || now - raw_at >= 1000000) {
for (int k = 1; k < KEY_COUNT; k++) raw[k] = (uint8_t)(ikb_down(k) != 0);
raw_at = now;
}
for (int k = 1; k < KEY_COUNT; k++) {
down[k] = raw[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;
if (!cap || 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;
mouse_camera(k, selector);
pick(&sr[0], &sr[1], k[0], k[1]);
int32_t shake = shake_stick(input_map_held(down, B_SHAKE));
if (shake) sr[0] = shake, 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);
*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.
// 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("NX2NAVI_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 && sdl_enter()) pad_physical(p), sdl_leave();
#ifdef _WIN32
if (state == 1) { // a DualShock 3 the reader has: its presses can be bound too
uint8_t dd[PAD_COUNT];
int32_t raw[4];
if (ds3_read(dd, raw))
for (int i = 1; i < PAD_COUNT; i++) p[i] |= dd[i];
}
#endif
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("NX2NAVI_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 && plat_mutex_trylock(&sdl_lock)) { // one skipped while SDL updates
ok = SDL_RumbleGamepad(pad, low, high, ms);
if (triggers) SDL_RumbleGamepadTriggers(pad, high, high, ms); // unsupported pads return false: ignored
sdl_leave();
}
#ifdef _WIN32
if (state == 1 && ds3_rumble(low, high, ms)) ok = 1; // no-op unless the DualShock 3 reader has a controller
#endif
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): 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("NX2NAVI_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 (NX2NAVI_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_enter()) { // no sample while SDL finds devices again
have = SDL_GamepadConnected(pad) && sensors_ready() &&
SDL_GetGamepadSensorData(pad, SDL_SENSOR_ACCEL, a, 3) && SDL_GetGamepadSensorData(pad, SDL_SENSOR_GYRO, g, 3);
sdl_leave();
}
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 (NX2NAVI_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);
}
// --probe-input: the gamepad path on SDL's virtual test gamepad (NX2NAVI_TEST_PAD). The pad thread comes up, a poll
// reads the gamepad, polls are quick, and while an update takes as long as a device search (up_test_ns) the polls go on
// with their last state instead of waiting for it. Needs no game files and no controller.
int probe_input(void) {
int fails = 0;
cfg_setenv("NX2NAVI_TEST_PAD", "20000,-12000,0,0");
uint64_t b = 0;
int32_t lx = 0, ly = 0;
uint64_t t0 = plat_now_ns();
int ok = input_pad_read(&b, &lx, &ly);
printf("input: first poll %.1f ms (SDL comes up on the pad thread), gamepad %s\n", (plat_now_ns() - t0) / 1e6,
ok ? "read" : "not read");
if (!ok) fails++, printf("FAIL: no gamepad was read, with the test gamepad attached\n");
plat_mutex_lock(&lock);
const char *nm = pad && plat_mutex_trylock(&sdl_lock) ? SDL_GetGamepadName(pad) : NULL;
int test = nm && strstr(nm, "test gamepad");
if (nm) sdl_leave();
plat_mutex_unlock(&lock);
if (test && (lx < 8000 || ly == 0)) fails++, printf("FAIL: the test gamepad's left stick read %d,%d\n", lx, ly);
if (!test) printf("input: another gamepad is the primary one; the stick values are not checked\n");
// ordinary polls: each asks the pad thread for an update unless one is fresh
uint64_t worst = 0, sum = 0;
enum { N = 400 };
for (int i = 0; i < N; i++) {
plat_wait_ns(700000); // past UP_FRESH_NS: every poll asks
uint64_t p0 = plat_now_ns();
int32_t x, y;
ok = input_pad_read(&b, &x, &y);
uint64_t d = plat_now_ns() - p0;
sum += d;
if (d > worst) worst = d;
if (!ok || (test && x != lx)) { fails++, printf("FAIL: poll %d read %d (stick %d, was %d)\n", i, ok, x, lx); break; }
}
printf("input: %d polls, each with its own update: %.0f us on average, %.0f us the longest\n", N, sum / 1e3 / N, worst / 1e3);
if (worst > UP_WAIT_NS + 3000000) fails++, printf("FAIL: a poll took %.1f ms\n", worst / 1e6);
uint64_t stale0 = up_stale;
// an update that takes 300 ms: the first poll waits UP_WAIT_NS for it, the others not at all, and all get the stick
__atomic_store_n(&up_test_ns, 300000000ull, __ATOMIC_RELAXED);
plat_wait_ns(700000);
uint64_t s0 = plat_now_ns(), first = 0, rest = 0;
int n = 0;
while (plat_now_ns() - s0 < 250000000ull) {
uint64_t p0 = plat_now_ns();
int32_t x, y;
ok = input_pad_read(&b, &x, &y);
uint64_t d = plat_now_ns() - p0;
if (!n) first = d;
else if (d > rest) rest = d;
n++;
if (!ok || (test && x != lx)) { fails++, printf("FAIL: a poll during the long update read %d (stick %d, was %d)\n", ok, x, lx); break; }
plat_wait_ns(1000000);
}
printf("input: during a 300 ms update: %d polls, the first %.1f ms, the longest after it %.0f us, %llu went on with "
"the last state\n", n, first / 1e6, rest / 1e3, (unsigned long long)(up_stale - stale0));
if (first > UP_WAIT_NS + 3000000 || rest > 1500000 || up_stale - stale0 < (uint64_t)n - 1)
fails++, printf("FAIL: polls waited for the long update\n");
plat_wait_ns(120000000); // the long update ends: polls are fresh again
uint64_t g0 = up_gen;
ok = input_pad_read(&b, &lx, &ly);
if (!ok || up_gen == g0) fails++, printf("FAIL: no update after the long one\n");
double pct[2];
input_pad_stick_percent(pct);
input_rumble(1000, 1000, 10);
printf("probe-input: %s\n", fails ? "FAILED" : "passed");
return fails != 0;
}