master ohmu/fastfetch-openbsd / src / detection / gamepad / gamepad_windows.c
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}