Add DualShock 3 support on Windows, read straight from the controller when no driver presents it, off by default.

runtime/input_ds3_win.c finds a DualShock 3 through hid.dll and setupapi (loaded at run time), sends it the command
that makes it report, sets its first player light and decodes its reports (runtime/input_ds3.c, tested by
tests/ds3_check) into the pad state, with rumble through its light motor. input.c follows whichever pad was pressed
last when an SDL pad is connected too. ds3 = 1 in nx2navi.ini turns it on: it starts a scan for the controller every
three seconds and opens it read-write, so it stays off unless a player asks for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
FemOrWhat
2026-11-09 08:21:07 -05:00
committed by chrg6
parent 2d94ede66a
commit 844940c6fb
10 changed files with 591 additions and 5 deletions

View File

@@ -251,6 +251,7 @@ change passes these, all run with `.venv`:
| `tests\diff\atomic.py` | exclusive load/store sequences and ARMv8.1 atomics | no |
| `tests\diff\func.py` | whole functions as the generator emits them, loops, jump tables and calls included (calls go to a stub that checks the registers at the call) | yes |
| `tests\flags_check.py` | the function-local flag helpers of `runtime\cpu.h` against its packed ones | no |
| `tests\ds3_check.py` | the DualShock 3 report decoding and output report of `runtime\input_ds3.c` | no |
| `tests\excl_mt.py` | the exclusive monitor helpers under contention and ABA, on several threads | no |
| `tests\jumptables_check.py` | `recomp/jumptables.py` against a Unicorn oracle; must report 0 disagreements, on 1.4.3 too | yes |

View File

@@ -8,7 +8,7 @@ disables both (automated runs). `NX2NAVI_*` settings can also be set in `nx2navi
`motion = 0`, `rumble = 0`); the environment overrides the file. `NX2NAVI_RUMBLE_STRENGTH` is the rumble strength in
percent (0 to 300, default 130; 0 is the same as `rumble = 0`). `NX2NAVI_RUMBLE_CURVE` is the exponent applied to the
amplitude before the strength (0.2 to 1.5, default 0.6; lower makes weak pulses stronger, 1 is linear).
`NX2NAVI_RUMBLE_TRIGGERS=1` also drives the impulse triggers of an Xbox pad.
`NX2NAVI_RUMBLE_TRIGGERS=1` also drives the impulse triggers of an Xbox pad. `NX2NAVI_DS3=1` turns on the direct reader for a DualShock 3 (see PlayStation 3 controllers).
`NX2NAVI_PAD_LAYOUT` (`pad_layout`, the *Button layout* row on the Controls tab, live) picks which face button is Switch A:
@@ -120,6 +120,21 @@ The stick direction bits (`StickLLeft` to `StickRDown`) are derived from the sti
Windows does not present the Switch 2 Pro Controller as a gamepad SDL can open (the log says `input: no gamepad`), so it needs a bridge program that wakes it and creates a virtual gamepad, for example one that emulates a DualSense: that gives buttons, rumble, gyro and accelerometer. The log then shows `input: gamepad "DualSense Wireless Controller" ... (rumble, gyro and accelerometer, ...)` and `input: motion sensors on`. Bridges that output a DualShock 4 whose gyro and accelerometer never change give buttons and rumble but no motion. Run one bridge at a time; Steam Input can also claim the controller, so close Steam if the bridge does not find it. With several virtual and real gamepads connected the game follows whichever one is pressed.
## PlayStation 3 controllers
With `ds3 = 1` in `nx2navi.ini` (off by default), a DualShock 3 on Windows is read directly (`runtime/input_ds3_win.c`) when no driver presents it to SDL or XInput as a gamepad. Plug it in by USB, or pair it with a Bluetooth driver that exposes it as a HID device with its own name, "PLAYSTATION(R)3 Controller"; the log then shows `input: DualShock 3 opened`, the controller's first player light turns on and `input: DualShock 3 is reporting` follows. The reader sends the controller the command that makes it start reporting, which it does not do on its own. It looks for a controller every three seconds while none is open, so one plugged in or switched on later is found without a restart.
| Switch | DualShock 3 by position |
|---|---|
| A, B, X, Y | circle, cross, triangle, square (east, south, north, west: the button layout setting applies as for any pad) |
| ZL, ZR | L2, R2 (over 50% pressure) |
| L, R | L1, R1 |
| Left, right stick click | L3, R3 |
| Plus, Minus | Start, Select |
| D-pad | D-pad |
The PS button is not used. The Controls tab binds and remaps its buttons like any other pad's, with the same pad names. Vibration works through the strength, curve and *Vibration* settings: the controller's light motor can only be on or off, so it comes on from a sixteenth of the amplitude the game asks of it. The accelerometer and the single gyro axis are not used: the game's motion controls need all three gyro axes, so a DualShock 3 gives no motion. `ds3 = 0` in `nx2navi.ini` (the default, live) turns the reader off. A controller that a driver presents in a generic gamepad mode, with another name or report layout, is not recognised by the reader and goes through SDL or XInput instead, as before.
## Scripted input
`NX2NAVI_INPUT_SCRIPT=<file>` replays presses. It is ORed with real input and still applies under `NX2NAVI_INPUT=none`. One event per line, `#` starts a comment:

View File

@@ -379,6 +379,10 @@
# rumble_strength = 130
# rumble_curve = 0.6
# rumble_triggers = 0
# PlayStation 3 controller (DualShock 3), Windows: 1 reads its buttons, sticks and rumble directly from the controller
# when no driver presents it to the game as a gamepad; 0 (default) leaves it to SDL and XInput. Changes while the game
# runs take effect at once.
# ds3 = 0
# Which gamepad button is Switch A: nintendo (the default: the Switch's buttons by position on any pad, east A,
# south B, west Y, north X), xbox (south A, east B, west X, north Y, as an Xbox pad labels them; cross A on a
# PlayStation pad), playstation (the same as xbox: cross A, circle B, square X, triangle Y) or auto (nintendo for a

View File

@@ -41,7 +41,7 @@ static const char *const known[] = {
"BIND_ZL", "BIND_ZR", "BLOCK_VIEWS_3D", "CLOTH_REALTIME", "COLOR_BRIGHTNESS", "COLOR_CONTRAST", "COLOR_GAMMA",
"COLOR_PRESET", "COLOR_SATURATION", "CONTACT_DEBUG", "CONTACT_SHADOWS", "CORE_DIAG", "CRIT", "CRIT_OUT",
"DESC_TEMPLATE", "DEVICE_PROFILE", "DISCORD_APP_ID", "DISCORD_IMAGE", "DISCORD_RPC", "DLAA", "DLAA_CAM_KEEP", "DLAA_JITTER", "DLAA_MV", "DLAA_MASKDEBUG", "DLAA_MVDEBUG", "DLAA_OBJMV", "DLAA_SURVEY", "DLAA_TIMES", "DLSS", "DLSS_CHAR_MASK", "DLSS_PRESET", "DLSS_REACTIVE", "DUMP", "DUMP_BUF", "DUMP_DIR",
"DUMP_LOG", "LOG_CS_BINDS", "LOG_TEX_FLAGS", "LOG_MESH", "MESH", "POOL", "NATIVE_TENSOR", "LOG_RCOND", "LOG_RT_SIZES", "DUMP_WHITE", "DUMP_AFTER", "DUMP_MOVIE", "SUBMIT_FINISH", "ACQUIRE_WAIT", "CS_FALLBACK", "EXEFS", "FEEDBACK", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES",
"DUMP_LOG", "DS3", "LOG_CS_BINDS", "LOG_TEX_FLAGS", "LOG_MESH", "MESH", "POOL", "NATIVE_TENSOR", "LOG_RCOND", "LOG_RT_SIZES", "DUMP_WHITE", "DUMP_AFTER", "DUMP_MOVIE", "SUBMIT_FINISH", "ACQUIRE_WAIT", "CS_FALLBACK", "EXEFS", "FEEDBACK", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES",
"FLIGHT_SPIKE_MS", "FLIGHT_THUMBS", "FLUSH_SPAN", "FOLIAGE_SHADOW_CULL", "FOLIAGE_SHADOW_VRS", "FOV", "FOV_PATCH", "FG_HUDLESS_SHOW", "FPS60", "FPS_LIMIT", "FRAMEGEN", "FREEZE_IDLE", "FREE_CORES",
"FULLSCREEN", "FXAA", "GAME_ASPECT", "GAME_RES", "GPA", "GPA_EMPTY", "GPA_TWIN", "GPL", "GPL_DRAW_BUILD", "GPU", "GPU_INDEX", "GPU_PASSES", "GPU_NAME", "HDR", "HDR_BLOWOUT", "HDR_CONTRAST",
"HDR_EXPOSURE", "HDR_FLARE", "HDR_GAMMA", "HDR_GAME_NITS", "HDR_GRADE", "HDR_HIGHLIGHT_SAT", "HDR_HIGHLIGHTS", "HDR_HUE",
@@ -91,7 +91,7 @@ static const struct { const char *key, *section; } sections[] = {
{"MODS_DIR", "mods"}, {"MODS_ORDER", "mods"}, {"MODS_DISABLED", "mods"}, {"MODS_REBUILD", "mods"},
{"MENU_LANG", "language"}, {"LANG", "language"}, {"OPERATION_MODE", "language"},
{"VOLUME", "audio"}, {"AUDIO_CHANNELS", "audio"}, {"AUDIO_DEVICE", "audio"}, {"MICROPHONE", "audio"}, {"MENU_SOUND_VOLUME", "audio"},
{"MOTION", "input"}, {"RUMBLE_STRENGTH", "input"}, {"RUMBLE_CURVE", "input"}, {"RUMBLE_TRIGGERS", "input"},
{"MOTION", "input"}, {"RUMBLE_STRENGTH", "input"}, {"RUMBLE_CURVE", "input"}, {"RUMBLE_TRIGGERS", "input"}, {"DS3", "input"},
{"PAD_LAYOUT", "input"}, {"PROMPTS", "input"}, {"STICK_DEADZONE", "input"}, {"LSTICK_DEADZONE", "input"}, {"RSTICK_DEADZONE", "input"},
{"STICK_SENS", "input"},
{"MENU_CURSOR", "input"}, {"MOUSE_MODE", "input"}, {"MOUSE_SENS", "input"}, {"WALK_KEY", "input"}, {"WALK_SCALE", "input"}, {"INPUT_RECORD", "input"},
@@ -173,6 +173,7 @@ static const CfgMeta metas[] = {
{"MICROPHONE", "1", "0|1", RESTART}, // ov_audio_input.c mic_on: 0 refuses the game's input streams
{"MOTION", "1", "0|1", LIVE}, {"RUMBLE_STRENGTH", "130", "0..300", LIVE}, {"RUMBLE_CURVE", "0.6", "0.2..1.5", LIVE},
{"RUMBLE_TRIGGERS", "0", "0|1", LIVE}, {"PAD_LAYOUT", "nintendo", "auto|nintendo|xbox|playstation", LIVE},
{"DS3", "0", "0|1", LIVE}, // input_ds3_win.c ds3_read: read a DualShock 3 directly; off: no HID scan
{"PROMPTS", "nintendo", "nintendo|xbox|playstation|pc", RESTART},
{"STICK_DEADZONE", "8", "0..50", LIVE}, {"STICK_SENS", "1", "0.1..3", RESTART},
{"LSTICK_DEADZONE", "", "0..50", LIVE}, {"RSTICK_DEADZONE", "", "0..50", LIVE},

View File

@@ -17,6 +17,9 @@
#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"
@@ -691,11 +694,42 @@ static int xinput_read(uint8_t pd[PAD_COUNT], int32_t *raw) {
}
#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(uint64_t *b, int32_t sl[2], int32_t sr[2]) {
uint8_t pd[PAD_COUNT] = {0};
#ifdef _WIN32
if (!pad_physical(pd)) {
int32_t raw[4];
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);
@@ -715,6 +749,15 @@ int input_pad_stick_percent(double pct[2]) {
int ok = 0;
pct[0] = pct[1] = 0;
plat_mutex_lock(&lock);
#ifdef _WIN32
uint8_t dd[PAD_COUNT];
int32_t raw[4];
if (state == 1 && ds3_primary(dd, raw, pad && SDL_GamepadConnected(pad))) {
pct[0] = input_stick_percent(raw[0], raw[1]);
pct[1] = input_stick_percent(raw[2], raw[3]);
ok = 1;
} else
#endif
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));
@@ -903,6 +946,14 @@ int input_capture(int reset, int *id) {
plat_mutex_lock(&lock);
if (!state) start();
if (state == 1) pad_physical(p);
#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) {
@@ -932,6 +983,9 @@ int input_rumble(uint16_t low, uint16_t high, uint32_t ms) {
ok = SDL_RumbleGamepad(pad, low, high, ms);
if (triggers) SDL_RumbleGamepadTriggers(pad, high, high, ms); // unsupported pads return false: ignored
}
#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;
}

42
runtime/input_ds3.c Normal file
View File

@@ -0,0 +1,42 @@
// The DualShock 3's report formats, free of any operating system (input_ds3.h): the input report to inputs by position
// and the output report that drives the motors and the player light. tests/ds3_check.c checks both; input_ds3_win.c
// moves the reports.
#include <string.h>
#include "input_ds3.h"
int ds3_decode(const uint8_t *r, unsigned n, uint8_t pd[PAD_COUNT], int32_t raw[4]) {
if (n < 10 || r[0] != 0x01) return 0;
uint8_t b1 = r[2], b2 = r[3];
memset(pd, 0, PAD_COUNT);
pd[PAD_BACK] = (b1 & 0x01) != 0, pd[PAD_LSTICK] = (b1 & 0x02) != 0, pd[PAD_RSTICK] = (b1 & 0x04) != 0;
pd[PAD_START] = (b1 & 0x08) != 0;
pd[PAD_DPAD_UP] = (b1 & 0x10) != 0, pd[PAD_DPAD_RIGHT] = (b1 & 0x20) != 0;
pd[PAD_DPAD_DOWN] = (b1 & 0x40) != 0, pd[PAD_DPAD_LEFT] = (b1 & 0x80) != 0;
pd[PAD_L] = (b2 & 0x04) != 0, pd[PAD_R] = (b2 & 0x08) != 0;
pd[PAD_NORTH] = (b2 & 0x10) != 0, pd[PAD_EAST] = (b2 & 0x20) != 0; // triangle, circle
pd[PAD_SOUTH] = (b2 & 0x40) != 0, pd[PAD_WEST] = (b2 & 0x80) != 0; // cross, square
// The triggers go by pressure, as an XInput trigger does; a report without it (n < 20) has the switch only
unsigned l2 = n > 19 ? r[18] : 0, r2 = n > 19 ? r[19] : 0;
pd[PAD_ZL] = l2 ? l2 > DS3_TRIGGER_ON : (b2 & 0x01) != 0;
pd[PAD_ZR] = r2 ? r2 > DS3_TRIGGER_ON : (b2 & 0x02) != 0;
for (int i = 0; i < 4; i++) {
int d = (int)r[6 + i] - 128;
int v = d >= 0 ? d * 32767 / 127 : d * 32767 / 128;
raw[i] = i & 1 ? -v : v; // y: the controller's grows downwards, the game's upwards
}
return 1;
}
void ds3_output_report(uint8_t *o, unsigned n, int left, int right, unsigned dur, int led) {
memset(o, 0, n);
if (n < 36) return;
o[0] = 0x01;
o[2] = (uint8_t)dur, o[3] = (uint8_t)(right != 0);
o[4] = (uint8_t)dur, o[5] = (uint8_t)left;
o[10] = (uint8_t)(led << 1);
for (int i = 0; i < 4; i++) { // the four lights' blink timing: steady
uint8_t *l = o + 11 + 5 * i;
l[0] = 0xFF, l[1] = 0x27, l[2] = 0x10, l[3] = 0x00, l[4] = 0x32;
}
}

29
runtime/input_ds3.h Normal file
View File

@@ -0,0 +1,29 @@
// The DualShock 3 reader of the Windows build (input_ds3_win.c): the controller's own HID reports, read directly, for
// when no driver presents it to SDL or XInput as a gamepad. Windows only; input.c calls it from its gamepad poll.
// NX2NAVI_DS3=0 (`ds3 = 0` in nx2navi.ini) turns it off. Controls: docs/controls.md.
#pragma once
#include <stdint.h>
#include "input_map.h"
// One poll, under input.c's lock: 1 while a DualShock 3 is connected and reporting, with its inputs by position in
// pd[PAD_*] (every entry written; PAD_ZL and PAD_ZR from the trigger pressure) and its sticks in raw[] as lx, ly, rx, ry
// in -32767..32767, up is +y, before any dead zone. 0 (pd and raw untouched) without one. Also finds a controller that
// is plugged in or switched on later, and lets go of one that went away.
int ds3_read(uint8_t pd[PAD_COUNT], int32_t raw[4]);
// Rumble for the connected controller, under input.c's lock: low (0..65535) drives the heavy (left) motor, high the
// light (right) motor, which the controller can only switch on or off (on from 1/16 of full strength). Lasts ms
// milliseconds; 0 stops it. 1 when a controller took it.
int ds3_rumble(uint16_t low, uint16_t high, uint32_t ms);
// ---- the report formats (input_ds3.c, any OS)
enum { DS3_TRIGGER_ON = 128 }; // L2/R2 pressure, 0..255, that counts as pressed
// Input report 0x01 (n bytes, the report id first) to inputs by position in pd[PAD_*] (every entry written) and the
// sticks in raw[] as lx, ly, rx, ry (-32767..32767, up is +y, rest 0). 0, with pd and raw untouched, for another report.
int ds3_decode(const uint8_t *r, unsigned n, uint8_t pd[PAD_COUNT], int32_t raw[4]);
// Output report 0x01 into o (n bytes, 36 or more; zeroed first): the heavy motor at left (0..255), the light motor on
// when right is nonzero, both for dur steps of 10 ms (0 stops them), and player light led (1..4, 0 none).
void ds3_output_report(uint8_t *o, unsigned n, int left, int right, unsigned dur, int led);

306
runtime/input_ds3_win.c Normal file
View File

@@ -0,0 +1,306 @@
// The DualShock 3 reader of the Windows build (input_ds3.h): the controller's HID reports read directly with hid.dll.
// The controller sends nothing until it is told to (a feature report, sent on connect), and SDL and XInput get nothing
// from it without a driver that does this and presents it as a gamepad; with one, SDL or XInput already see it and this
// file is only a second reader of the same pad (input.c lets the SDL pad win then, see pad_read).
//
// hid.dll and setupapi.dll are loaded at run time, so the exe links no extra library and starts without them. A scan
// for the controller runs on a thread of its own every SCAN_MS while none is open: walking every HID device takes
// milliseconds, which the game's poll must never wait for. Everything else (open, read, rumble, close) runs on the
// caller's thread, under input.c's lock.
//
// The report formats are in input_ds3.c. The accelerometer and the single (yaw) gyro are not read: the game's six-axis
// sensor needs all three gyro axes.
#include <windows.h>
#include <setupapi.h>
#include <stdint.h>
#include <string.h>
#include <wchar.h>
#include "cfg.h"
#include "host.h"
#include "input_ds3.h"
#include "platform/platform.h"
enum {
VID_SONY = 0x054C, PID_DS3 = 0x0268,
SCAN_MS = 3000, // between scans while no controller is open
OPEN_RETRY_MS = 1000, // between attempts to open a path the scan found
SILENT_NS = 2000000000, // no report this long: not reporting (the read stays queued, a report revives it)
PATH_MAX_LEN = 512,
};
typedef struct { ULONG Size; USHORT VendorID, ProductID, VersionNumber; } HidAttr; // HIDD_ATTRIBUTES
typedef struct { USHORT Usage, UsagePage, InputLen, OutputLen, FeatureLen; USHORT Rest[29]; } HidCaps; // HIDP_CAPS (64 bytes)
static struct {
void (__stdcall *GetHidGuid)(GUID *);
BOOLEAN (__stdcall *GetAttributes)(HANDLE, HidAttr *);
BOOLEAN (__stdcall *GetProductString)(HANDLE, void *, ULONG);
BOOLEAN (__stdcall *GetPreparsed)(HANDLE, void **);
BOOLEAN (__stdcall *FreePreparsed)(void *);
LONG (__stdcall *GetCaps)(void *, HidCaps *); // NTSTATUS, 0x110000 on success
BOOLEAN (__stdcall *SetFeature)(HANDLE, void *, ULONG);
BOOLEAN (__stdcall *SetOutput)(HANDLE, void *, ULONG);
HDEVINFO (__stdcall *ClassDevs)(const GUID *, PCSTR, HWND, DWORD);
BOOL (__stdcall *EnumInterfaces)(HDEVINFO, PSP_DEVINFO_DATA, const GUID *, DWORD, PSP_DEVICE_INTERFACE_DATA);
BOOL (__stdcall *InterfaceDetail)(HDEVINFO, PSP_DEVICE_INTERFACE_DATA, PSP_DEVICE_INTERFACE_DETAIL_DATA_A, DWORD,
PDWORD, PSP_DEVINFO_DATA);
BOOL (__stdcall *DestroyList)(HDEVINFO);
} api;
static int api_state; // 0 not tried, 1 loaded, -1 unavailable
static int api_load(void) {
if (api_state) return api_state > 0;
api_state = -1;
HMODULE hid = LoadLibraryA("hid.dll"), sa = LoadLibraryA("setupapi.dll");
if (!hid || !sa) return 0;
#define LOAD(m, field, name) *(FARPROC *)&api.field = GetProcAddress(m, name)
LOAD(hid, GetHidGuid, "HidD_GetHidGuid");
LOAD(hid, GetAttributes, "HidD_GetAttributes");
LOAD(hid, GetProductString, "HidD_GetProductString");
LOAD(hid, GetPreparsed, "HidD_GetPreparsedData");
LOAD(hid, FreePreparsed, "HidD_FreePreparsedData");
LOAD(hid, GetCaps, "HidP_GetCaps");
LOAD(hid, SetFeature, "HidD_SetFeature");
LOAD(hid, SetOutput, "HidD_SetOutputReport");
LOAD(sa, ClassDevs, "SetupDiGetClassDevsA");
LOAD(sa, EnumInterfaces, "SetupDiEnumDeviceInterfaces");
LOAD(sa, InterfaceDetail, "SetupDiGetDeviceInterfaceDetailA");
LOAD(sa, DestroyList, "SetupDiDestroyDeviceInfoList");
#undef LOAD
if (!api.GetHidGuid || !api.GetAttributes || !api.GetPreparsed || !api.FreePreparsed || !api.GetCaps ||
!api.SetFeature || !api.SetOutput || !api.ClassDevs || !api.EnumInterfaces || !api.InterfaceDetail ||
!api.DestroyList)
return 0;
api_state = 1;
return 1;
}
// ---- the scan (its own thread)
static char found_path[PATH_MAX_LEN]; // written by the scan thread, then found set; read by the poll
static volatile int32_t found; // found_path holds a controller
static volatile int32_t open_now; // the poll holds a controller open: the scan rests
static volatile int32_t scan_on; // NX2NAVI_DS3 is on (default off): the scan runs
static volatile int32_t scan_started;
// The path of the first DualShock 3 among the HID devices that are present, copied into out; 0 when there is none.
static int scan_once(char *out, size_t cap) {
GUID g;
api.GetHidGuid(&g);
HDEVINFO set = api.ClassDevs(&g, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
if (set == INVALID_HANDLE_VALUE) return 0;
int hit = 0;
for (DWORD i = 0; !hit; i++) {
SP_DEVICE_INTERFACE_DATA d;
memset(&d, 0, sizeof d);
d.cbSize = sizeof d;
if (!api.EnumInterfaces(set, NULL, &g, i, &d)) break;
DWORD need = 0;
api.InterfaceDetail(set, &d, NULL, 0, &need, NULL);
if (need < sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_A) || need > 4096) continue;
SP_DEVICE_INTERFACE_DETAIL_DATA_A *det = (SP_DEVICE_INTERFACE_DETAIL_DATA_A *)HeapAlloc(GetProcessHeap(), 0, need);
if (!det) continue;
det->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_A);
if (api.InterfaceDetail(set, &d, det, need, NULL, NULL) && strlen(det->DevicePath) < cap) {
// access 0: enough to ask for the ids, and it works on a device another program has open
HANDLE h = CreateFileA(det->DevicePath, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
if (h != INVALID_HANDLE_VALUE) {
HidAttr at = {sizeof at};
if (api.GetAttributes(h, &at) && at.VendorID == VID_SONY && at.ProductID == PID_DS3) {
strcpy(out, det->DevicePath);
hit = 1;
}
CloseHandle(h);
}
}
HeapFree(GetProcessHeap(), 0, det);
}
api.DestroyList(set);
return hit;
}
static void scan_thread(void *arg) {
(void)arg;
for (;;) {
if (scan_on && !open_now && !found) {
char p[PATH_MAX_LEN];
if (scan_once(p, sizeof p)) {
memcpy(found_path, p, sizeof p);
plat_xchg32(&found, 1);
}
}
plat_sleep_ms(SCAN_MS);
}
}
// ---- the open controller (the poll's thread)
static struct {
HANDLE h;
HANDLE ev; // the read's event, made once
OVERLAPPED ov;
int pending; // a ReadFile is queued
uint8_t buf[128]; // its buffer
unsigned in_len, out_len, feat_len;
uint8_t pd[PAD_COUNT]; // the newest report
int32_t raw[4];
uint64_t last_report; // plat_now_ns of the newest valid report, 0 none yet
int reported; // logged: a report came in
uint64_t next_open;
int motors_on; // the controller's motors are driven now
int sent_left, sent_right; // the last output report's motors
uint64_t sent_at, until; // when it went out; when the rumble ends (plat_now_ns), 0 none
} s;
static void drop(const char *why) {
if (!s.h) return;
rt_log("input: DualShock 3 %s", why);
CancelIo(s.h);
CloseHandle(s.h);
memset(&s.h, 0, sizeof s.h); // h only: the buffers stay where a cancelled read may still write
s.pending = 0;
s.last_report = 0;
s.reported = 0;
s.motors_on = 0;
s.sent_left = s.sent_right = 0;
s.until = 0;
plat_xchg32(&open_now, 0);
}
// Output report 0x01: motors and the player light. left 0..255 (heavy), right 0 or 1 (light), dur in 10 ms steps.
static int send_output(int left, int right, unsigned dur, int led) {
uint8_t o[64];
unsigned n = s.out_len ? s.out_len : 49;
if (n > sizeof o) n = sizeof o;
ds3_output_report(o, n, left, right, dur, led);
return api.SetOutput(s.h, o, n) != 0;
}
static void try_open(void) {
uint64_t now = plat_now_ns();
if (now < s.next_open || !plat_cas32(&found, 0, 1)) return; // nothing found, or too soon
s.next_open = now + (uint64_t)OPEN_RETRY_MS * 1000000u;
char path[PATH_MAX_LEN];
memcpy(path, found_path, sizeof path);
HANDLE h = CreateFileA(path, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
FILE_FLAG_OVERLAPPED, NULL);
if (h == INVALID_HANDLE_VALUE) {
rt_log("input: DualShock 3 found but not opened (error %lu)", (unsigned long)GetLastError());
return;
}
// A pad that presents itself in another report layout (a driver's generic mode) would read as garbage here
wchar_t name[128] = {0};
if (api.GetProductString && api.GetProductString(h, name, sizeof name - sizeof *name)) {
for (wchar_t *c = name; *c; c++) if (*c >= 'a' && *c <= 'z') *c -= 32;
if (!wcsstr(name, L"PLAYSTATION")) {
rt_log("input: HID device \"%ls\" has the DualShock 3's ids but not its name; left to SDL", name);
CloseHandle(h);
return;
}
}
HidCaps caps;
void *pre = NULL;
memset(&caps, 0, sizeof caps);
if (!api.GetPreparsed(h, &pre)) { CloseHandle(h); return; }
LONG st = api.GetCaps(pre, &caps);
api.FreePreparsed(pre);
if (st != 0x110000 || caps.InputLen < 10 || caps.InputLen > sizeof s.buf) {
rt_log("input: DualShock 3 reports are not as expected (input %u bytes); left to SDL", caps.InputLen);
CloseHandle(h);
return;
}
s.h = h;
s.in_len = caps.InputLen, s.out_len = caps.OutputLen, s.feat_len = caps.FeatureLen;
if (!s.ev) s.ev = CreateEventA(NULL, TRUE, FALSE, NULL);
s.pending = 0;
plat_xchg32(&open_now, 1);
// Reports start only after this: feature report 0xF4, "start sending"
uint8_t en[64] = {0xF4, 0x42, 0x03, 0x00, 0x00};
unsigned fl = s.feat_len > 5 && s.feat_len <= sizeof en ? s.feat_len : 5;
int ok = api.SetFeature(h, en, fl) != 0;
if (!ok && fl != 5) ok = api.SetFeature(h, en, 5) != 0;
rt_log("input: DualShock 3 opened (reports %u bytes, start command %s)", s.in_len, ok ? "accepted" : "refused");
send_output(0, 0, 0, 1); // player 1's light: shows that the controller is ours
}
static void parse(const uint8_t *r, unsigned n) {
if (!ds3_decode(r, n, s.pd, s.raw)) return;
s.last_report = plat_now_ns();
if (!s.reported) s.reported = 1, rt_log("input: DualShock 3 is reporting");
}
// Reads every report that came in since the last poll; the newest wins.
static void pump(void) {
for (int guard = 0; guard < 64 && s.h; guard++) {
if (!s.pending) {
memset(&s.ov, 0, sizeof s.ov);
s.ov.hEvent = s.ev;
ResetEvent(s.ev);
if (!ReadFile(s.h, s.buf, s.in_len, NULL, &s.ov) && GetLastError() != ERROR_IO_PENDING) {
drop("disconnected");
return;
}
s.pending = 1;
}
DWORD n = 0;
if (!GetOverlappedResult(s.h, &s.ov, &n, FALSE)) {
if (GetLastError() == ERROR_IO_INCOMPLETE) return;
drop("disconnected");
return;
}
s.pending = 0;
parse(s.buf, n);
}
}
// Ends a rumble whose time is up (the controller stops by itself after the duration it was given; this is the same
// moment from our side, so the next rumble starts from a known state).
static void rumble_expire(void) {
if (s.until && plat_now_ns() >= s.until) {
s.until = 0;
if (s.motors_on) s.motors_on = 0, s.sent_left = s.sent_right = 0, send_output(0, 0, 0, 1);
}
}
int ds3_read(uint8_t pd[PAD_COUNT], int32_t raw[4]) {
static unsigned seen = ~0u;
unsigned ver = cfg_version();
if (ver != seen) { // live: the setting is read again when one changed
seen = ver;
int on = cfg_bool("NX2NAVI_DS3"); // off by default (cfg.c metas): the reader opens the pad read-write
if (!on && s.h) drop("closed (ds3 = 0)");
plat_xchg32(&scan_on, on);
}
if (!scan_on || !api_load()) return 0;
if (!scan_started) {
scan_started = 1;
plat_thread_spawn(scan_thread, NULL, "ds3-scan", 1u << 18);
}
if (!s.h) try_open();
if (!s.h) return 0;
pump();
rumble_expire();
if (!s.h || !s.last_report || plat_now_ns() - s.last_report > SILENT_NS) return 0;
memcpy(pd, s.pd, PAD_COUNT);
memcpy(raw, s.raw, sizeof s.raw);
return 1;
}
int ds3_rumble(uint16_t low, uint16_t high, uint32_t ms) {
if (!s.h || !s.last_report) return 0;
int left = low >> 8, right = high >= 0x1000;
if (!ms) left = right = 0;
uint64_t now = plat_now_ns();
// Resend when a motor changed or the controller's own timer is about to run out; the game renews a rumble every
// 50 ms, which is more often than the controller needs
unsigned dur = ms / 10 + 5 > 255 ? 255 : ms / 10 + 5;
int changed = left != s.sent_left || right != s.sent_right;
if (!changed && now - s.sent_at < (uint64_t)ms * 500000u) {
s.until = ms ? now + (uint64_t)ms * 1000000u : 0;
return 1;
}
if (!send_output(left, right, ms ? dur : 0, 1)) return 0;
s.sent_left = left, s.sent_right = right, s.sent_at = now;
s.motors_on = left || right;
s.until = ms && s.motors_on ? now + (uint64_t)ms * 1000000u : 0;
return 1;
}

105
tests/ds3_check.c Normal file
View File

@@ -0,0 +1,105 @@
// runtime/input_ds3.c: the DualShock 3 input report decoded to inputs by position, and the output report's layout.
// Built and run by tests/ds3_check.py; prints each failed check and exits 1 on any.
#include <stdio.h>
#include <string.h>
#include "input_ds3.h"
static int fails;
#define CHECK(c, ...) do { if (!(c)) { fails++; printf("FAIL line %d: ", __LINE__); printf(__VA_ARGS__); printf("\n"); } } while (0)
// A report at rest: id 1, sticks centred, nothing pressed.
static void rest(uint8_t r[49]) {
memset(r, 0, 49);
r[0] = 0x01;
r[6] = r[7] = r[8] = r[9] = 128;
}
int main(void) {
uint8_t r[49], pd[PAD_COUNT];
int32_t raw[4];
rest(r);
CHECK(ds3_decode(r, 49, pd, raw), "a rest report decodes");
for (int i = 0; i < PAD_COUNT; i++) CHECK(!pd[i], "rest: input %d is down", i);
for (int i = 0; i < 4; i++) CHECK(raw[i] == 0, "rest: axis %d is %d", i, raw[i]);
// Every button of byte 2 and 3 alone lights exactly its own input.
static const struct { int byte; uint8_t bit; int pad; const char *name; } map[] = {
{2, 0x01, PAD_BACK, "select"}, {2, 0x02, PAD_LSTICK, "L3"}, {2, 0x04, PAD_RSTICK, "R3"},
{2, 0x08, PAD_START, "start"}, {2, 0x10, PAD_DPAD_UP, "up"}, {2, 0x20, PAD_DPAD_RIGHT, "right"},
{2, 0x40, PAD_DPAD_DOWN, "down"}, {2, 0x80, PAD_DPAD_LEFT, "left"},
{3, 0x01, PAD_ZL, "L2"}, {3, 0x02, PAD_ZR, "R2"}, {3, 0x04, PAD_L, "L1"}, {3, 0x08, PAD_R, "R1"},
{3, 0x10, PAD_NORTH, "triangle"}, {3, 0x20, PAD_EAST, "circle"}, {3, 0x40, PAD_SOUTH, "cross"},
{3, 0x80, PAD_WEST, "square"}};
for (size_t k = 0; k < sizeof map / sizeof *map; k++) {
rest(r);
r[map[k].byte] = map[k].bit;
CHECK(ds3_decode(r, 49, pd, raw), "%s decodes", map[k].name);
for (int i = 0; i < PAD_COUNT; i++)
CHECK(!!pd[i] == (i == map[k].pad), "%s: input %d is %d", map[k].name, i, pd[i]);
}
// The PS button (byte 4) is not a Switch button.
rest(r);
r[4] = 1;
ds3_decode(r, 49, pd, raw);
for (int i = 0; i < PAD_COUNT; i++) CHECK(!pd[i], "PS button: input %d is down", i);
// Sticks: 0 and 255 reach the ends, y is turned so that up is positive.
rest(r);
r[6] = 0, r[7] = 0, r[8] = 255, r[9] = 255;
ds3_decode(r, 49, pd, raw);
CHECK(raw[0] == -32767 && raw[1] == 32767, "left stick up-left: %d %d", raw[0], raw[1]);
CHECK(raw[2] == 32767 && raw[3] == -32767, "right stick down-right: %d %d", raw[2], raw[3]);
rest(r);
r[6] = 129, r[7] = 127;
ds3_decode(r, 49, pd, raw);
CHECK(raw[0] > 0 && raw[0] < 400 && raw[1] > 0 && raw[1] < 400, "one step from rest: %d %d", raw[0], raw[1]);
// Triggers: pressure decides when the report has it, the switch when the pressure reads 0.
rest(r);
r[3] = 0x01, r[18] = 100;
ds3_decode(r, 49, pd, raw);
CHECK(!pd[PAD_ZL], "L2 at pressure 100 with the switch on is %d", pd[PAD_ZL]);
r[18] = 200;
ds3_decode(r, 49, pd, raw);
CHECK(pd[PAD_ZL], "L2 at pressure 200 is %d", pd[PAD_ZL]);
r[19] = 255;
r[3] = 0;
ds3_decode(r, 49, pd, raw);
CHECK(pd[PAD_ZR], "R2 at pressure 255 without the switch is %d", pd[PAD_ZR]);
rest(r);
r[3] = 0x02;
ds3_decode(r, 12, pd, raw); // a short report: the switch alone
CHECK(pd[PAD_ZR], "R2 switch in a short report is %d", pd[PAD_ZR]);
// Other reports are refused and leave pd and raw alone.
memset(pd, 7, sizeof pd);
raw[0] = 123;
rest(r);
r[0] = 0x02;
CHECK(!ds3_decode(r, 49, pd, raw) && pd[PAD_SOUTH] == 7 && raw[0] == 123, "another report id is refused");
rest(r);
CHECK(!ds3_decode(r, 9, pd, raw) && pd[PAD_SOUTH] == 7, "a short report is refused");
// Output report.
uint8_t o[49];
memset(o, 0xAA, sizeof o);
ds3_output_report(o, sizeof o, 200, 1, 30, 1);
CHECK(o[0] == 0x01 && o[1] == 0, "output: id %02x padding %02x", o[0], o[1]);
CHECK(o[2] == 30 && o[3] == 1 && o[4] == 30 && o[5] == 200, "output: motors %d %d %d %d", o[2], o[3], o[4], o[5]);
CHECK(o[10] == 0x02, "output: light mask %02x", o[10]);
for (int i = 0; i < 4; i++)
CHECK(!memcmp(o + 11 + 5 * i, "\xFF\x27\x10\x00\x32", 5), "output: light %d timing", i);
for (int i = 6; i < 10; i++) CHECK(o[i] == 0, "output: byte %d is %02x", i, o[i]);
for (int i = 31; i < 49; i++) CHECK(o[i] == 0, "output: tail byte %d is %02x", i, o[i]);
ds3_output_report(o, sizeof o, 999, 5, 0, 0);
CHECK(o[3] == 1 && o[2] == 0 && o[10] == 0, "output: light on is 1 (%d), stop (%d), no light (%02x)", o[3], o[2], o[10]);
memset(o, 0xAA, sizeof o);
ds3_output_report(o, 20, 1, 1, 1, 1); // too short for the layout: zeros
for (int i = 0; i < 20; i++) CHECK(o[i] == 0, "output: short buffer byte %d is %02x", i, o[i]);
printf(fails ? "ds3_check: %d failed\n" : "ds3_check: ok\n", fails);
return fails != 0;
}

29
tests/ds3_check.py Normal file
View File

@@ -0,0 +1,29 @@
"""Build and run tests/ds3_check.c: runtime/input_ds3.c's input report decoding and output report layout.
Usage: ds3_check.py
"""
import os
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / 'tests' / 'diff'))
from common import MSYS_BIN, compiler # noqa: E402
def main():
work = ROOT / 'build' / 'test' / 'ds3'
work.mkdir(parents=True, exist_ok=True)
exe = work / ('ds3_check.exe' if os.name == 'nt' else 'ds3_check')
env = dict(os.environ)
if os.name == 'nt' and MSYS_BIN.exists():
env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '')
subprocess.check_call([compiler(), '-O1', '-Wall', '-fno-strict-aliasing', '-I', str(ROOT / 'runtime'),
str(ROOT / 'tests' / 'ds3_check.c'), str(ROOT / 'runtime' / 'input_ds3.c'), '-o', str(exe)],
env=env)
return subprocess.run([str(exe)], env=env).returncode
if __name__ == '__main__':
sys.exit(main())