ohmu
·
2026-10-15
1#include "gamepad.h"
2#include "common/debug.h"
3#include "common/io.h"
4#include "common/mallocHelper.h"
5#include "common/time.h"
6#include "common/windows/unicode.h"
7
8#include <windows.h>
9#include <hidsdi.h>
10
11static const char* detectKnownDeviceName(uint32_t vendorId, uint32_t productId) {
12 switch (vendorId) {
13 // Nintendo
14 case 0x057E: {
15 switch (productId) {
16 case 0x2006:
17 return "Nintendo Switch Joycon L";
18 case 0x2007:
19 return "Nintendo Switch Joycon R";
20 case 0x2008:
21 return "Nintendo Switch Joycon L+R";
22 case 0x2009:
23 return "Nintendo Switch Pro Controller";
24 case 0x200E:
25 return "Nintendo Switch Charging Grip";
26 case 0x2017:
27 return "Nintendo Switch SNES Controller";
28
29 case 0x2066:
30 return "Nintendo Switch 2 Joycon R";
31 case 0x2067:
32 return "Nintendo Switch 2 Joycon L";
33 case 0x2068:
34 return "Nintendo Switch 2 Joycon L+R";
35 case 0x2069:
36 return "Nintendo Switch 2 Pro Controller";
37
38 default:
39 return nullptr;
40 }
41 }
42
43 // Sony
44 case 0x054C: {
45 switch (productId) {
46 case 0x0268:
47 return "Sony DualShock 3 / Six Axis";
48
49 case 0x05C4:
50 return "Sony DualShock 4 Gen1";
51 case 0x09CC:
52 return "Sony DualShock 4 Gen2";
53 case 0x0BA0:
54 return "Sony DualShock 4 USB receiver";
55
56 case 0x0CE6:
57 return "Sony DualSense";
58 case 0x0DF2:
59 return "Sony DualSense Edge";
60 case 0x0E5F:
61 return "Sony Access Controller";
62
63 default:
64 return nullptr;
65 }
66 }
67
68 // Logitech
69 case 0x046D: {
70 switch (productId) {
71 case 0xC216:
72 return "Logitech F310, DirectInput";
73 case 0xC218:
74 return "Logitech F510, DirectInput";
75 case 0xC219:
76 return "Logitech F710, DirectInput";
77 case 0xC21D:
78 return "Logitech F310";
79 case 0xC21E:
80 return "Logitech F510";
81 case 0xC21F:
82 return "Logitech F710";
83
84 default:
85 return nullptr;
86 }
87 }
88
89 case 0x045E: // Microsoft Xbox compatible controllers should be handled by Windows without problems
90 default:
91 return nullptr;
92 }
93}
94
95// A HID read on Windows hands back as many bytes as the descriptor declares for the largest report,
96// so the number of bytes a read returns says nothing about which report actually arrived. The report
97// id, the first byte of the buffer, is what identifies it.
98static bool readHidReport(HANDLE hHidFile, uint8_t* buffer, uint32_t length, DWORD timeoutMs, DWORD* nBytes) {
99 OVERLAPPED overlapped = {};
100 DWORD read;
101 if (!ReadFile(hHidFile, buffer, length, &read, &overlapped)) {
102 // A synchronous failure -- the device was unplugged, the handle went stale, access was
103 // denied -- does not set up `overlapped` at all, so it still looks like `STATUS_SUCCESS`
104 // and asking for the result below would report success with zero bytes read.
105 if (GetLastError() != ERROR_IO_PENDING) {
106 return false;
107 }
108
109 if (!GetOverlappedResultEx(hHidFile, &overlapped, &read, timeoutMs, TRUE)) {
110 // A timeout leaves the read pending, and the driver writes into `buffer` and `overlapped` when
111 // it eventually completes -- both of which are gone by then: the buffer belongs to the caller
112 // and the OVERLAPPED sits on this frame. So the read is cancelled by its own OVERLAPPED rather
113 // than by handle (CancelIo would take every other read on the same handle with it), and the
114 // cancellation is waited for before returning.
115 CancelIoEx(hHidFile, &overlapped);
116 GetOverlappedResultEx(hHidFile, &overlapped, &read, INFINITE, FALSE);
117 return false;
118 }
119 }
120
121 *nBytes = read;
122 return true;
123}
124
125// The DualShock 4 sends a battery level in its state packet, but only in the extended report it
126// switches to when a program asks it to. Until then it sends the plain HID gamepad report, which
127// ends after the buttons.
128typedef enum FFDs4ReportId: uint8_t {
129 FF_DS4_REPORT_ID_USB_STATE = 0x01,
130 FF_DS4_REPORT_ID_BLUETOOTH_STATE_FIRST = 0x11,
131 FF_DS4_REPORT_ID_BLUETOOTH_STATE_LAST = 0x19,
132} FFDs4ReportId;
133
134// Offset of the battery level in the state packet, which is what the DualShock 4 documents and
135// what SDL reads: five pad bytes after the six IMU axes.
136#define FF_DS4_STATE_PACKET_BATTERY_OFFSET 29
137
138// The plain HID gamepad report is ten bytes long, report id included
139#define FF_DS4_NON_EXTENDED_REPORT_LENGTH 10
140
141// Reads the battery level out of one input report of a DualShock 4, and returns 0 when the report
142// carries none. `report[0]` is the report id and `reportLength` the size of the buffer.
143static uint8_t parseDs4Battery(const uint8_t* report, uint32_t reportLength) {
144 uint32_t batteryOffset;
145
146 if (report[0] == FF_DS4_REPORT_ID_USB_STATE) {
147 // A non-extended report stops after the buttons, so a non-zero byte past that can only come
148 // from an extended packet. A wired controller always sends the extended one, a Bluetooth one
149 // only once another program has enabled it.
150 bool extended = false;
151 for (uint32_t i = FF_DS4_NON_EXTENDED_REPORT_LENGTH; i < reportLength; ++i) {
152 if (report[i]) {
153 extended = true;
154 break;
155 }
156 }
157 if (!extended) {
158 return 0;
159 }
160 // On USB the state packet follows the report id directly
161 batteryOffset = 1 + FF_DS4_STATE_PACKET_BATTERY_OFFSET;
162 } else if (report[0] >= FF_DS4_REPORT_ID_BLUETOOTH_STATE_FIRST && report[0] <= FF_DS4_REPORT_ID_BLUETOOTH_STATE_LAST) {
163 // A Bluetooth state packet has two extra bytes in front, the first of which says whether the
164 // packet carries HID state at all
165 if (reportLength <= 1 || !(report[1] & 0x80)) {
166 return 0;
167 }
168 batteryOffset = 3 + FF_DS4_STATE_PACKET_BATTERY_OFFSET;
169 } else {
170 return 0;
171 }
172
173 if (batteryOffset >= reportLength) {
174 return 0;
175 }
176
177 uint8_t batteryInfo = report[batteryOffset];
178 uint8_t level = batteryInfo & 0x0f;
179 if ((batteryInfo & 0x10 /*charging?*/) && level > 11) {
180 return 0; // A charging controller reports 11 for a full battery; anything above is not a level
181 }
182
183 uint8_t battery = (uint8_t) (level * 10 + 5); // 0..11 maps onto the middle of eleven 10% steps
184 return battery > 100 ? 100 : battery;
185}
186
187// The DualSense uses one report id over USB and another over Bluetooth, and the state packet follows
188// the id directly in the first case and one byte later in the second. The official controller sends
189// SDL's PS5StatePacket_t; the shorter PS5StatePacketAlt_t belongs to third-party pads and is not read.
190typedef enum FFDualSenseReportId: uint8_t {
191 FF_DUALSENSE_REPORT_ID_USB_STATE = 0x01,
192 FF_DUALSENSE_REPORT_ID_BLUETOOTH_STATE = 0x31,
193} FFDualSenseReportId;
194
195// Offset of the battery level in PS5StatePacket_t, and the length of a whole USB state report
196#define FF_DUALSENSE_STATE_PACKET_BATTERY_OFFSET 52
197#define FF_DUALSENSE_USB_REPORT_LENGTH 64
198
199static uint8_t parseDualSenseBattery(const uint8_t* report, uint32_t reportLength) {
200 uint32_t batteryOffset;
201
202 if (report[0] == FF_DUALSENSE_REPORT_ID_USB_STATE) {
203 // Over Bluetooth the controller answers the same id with a short report that stops before the
204 // battery, so the USB report length is what tells the two apart
205 if (reportLength != FF_DUALSENSE_USB_REPORT_LENGTH) {
206 return 0;
207 }
208 batteryOffset = 1 + FF_DUALSENSE_STATE_PACKET_BATTERY_OFFSET;
209 } else if (report[0] == FF_DUALSENSE_REPORT_ID_BLUETOOTH_STATE) {
210 // The Bluetooth report has one extra byte in front of the state packet
211 batteryOffset = 2 + FF_DUALSENSE_STATE_PACKET_BATTERY_OFFSET;
212 } else {
213 return 0;
214 }
215
216 if (batteryOffset >= reportLength) {
217 return 0;
218 }
219
220 uint8_t batteryInfo = report[batteryOffset];
221 uint32_t status = (batteryInfo >> 4) & 0x0f;
222 if (status == 2) {
223 return 100; // Charged
224 }
225 if (status > 1) {
226 return 0; // Not a status the controller documents
227 }
228
229 uint32_t battery = (batteryInfo & 0x0f) * 10 + 5; // Discharging and charging share one scale
230 return (uint8_t) (battery > 100 ? 100 : battery);
231}
232
233// A Switch controller interleaves its full state report with subcommand replies, which carry no
234// battery at all. SDL skips those replies and waits for the next full report, and so does this,
235// inside the same one-second budget a single read of any other controller gets.
236typedef enum FFSwitchReportId: uint8_t {
237 FF_SWITCH_REPORT_ID_FULL_STATE_FIRST = 0x30,
238 FF_SWITCH_REPORT_ID_FULL_STATE_LAST = 0x31,
239} FFSwitchReportId;
240
241// The controller state packet starts one byte after the report id, and its second byte carries the
242// battery level in its top three bits and the charging flag in bit 4. Anything that is not a full
243// state report carries no battery and answers 0.
244#define FF_SWITCH_STATE_PACKET_BATTERY_OFFSET 2
245
246static uint8_t parseSwitchBattery(const uint8_t* report, uint32_t reportLength) {
247 if (report[0] < FF_SWITCH_REPORT_ID_FULL_STATE_FIRST || report[0] > FF_SWITCH_REPORT_ID_FULL_STATE_LAST) {
248 return 0;
249 }
250 if (reportLength <= FF_SWITCH_STATE_PACKET_BATTERY_OFFSET) {
251 return 0;
252 }
253
254 uint8_t batteryInfo = report[FF_SWITCH_STATE_PACKET_BATTERY_OFFSET];
255 uint32_t battery = ((batteryInfo & 0xE0) >> 4) * 100 / 8; // A level of 8 is a full battery
256 if (battery == 0) {
257 return 1; // Keep a drained controller apart from the "unknown" 0 the module uses
258 }
259 return (uint8_t) (battery > 100 ? 100 : battery);
260}
261
262// The longest run of reports without a full state report measured on a Pro Controller was eleven
263#define FF_SWITCH_BATTERY_MAX_READS 16
264
265static uint8_t detectSwitchBattery(HANDLE hHidFile, uint8_t* reportBuffer, uint32_t reportLength) {
266 const double deadline = ffTimeGetTick() + (double) FF_IO_TERM_RESP_WAIT_MS;
267
268 for (uint32_t i = 0; i < FF_SWITCH_BATTERY_MAX_READS; ++i) {
269 double remaining = deadline - ffTimeGetTick();
270 if (remaining < 1.) {
271 break;
272 }
273
274 DWORD nBytes;
275 // A report never arrives empty, so zero bytes means the read did not deliver one
276 if (!readHidReport(hHidFile, reportBuffer, reportLength, (DWORD) remaining, &nBytes) || !nBytes) {
277 break;
278 }
279
280 uint8_t battery = parseSwitchBattery(reportBuffer, reportLength);
281 if (battery) {
282 return battery;
283 }
284 }
285
286 return 0;
287}
288
289// Which of the battery layouts a controller uses, if any. A controller that is not listed here keeps
290// the module's "unknown" 0 without a single read being issued.
291typedef enum FFGamepadBatteryKind: uint8_t {
292 FF_GAMEPAD_BATTERY_NONE,
293 FF_GAMEPAD_BATTERY_DS4,
294 FF_GAMEPAD_BATTERY_DUALSENSE,
295 FF_GAMEPAD_BATTERY_SWITCH,
296} FFGamepadBatteryKind;
297
298static FFGamepadBatteryKind detectBatteryKind(uint32_t vendorId, uint32_t productId) {
299 switch (vendorId) {
300 case 0x054C: // Sony
301 switch (productId) {
302 case 0x0CE6: // DualSense
303 case 0x0DF2: // DualSense Edge
304 case 0x0E5F: // Access Controller
305 return FF_GAMEPAD_BATTERY_DUALSENSE;
306
307 case 0x05C4: // DualShock 4 Gen1
308 case 0x09CC: // DualShock 4 Gen2
309 case 0x0BA0: // DualShock 4 USB receiver
310 return FF_GAMEPAD_BATTERY_DS4;
311
312 default:
313 return FF_GAMEPAD_BATTERY_NONE;
314 }
315
316 // The Nintendo list is deliberately shorter than the name table above. The Charging Grip
317 // (0x200E) is a rail that charges Joy-Con and has no battery of its own, and the SNES
318 // online controller (0x2017) and the Switch 2 controllers (0x2066-0x2069) are ones SDL --
319 // which is what this parsing follows -- reports no battery for: `SDL_hidapi_switch2.c` has
320 // no power reporting at all, and the SNES entry is commented out of `controller_list.h`.
321 // Reading the Switch state report from them would be a guess, so they keep the module's
322 // "unknown".
323 case 0x057E: // Nintendo
324 switch (productId) {
325 case 0x2006: // Joy-Con L
326 case 0x2007: // Joy-Con R
327 case 0x2008: // Joy-Con pair
328 case 0x2009: // Pro Controller
329 return FF_GAMEPAD_BATTERY_SWITCH;
330
331 default:
332 return FF_GAMEPAD_BATTERY_NONE;
333 }
334
335 default:
336 return FF_GAMEPAD_BATTERY_NONE;
337 }
338}
339
340const char* ffDetectGamepad(FFlist* devices /* List of FFGamepadDevice */) {
341 UINT nDevices = 0;
342 if (GetRawInputDeviceList(nullptr, &nDevices, sizeof(RAWINPUTDEVICELIST))) {
343 FF_DEBUG("GetRawInputDeviceList(nullptr) failed: %s", ffDebugWin32Error(GetLastError()));
344 return "GetRawInputDeviceList(nullptr) failed";
345 }
346 if (nDevices == 0) {
347 FF_DEBUG("No HID devices found");
348 return "No HID devices found";
349 }
350 FF_AUTO_FREE RAWINPUTDEVICELIST* pRawInputDeviceList = (RAWINPUTDEVICELIST*) malloc(sizeof(RAWINPUTDEVICELIST) * nDevices);
351 if ((nDevices = GetRawInputDeviceList(pRawInputDeviceList, &nDevices, sizeof(RAWINPUTDEVICELIST))) == (UINT) -1) {
352 FF_DEBUG("GetRawInputDeviceList(pRawInputDeviceList) failed: %s", ffDebugWin32Error(GetLastError()));
353 return "GetRawInputDeviceList(pRawInputDeviceList) failed";
354 }
355
356 for (UINT i = 0; i < nDevices; ++i) {
357 if (pRawInputDeviceList[i].dwType != RIM_TYPEHID) {
358 continue;
359 }
360
361 HANDLE hDevice = pRawInputDeviceList[i].hDevice;
362
363 RID_DEVICE_INFO rdi;
364 UINT rdiSize = sizeof(rdi);
365 if (GetRawInputDeviceInfoW(hDevice, RIDI_DEVICEINFO, &rdi, &rdiSize) == (UINT) -1) {
366 continue;
367 }
368
369 if (rdi.hid.usUsagePage != 1 || (rdi.hid.usUsage != 4 /*Joystick*/ && rdi.hid.usUsage != 5 /*Gamepad*/)) {
370 continue;
371 }
372
373 WCHAR devName[MAX_PATH] = L"";
374 UINT nameSize = MAX_PATH;
375 if (GetRawInputDeviceInfoW(hDevice, RIDI_DEVICENAME, devName, &nameSize) == (UINT) -1) {
376 continue;
377 }
378
379 FFGamepadDevice* device = FF_LIST_ADD(FFGamepadDevice, *devices);
380 ffStrbufInit(&device->serial);
381 ffStrbufInit(&device->name);
382 device->battery = 0;
383
384 const char* knownGamepad = detectKnownDeviceName(rdi.hid.dwVendorId, rdi.hid.dwProductId);
385 if (knownGamepad) {
386 ffStrbufSetS(&device->name, knownGamepad);
387 }
388 FF_AUTO_CLOSE_FD HANDLE hHidFile = CreateFileW(devName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nullptr);
389 if (hHidFile == INVALID_HANDLE_VALUE) {
390 if (!knownGamepad) {
391 ffStrbufSetF(&device->name, "Unknown gamepad %04X-%04X", (unsigned) rdi.hid.dwVendorId, (unsigned) rdi.hid.dwProductId);
392 }
393 continue;
394 }
395
396 if (!knownGamepad) {
397 wchar_t displayName[126];
398 if (HidD_GetProductString(hHidFile, displayName, sizeof(displayName) /*in bytes*/)) {
399 wchar_t manufacturer[126];
400 if (HidD_GetManufacturerString(hHidFile, manufacturer, sizeof(manufacturer) /*in bytes*/)) {
401 ffStrbufSetWS(&device->name, manufacturer);
402 FF_STRBUF_AUTO_DESTROY displayNameStr = ffStrbufCreateWS(displayName);
403 ffStrbufAppendC(&device->name, ' ');
404 ffStrbufAppend(&device->name, &displayNameStr);
405 } else {
406 ffStrbufSetWS(&device->name, displayName);
407 }
408 }
409 }
410
411 wchar_t serialNumber[127] = L"";
412 if (HidD_GetSerialNumberString(hHidFile, serialNumber, sizeof(serialNumber) /*in bytes*/)) {
413 ffStrbufSetWS(&device->serial, serialNumber);
414 }
415
416 PHIDP_PREPARSED_DATA preparsedData = nullptr;
417 if (HidD_GetPreparsedData(hHidFile, &preparsedData)) {
418 HIDP_CAPS caps;
419 NTSTATUS capsResult = HidP_GetCaps(preparsedData, &caps);
420 HidD_FreePreparsedData(preparsedData);
421 if (!NT_SUCCESS(capsResult)) {
422 continue;
423 }
424
425 // Sony and Nintendo controllers report a battery level only in a report a program has to
426 // ask the controller for, so a controller that nothing else has opened reports none.
427 FFGamepadBatteryKind batteryKind = detectBatteryKind(rdi.hid.dwVendorId, rdi.hid.dwProductId);
428 if (batteryKind != FF_GAMEPAD_BATTERY_NONE) {
429 FF_AUTO_FREE uint8_t* reportBuffer = malloc(caps.InputReportByteLength);
430 DWORD nBytes;
431 switch (batteryKind) {
432 case FF_GAMEPAD_BATTERY_DS4:
433 // `reportBuffer` is uninitialised malloc() memory, so a read that delivered
434 // nothing must not be parsed
435 if (readHidReport(hHidFile, reportBuffer, caps.InputReportByteLength, FF_IO_TERM_RESP_WAIT_MS, &nBytes) && nBytes) {
436 device->battery = parseDs4Battery(reportBuffer, caps.InputReportByteLength);
437 }
438 break;
439 case FF_GAMEPAD_BATTERY_DUALSENSE:
440 if (readHidReport(hHidFile, reportBuffer, caps.InputReportByteLength, FF_IO_TERM_RESP_WAIT_MS, &nBytes) && nBytes) {
441 device->battery = parseDualSenseBattery(reportBuffer, caps.InputReportByteLength);
442 }
443 break;
444 case FF_GAMEPAD_BATTERY_SWITCH:
445 device->battery = detectSwitchBattery(hHidFile, reportBuffer, caps.InputReportByteLength);
446 break;
447 case FF_GAMEPAD_BATTERY_NONE:
448 break;
449 }
450 }
451 }
452 }
453
454 return nullptr;
455}