Files
Sub 41bece1b31 padbridge 1.3.0: rumble in PS4 games, Switch Pro Controller, View + Menu as the touchpad (#6)
* Rumble research: log what the console asks of each virtual pad

scePadVirtualDeviceGetRemoteSetting read every 16 ms per live pad and
logged when it changes (at most 20 lines a second). On the PS5 with a
virtual DualSense: [0] update flag, [5..7] light bar RGB (Hades asked
for ba 01 13), [56] a user is assigned. [3] and [4], the motors on a
PS4, stayed 0 in Hades and Hollow Knight (PS5 games). Not for release.

* Rumble: what the console asks of a virtual pad reaches XInput pads

scePadVirtualDeviceGetRemoteSetting, read every 16 ms per live pad,
holds the motors as a DualShock 4 output report does: [3] the large
motor, [4] the small one (seen in Crash Bandicoot N. Sane Trilogy, a PS4
game: 23/47/6b/8e ramps on [3], 3f/8c on [4]). PS5 games (Hades, Hollow
Knight) send DualSense haptics as audio instead and leave them at 0.

- usbpad_rumble(): XInput rumble (00 08 00 <large> <small> 00 00 00) on
  the OUT endpoint, started without waiting; a newer value replaces one
  not sent yet.
- Rumble stopped when the console turns a pad off and when padbridge
  stops, so no controller is left rumbling.
- The research log now covers all 256 bytes.

Tested on the PS5: the Ultimate 2C on its dongle in XInput mode rumbles
on Crash's TNT/Nitro boxes and deaths.

* View + Menu as the touchpad on XInput pads; research logs

XInput pads have no touchpad and no spare button for it: the Ultimate 2C
sends no L4/R4 in XInput mode, and its Star and Mapping buttons never
reach the console in either mode (checked on raw reports). Pressing View
and Menu together is now the touchpad click on XInput pads.

- src/combo.c: View or Menu alone is held back for 60 ms, so the console
  does not see a Create press on the way to the touchpad; held longer
  they go through as they are, and a shorter tap is sent when it ends.
  The touchpad stays until both are released. tests/test_combo.c.
- The settings page shows what the virtual pad is given (the touchpad
  lit, not View and Menu).
- Research only, to be trimmed before a release: raw XInput reports are
  logged when they change.

The 2C receiver in DInput mode does not rumble: report 1 (8 bytes) and
the 8BitDo-style 05 report, by interrupt OUT and by SET_REPORT, are
accepted and ignored; 8BitDo documents rumble for XInput only.

Tested on the PS5: View + Menu lights the touchpad, each alone stays
Create or Options; XInput and DInput.

* padbridge 1.3.0: rumble in PS4 games, View + Menu as the touchpad

- Rumble: the large and small motors a game asks of the virtual
  DualSense (scePadVirtualDeviceGetRemoteSetting [3], [4]) go to
  controllers in XInput mode as XInput rumble. PS4 games fill them; PS5
  games turn rumble into DualSense haptics inside the console and leave
  them at 0. The log says when a controller's rumble starts and stops,
  and whether that controller can rumble.
- The research logging (all remote setting bytes, raw XInput reports)
  is gone.
- The log line for a device padbridge does not read says so plainly
  ("not read by padbridge"): a DualSense on its cable is one.
- README: a Vibration section, the 2C's modes with what each gives,
  Star and Mapping, View + Menu in the button map, the 60 ms wait.
- Version 1.3.0, CHANGELOG.

Tested on the PS5: the Ultimate 2C in XInput rumbles in Crash Bandicoot
N. Sane Trilogy; View + Menu lights the touchpad.

* Switch Pro Controller over USB: the adapter's Switch mode, with rumble

The 8BitDo USB Wireless Adapter 2 passes rumble on in XInput mode to
PlayStation controllers only. In its Switch mode it is a Switch Pro
Controller (057e:2009) and passes rumble to Switch-type controllers (a
GameSir G8 in Switch mode, Switch Pro, Joy-Con). As documented by
dekuNukem's Switch reverse engineering notes and done by SDL:

- usbdesc: 057e:2009's HID interface is USBPAD_SWITCH (the choice now
  takes the product id).
