diff --git a/docs/building.md b/docs/building.md index a7a3bb74..c4c73f5d 100644 --- a/docs/building.md +++ b/docs/building.md @@ -452,6 +452,9 @@ A change to the mouse camera's stick deflection (`runtime/input_map.c` `input_mo deflection at each camera speed: a slow movement is divided by the factor, a fast flick reaches full tilt. No game files. +A change to how a gamepad's axes are read (`runtime/input.c` `pad_axis`: an axis counts once it has been seen at rest) +passes `python tests\pad_rest_check.py`, which builds that function with stand-ins for SDL. No game files. + A change to which render passes count as drawn frame after frame (`runtime/vk_target_frames.h`: a draw into one is skipped while its pipeline builds, a draw into any other waits) passes `python tests\target_frames_check.py`, which builds the header alone and replays an area change after a loading screen from a player's log: the scene, probe and diff --git a/runtime/input.c b/runtime/input.c index bda39037..e1547b95 100644 --- a/runtime/input.c +++ b/runtime/input.c @@ -498,16 +498,13 @@ static int sdl_enter(void) { 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 uint8_t pad_rest[MAX_PADS]; // per open gamepad, bit a: axis a has been seen at rest (pad_axis) static int npads; static int pad_pressed_logged; // the primary's first press was logged (once per primary) @@ -552,7 +549,8 @@ static void scan_pads(void) { for (int k = 0; k < 4; k++) plat_xchg32(&pad_labels[k], PAD_LABEL_NONE); } SDL_CloseGamepad(pads[i]); - pads[i] = pads[--npads]; + --npads; + pads[i] = pads[npads], pad_rest[i] = pad_rest[npads]; } int n = 0; SDL_JoystickID *ids = SDL_GetGamepads(&n); @@ -562,6 +560,7 @@ static void scan_pads(void) { 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; } + pad_rest[npads] = 0; pads[npads++] = p; if (pad) rt_log("input: gamepad \"%s\" connected (the game switches to it at its first press)", SDL_GetGamepadName(p)); } @@ -573,12 +572,35 @@ static void scan_pads(void) { } } -// Under lock: 1 when p has a button down, a trigger pulled or a stick past half way (a drifting stick does not count). +// Under lock: axis a of open gamepad p, or 0 until the axis has been seen at rest (within a quarter of its range of the +// middle; a trigger's rest is 0). A device that reports an axis held from the start (a virtual pad nobody holds, a +// trigger or pedal mapped onto a stick, a stick that rests far off its middle) would otherwise turn the camera on its +// own, and, counted as pressed, take the gamepad role from the controller in the player's hands for good (pad_follow). +// A stick the player holds at the start counts once it has been let go. +enum { AXIS_REST = 8192 }; +static int16_t pad_axis(SDL_Gamepad *p, SDL_GamepadAxis a) { + int16_t v = SDL_GetGamepadAxis(p, a); + for (int i = 0; i < npads; i++) { + if (pads[i] != p) continue; + if (pad_rest[i] >> a & 1) return v; + if (abs(v) >= AXIS_REST) return 0; + pad_rest[i] |= (uint8_t)(1u << a); + return v; + } + return v; +} + +static void stick(SDL_GamepadAxis ax, SDL_GamepadAxis ay, float dead, int32_t out[2]) { + input_pad_stick(pad_axis(pad, ax), -(int32_t)pad_axis(pad, ay), dead, out); +} + +// Under lock: 1 when p has a button down, a trigger pulled or a stick past half way (a drifting stick does not count, +// nor an axis not yet seen at rest). 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; + if (abs(pad_axis(p, (SDL_GamepadAxis)a)) > PAD_USED) return 1; return 0; } @@ -738,8 +760,8 @@ static int pad_physical(uint8_t pd[PAD_COUNT]) { 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; + pd[PAD_ZL] = pad_axis(pad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > TRIGGER_ON; + pd[PAD_ZR] = pad_axis(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++) @@ -883,10 +905,8 @@ int input_pad_stick_percent(double pct[2]) { } 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)); + pct[0] = input_stick_percent(pad_axis(pad, SDL_GAMEPAD_AXIS_LEFTX), pad_axis(pad, SDL_GAMEPAD_AXIS_LEFTY)); + pct[1] = input_stick_percent(pad_axis(pad, SDL_GAMEPAD_AXIS_RIGHTX), pad_axis(pad, SDL_GAMEPAD_AXIS_RIGHTY)); ok = 1; } if (in) sdl_leave(); diff --git a/tests/pad_rest_check.py b/tests/pad_rest_check.py new file mode 100644 index 00000000..5ead782c --- /dev/null +++ b/tests/pad_rest_check.py @@ -0,0 +1,90 @@ +"""A gamepad axis counts only once it has been seen at rest (runtime/input.c pad_axis): builds pad_axis, cut from +input.c by its text, with stand-ins for the SDL calls it makes, and checks that + + - an axis held from the start (a trigger mapped onto a stick, a virtual pad that reports a held axis) reads 0, and + keeps reading 0 while it stays held; + - once it has been at rest it reads what the device reports, held or not; + - each axis and each gamepad has its own state. + +No game files, no SDL. Usage: python tests/pad_rest_check.py [--cc ] +""" +import argparse +import os +import subprocess +import sys +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +MSYS_BIN = Path(r'C:\msys64\ucrt64\bin') + +HARNESS = r''' +#include +#include +#include +typedef struct SDL_Gamepad { int16_t axis[6]; } SDL_Gamepad; +typedef enum { SDL_GAMEPAD_AXIS_LEFTX, SDL_GAMEPAD_AXIS_LEFTY, SDL_GAMEPAD_AXIS_RIGHTX, SDL_GAMEPAD_AXIS_RIGHTY, + SDL_GAMEPAD_AXIS_LEFT_TRIGGER, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER } SDL_GamepadAxis; +static int16_t SDL_GetGamepadAxis(SDL_Gamepad *p, SDL_GamepadAxis a) { return p->axis[a]; } +enum { MAX_PADS = 8 }; +static SDL_Gamepad *pads[MAX_PADS]; +static uint8_t pad_rest[MAX_PADS]; +static int npads; +%s +static int fails; +#define CHECK(cond, ...) do { if (!(cond)) { printf("FAIL: " __VA_ARGS__); putchar('\n'); fails++; } } while (0) +int main(void) { + SDL_Gamepad real = {{0}}, ghost = {{0}}; + pads[0] = &ghost, pads[1] = &real, npads = 2; + ghost.axis[SDL_GAMEPAD_AXIS_RIGHTX] = 32767; // held from the start, for good + real.axis[SDL_GAMEPAD_AXIS_RIGHTX] = 20000; // held at the start by the player + for (int i = 0; i < 100; i++) { + CHECK(pad_axis(&ghost, SDL_GAMEPAD_AXIS_RIGHTX) == 0, "a held axis read %%d at poll %%d", pad_axis(&ghost, SDL_GAMEPAD_AXIS_RIGHTX), i); + CHECK(pad_axis(&real, SDL_GAMEPAD_AXIS_RIGHTX) == 0, "the player's held stick read before release"); + } + CHECK(pad_axis(&real, SDL_GAMEPAD_AXIS_LEFTX) == 0 && pad_axis(&real, SDL_GAMEPAD_AXIS_LEFTY) == 0, "resting axes"); + real.axis[SDL_GAMEPAD_AXIS_LEFTX] = -30000; + CHECK(pad_axis(&real, SDL_GAMEPAD_AXIS_LEFTX) == -30000, "an axis seen at rest reads the device: %%d", + pad_axis(&real, SDL_GAMEPAD_AXIS_LEFTX)); + real.axis[SDL_GAMEPAD_AXIS_RIGHTX] = 3000; // let go (drift below the rest band) + CHECK(pad_axis(&real, SDL_GAMEPAD_AXIS_RIGHTX) == 3000, "a released stick reads the device"); + real.axis[SDL_GAMEPAD_AXIS_RIGHTX] = 32767; + CHECK(pad_axis(&real, SDL_GAMEPAD_AXIS_RIGHTX) == 32767, "and then reads full tilt"); + CHECK(pad_axis(&ghost, SDL_GAMEPAD_AXIS_RIGHTX) == 0, "another gamepad's axis keeps its own state"); + ghost.axis[SDL_GAMEPAD_AXIS_LEFT_TRIGGER] = 32767; + CHECK(pad_axis(&ghost, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) == 0, "a trigger held from the start"); + SDL_Gamepad stray = {{32767}}; + CHECK(pad_axis(&stray, SDL_GAMEPAD_AXIS_LEFTX) == 32767, "a gamepad not open is read as it is"); + printf(fails ? "pad rest: %%d failures\n" : "pad rest: ok\n", fails); + return fails != 0; +} +''' + + +def cut(text, start, end_marker): + a = text.index(start) + b = text.index(end_marker, a) + return text[a:b] + + +def main(): + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument('--cc', help='the C compiler (default: MSYS2 UCRT64 clang on Windows, clang elsewhere)') + args = ap.parse_args() + win = os.name == 'nt' + cc = args.cc or (str(MSYS_BIN / 'clang.exe') if win and MSYS_BIN.exists() else 'clang') + env = dict(os.environ) + if win and MSYS_BIN.exists(): + env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '') + src = (ROOT / 'runtime' / 'input.c').read_text(encoding='utf-8') + body = cut(src, 'enum { AXIS_REST', '\nstatic void stick(') + with tempfile.TemporaryDirectory(prefix='nx2navi-pad-rest-') as tmp: + c = Path(tmp) / 'check.c' + c.write_text(HARNESS % body, encoding='utf-8') + exe = Path(tmp) / ('check.exe' if win else 'check') + subprocess.check_call([cc, '-O2', '-Wall', '-Wno-unused-function', str(c), '-o', str(exe)], env=env) + return subprocess.call([str(exe)]) + + +if __name__ == '__main__': + sys.exit(main())