- usbpad: at connection the USB handshake (80 02, 80 03, 80 02, 80 04)
  and subcommands on report 01: full input report 0x30, vibration on,
  player 1's light; each with a 100 ms wait and three tries. Without it
  the pad only sends a status and enumerates again every few seconds.
- mapswitch.c: report 0x30 (and 0x21) by position, as on a DualSense:
  B cross, A circle, Y square, X triangle, ZL/ZR as full L2/R2, Capture
  as the touchpad; sticks centred on the first report, reach learnt.
  Rumble as SDL encodes it (its tables, zlib licence), report 0x10, sent
  again every 50 ms while on.
- Fixed before it shipped: the mode name looked up past the end of its
  table for the new kind and crashed; the lookup is now guarded.
- tests/test_switch.c.

Tested on the PS5: the adapter locked in Switch mode with a GameSir G8
(Switch mode): setup answered in 150 ms; buttons, sticks, Capture as
the touchpad; rumble in Crash Bandicoot N. Sane Trilogy. The G8 sends
its buttons by label, so it needs the Nintendo layout switch.

* README and CHANGELOG: the adapter's Switch mode, rumble per controller, credits

* README, CHANGELOG: a DualSense through the adapter rumbles (tested)

* Switch rumble: felt on third-party pads too

Crash Bandicoot's spin asks the small motor alone at 63 (25%). Through
the adapter's Switch mode the GameSir G8 felt nothing: the small motor
went to the high band, which it hardly feels, and 25% is below what its
motor starts at. Now, for Switch Pro pads only:

- the low band takes the stronger of the two motors;
- any rumble is lifted to start at 100 (about 40%), up to 255
  (switch_rumble_boost).

Tested on the PS5: the G8 through the adapter in Switch mode rumbles
lightly on each spin, and harder on boxes and enemies.

* README: the GameSir G8 through the adapter (its Switch mode, Nintendo layout, rumble)
2026-10-23 11:40:36 -03:00

178 lines
7.2 KiB
C

/* Interface choice and the XInput mapping. Runs on the PC.
*
* The 310a descriptor is the one in issue #2 (padbridge-probe on firmware 13.00):
* the Ultimate 2C receiver in XInput mode. padbridge 0.2.1 read its keyboard. */
#include "usbdesc.h"
#include "usbpad.h"
#include <stdio.h>
#include <string.h>
static int fails;
#define CHECK(cond, ...) do { if (!(cond)) { printf("FAIL " __VA_ARGS__); printf("\n"); fails++; } } while (0)
/* configuration, interface, HID, endpoint descriptors */
#define CFG(total, n) 9, 0x02, (total) & 0xFF, (total) >> 8, n, 1, 0, 0xA0, 0xFA
#define IFACE(i, n, c, s, p) 9, 0x04, i, 0, n, c, s, p, 0
#define HID 9, 0x21, 0x11, 0x01, 0, 1, 0x22, 0x40, 0
#define EP(a, mps, iv) 7, 0x05, a, 0x03, (mps) & 0xFF, (mps) >> 8, iv
static const unsigned char desc_310a[] = {
CFG(9 + 9 + 7 + 7 + 9 + 9 + 7 + 9 + 9 + 7 + 7, 3),
IFACE(0, 2, 0xFF, 0x5D, 0x01), EP(0x84, 32, 1), EP(0x05, 32, 1),
IFACE(1, 1, 0x03, 0x01, 0x01), HID, EP(0x82, 16, 8),
IFACE(2, 2, 0x03, 0x00, 0x00), HID, EP(0x83, 64, 1), EP(0x06, 64, 1),
};
/* The receiver as 0.2.1 reads it: one HID interface. */
static const unsigned char desc_hid[] = {
CFG(9 + 9 + 9 + 7 + 7, 1),
IFACE(0, 2, 0x03, 0x00, 0x00), HID, EP(0x84, 64, 1), EP(0x03, 64, 1),
};
/* A boot keyboard and nothing else. */
static const unsigned char desc_kbd[] = {
CFG(9 + 9 + 9 + 7, 1),
IFACE(0, 1, 0x03, 0x01, 0x01), HID, EP(0x81, 8, 8),
};
static void test_choose(void)
{
usbdesc_pick p;
CHECK(usbdesc_choose(desc_310a, sizeof desc_310a, VID_8BITDO, 0, &p) == USBPAD_XINPUT, "310a: not XInput");
CHECK(p.iface == 0 && p.ep_in == 0x84 && p.mps_in == 32, "310a: in %#x/%u iface %u", p.ep_in, p.mps_in, p.iface);
CHECK(p.ep_out == 0x05 && p.mps_out == 32, "310a: out %#x/%u", p.ep_out, p.mps_out);
CHECK(usbdesc_choose(desc_310a, sizeof desc_310a, 0x045E, 0, &p) == USBPAD_XINPUT, "XInput of another vendor");
CHECK(usbdesc_choose(desc_hid, sizeof desc_hid, VID_8BITDO, 0, &p) == USBPAD_8BITDO, "8BitDo HID: not chosen");
CHECK(p.ep_in == 0x84 && p.mps_in == 64, "8BitDo HID: in %#x/%u", p.ep_in, p.mps_in);
CHECK(usbdesc_choose(desc_hid, sizeof desc_hid, 0x1234, 0, &p) == USBPAD_NONE, "HID of another vendor taken");
CHECK(usbdesc_choose(desc_kbd, sizeof desc_kbd, VID_8BITDO, 0, &p) == USBPAD_NONE, "boot keyboard taken");
CHECK(usbdesc_choose(desc_310a, 20, VID_8BITDO, 0, &p) == USBPAD_NONE, "truncated descriptor taken");
/* the boot keyboard beside the pad is to be hidden from the console */
usbdesc_choose(desc_310a, sizeof desc_310a, VID_8BITDO, 0, &p);
CHECK(p.hide == (1u << 1), "310a: hide %#x, want interface 1", (unsigned)p.hide);
usbdesc_choose(desc_hid, sizeof desc_hid, VID_8BITDO, 0, &p);
CHECK(p.hide == 0, "301c: hide %#x", (unsigned)p.hide);
}
static void test_names(void)
{
const char *n;
n = usbdesc_known_name(0x2DC8, 0x3105, "8BitDo");
CHECK(n && !strcmp(n, "8BitDo USB Wireless Adapter 2"), "3105: %s", n ? n : "(none)");
n = usbdesc_known_name(0x2DC8, 0x3106, "8BitDo");
CHECK(n && !strcmp(n, "8BitDo USB Wireless Adapter 2"), "3106: %s", n ? n : "(none)");
n = usbdesc_known_name(0x045E, 0x028E, "8BitDo");
CHECK(n && !strcmp(n, "8BitDo USB Wireless Adapter 2"), "028e from 8BitDo: %s", n ? n : "(none)");
CHECK(!usbdesc_known_name(0x045E, 0x028E, "Microsoft"), "a real Xbox 360 pad renamed");
CHECK(!usbdesc_known_name(0x2DC8, 0x301C, "8BitDo"), "the 2C dongle renamed");
}
/* Xbox 360 report: 00 14, buttons, LT, RT, LX, LY, RX, RY (int16, y up) */
static void xreport(unsigned char r[20], unsigned b, int lt, int rt, int lx, int ly, int rx, int ry)
{
int i;
for (i = 0; i < 20; i++) r[i] = 0;
r[0] = 0x00;
r[1] = 0x14;
r[2] = (unsigned char)b;
r[3] = (unsigned char)(b >> 8);
r[4] = (unsigned char)lt;
r[5] = (unsigned char)rt;
r[6] = (unsigned char)lx; r[7] = (unsigned char)(lx >> 8);
r[8] = (unsigned char)ly; r[9] = (unsigned char)(ly >> 8);
r[10] = (unsigned char)rx; r[11] = (unsigned char)(rx >> 8);
r[12] = (unsigned char)ry; r[13] = (unsigned char)(ry >> 8);
}
static void xexpect(const char *what, unsigned b, uint32_t want)
{
unsigned char r[20];
pad_state st;
xreport(r, b, 0, 0, 0, 0, 0, 0);
if (!usbpad_parse_xinput(r, 20, &st)) {
printf("FAIL %s: not parsed\n", what);
fails++;
} else if (st.buttons != want) {
printf("FAIL %s: buttons %#x, want %#x\n", what, (unsigned)st.buttons, (unsigned)want);
fails++;
}
}
static void test_xinput(void)
{
unsigned char r[20];
pad_state st;
xexpect("idle", 0, 0);
xexpect("up", 0x0001, PAD_UP);
xexpect("down", 0x0002, PAD_DOWN);
xexpect("left", 0x0004, PAD_LEFT);
xexpect("right", 0x0008, PAD_RIGHT);
xexpect("Start", 0x0010, PAD_OPTIONS);
xexpect("Back", 0x0020, PAD_CREATE);
xexpect("L3", 0x0040, PAD_L3);
xexpect("R3", 0x0080, PAD_R3);
xexpect("LB", 0x0100, PAD_L1);
xexpect("RB", 0x0200, PAD_R1);
xexpect("Guide", 0x0400, PAD_PS);
xexpect("A", 0x1000, PAD_CROSS);
xexpect("B", 0x2000, PAD_CIRCLE);
xexpect("X", 0x4000, PAD_SQUARE);
xexpect("Y", 0x8000, PAD_TRIANGLE);
xreport(r, 0, 0, 0, 0, 0, 0, 0);
usbpad_parse_xinput(r, 20, &st);
CHECK(st.lx == 128 && st.ly == 127 && st.rx == 128 && st.ry == 127, "centre %u %u %u %u", st.lx, st.ly, st.rx, st.ry);
xreport(r, 0, 255, 30, -32768, 32767, 32767, -32768);
usbpad_parse_xinput(r, 20, &st);
CHECK(st.lx == 0 && st.ly == 0, "left stick up-left: %u %u", st.lx, st.ly);
CHECK(st.rx == 255 && st.ry == 255, "right stick down-right: %u %u", st.rx, st.ry);
CHECK(st.l2 == 255 && st.r2 == 30, "triggers %u %u", st.l2, st.r2);
CHECK((st.buttons & (PAD_L2 | PAD_R2)) == (PAD_L2 | PAD_R2), "trigger buttons %#x", (unsigned)st.buttons);
xreport(r, 0, 20, 0, 0, 0, 0, 0);
usbpad_parse_xinput(r, 20, &st);
CHECK(!(st.buttons & PAD_L2), "light LT pressed L2");
r[0] = 0x01; r[1] = 0x03; /* LED status, not input */
CHECK(!usbpad_parse_xinput(r, 3, &st), "LED status taken as input");
xreport(r, 0, 0, 0, 0, 0, 0, 0);
CHECK(!usbpad_parse_xinput(r, 13, &st), "short report taken");
}
static void test_string(void)
{
/* string descriptor: length, type 3, UTF-16LE */
static const unsigned char name[] = { 22, 3, '8', 0, 'B', 0, 'i', 0, 't', 0, 'D', 0, 'o', 0, ' ', 0,
'2', 0, 'C', 0, ' ', 0 };
static const unsigned char odd[] = { 8, 3, 'A', 0, 0xE9, 0, 0x3A, 0x26 };
char s[64];
CHECK(usbdesc_string(name, sizeof name, s, sizeof s) && !strcmp(s, "8BitDo 2C"), "string \"%s\"", s);
CHECK(usbdesc_string(odd, sizeof odd, s, sizeof s) && !strcmp(s, "A??"), "non-ASCII \"%s\"", s);
CHECK(usbdesc_string(name, sizeof name, s, 5) && !strcmp(s, "8Bit"), "cut \"%s\"", s);
CHECK(!usbdesc_string(name, 1, s, sizeof s), "1 byte taken");
CHECK(!usbdesc_string((const unsigned char[]){ 4, 2, 'A', 0 }, 4, s, sizeof s), "not a string taken");
CHECK(!usbdesc_string((const unsigned char[]){ 4, 3, ' ', 0 }, 4, s, sizeof s), "blank taken");
}
int main(void)
{
test_string();
test_choose();
test_names();
test_xinput();
printf("%s (%d failure(s))\n", fails ? "FAILED" : "all passed", fails);
return fails != 0;
}