Ver Fonte

PicoPuck: XInput multi-controller (one Xbox pad per connected controller)

Xbox mode presented a single fixed XInput interface, so only one
controller ever appeared on the OS (macOS + Windows) even with two
connected. Make XInput dynamic like the emulated-HID modes:

- build_xinput_config(b, k) now emits k XInput pad interfaces (itf
  1..k, endpoint number 3+j) after the WebUSB vendor interface (itf 0),
  instead of a single hardcoded pad.
- Wire XInput into the shared dynamic mounter (usb_mount): enable it in
  Xbox mode and rebuild the XInput config from the connected count on
  re-enumeration; the count-encoding serial busts the host's cached
  config.
- xinput.c: per-USB-slot state array; each pad interface u is fed from
  bond slot g_usb_to_bond[u] and routes its rumble OUT back to the same
  slot, mirroring the emu HID path.

Matches OpenPuck's mode_xinput behaviour (one pad per connected
controller). HW-verify pending.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Jari há 2 meses atrás
pai
commit
a50efd64ad
3 ficheiros alterados com 148 adições e 106 exclusões
  1. 4 4
      PicoPuck/src/main.c
  2. 57 43
      PicoPuck/src/puck/xinput.c
  3. 87 59
      PicoPuck/src/usb/usb_descriptors.c

+ 4 - 4
PicoPuck/src/main.c

@@ -42,10 +42,10 @@ int main(void)
 	bool puck = mode_is_puck(settings_mode());
 	bool xinput = mode_is_xinput(settings_mode());
 
-	// HID emulated modes present one gamepad per CONNECTED controller, mounted /
-	// unmounted dynamically (no reboot) — same behaviour as OpenPuck. Puck and
-	// XInput modes keep their fixed interface layout.
-	if (!puck && !xinput)
+	// HID emulated modes AND XInput present one gamepad per CONNECTED controller,
+	// mounted / unmounted dynamically (no reboot) — same behaviour as OpenPuck, so
+	// two controllers show up as two pads. Only puck mode keeps a fixed layout.
+	if (!puck)
 		usb_mount_enable(true, PP_NSLOT);
 
 	// The user LED hangs off the CYW43 chip; this init also hands the radio to

+ 57 - 43
PicoPuck/src/puck/xinput.c

@@ -1,12 +1,12 @@
 // xinput.c — Xbox 360 wired (XInput) personality (see xinput.h).
 //
-// A custom TinyUSB class driver serves the vendor interface (0xFF/0x5D/0x01)
-// and its interrupt IN/OUT endpoints. The interface descriptor is emitted by
-// usb_descriptors.c; here we claim the endpoints in the driver's open()
-// callback, stream the 20-byte report from g_in, and decode host rumble on the
-// OUT endpoint. Single interface (the active controller) — PicoPuck presents one
-// emulated controller at a time, so the per-slot pool of the OpenPuck original
-// collapses to one state block here.
+// A custom TinyUSB class driver serves the XInput interfaces (0xFF/0x5D/0x01)
+// and their interrupt IN/OUT endpoints. The config descriptor (usb_descriptors.c)
+// exposes ONE pad interface per CONNECTED controller via the shared dynamic
+// mounter, so two controllers appear as two Xbox pads on the OS (matching
+// OpenPuck). Each USB pad interface u (itf u+1; vendor is itf 0) is fed from bond
+// slot g_usb_to_bond[u]; host rumble on that interface's OUT endpoint routes back
+// to the same bond slot.
 //
 // PicoPuck's main loop is cooperative (no separate usbd task), so IN transfers
 // are issued directly from xinput_task() rather than deferred to a SOF drain.
@@ -18,6 +18,7 @@
 #include "puck/relay.h"
 #include "sys/settings.h"
 #include "config/modes.h"
+#include "usb_mount.h"
 
 #include <string.h>
 #include "tusb.h"
@@ -34,28 +35,37 @@ enum {
 	XB_A = 0x1000, XB_B = 0x2000, XB_X = 0x4000, XB_Y = 0x8000
 };
 
+// One state block per USB pad interface (usbSlot u = descriptor interface u,
+// itf 1..k). u maps to bond slot g_usb_to_bond[u], exactly like the emu HID path.
 static struct {
 	uint8_t itf, ep_in, ep_out;
 	uint8_t in_buf[32], out_buf[32];
 	bool in_use;
-} g_xi;
+} g_xi[PP_NSLOT];
 
 // ---- custom class driver ---------------------------------------------------
-static void xi_init(void) { g_xi.in_use = false; }
+static void xi_init(void) { memset(g_xi, 0, sizeof(g_xi)); }
 static bool xi_deinit(void) { return true; }
 static void xi_reset(uint8_t rhport)
 {
 	(void)rhport;
-	g_xi.itf = g_xi.ep_in = g_xi.ep_out = 0;
-	g_xi.in_use = false;
+	// Cleared on every (re)enumeration; xi_open re-populates the mounted pads.
+	for (int u = 0; u < PP_NSLOT; u++) {
+		g_xi[u].itf = g_xi[u].ep_in = g_xi[u].ep_out = 0;
+		g_xi[u].in_use = false;
+	}
 }
 static uint16_t xi_open(uint8_t rhport, tusb_desc_interface_t const *itf,
 			uint16_t max_len)
 {
 	if (!(itf->bInterfaceClass == 0xFF && itf->bInterfaceSubClass == 0x5D &&
 	      itf->bInterfaceProtocol == 0x01))
-		return 0;  // not the XInput interface → let another driver claim it
-	g_xi.itf = itf->bInterfaceNumber;
+		return 0;  // not an XInput interface → let another driver claim it
+	// Vendor (WebUSB) is itf 0; pad interfaces are itf 1..k → usbSlot u = itf-1.
+	uint8_t u = (uint8_t)(itf->bInterfaceNumber - 1);
+	if (u >= PP_NSLOT)
+		return 0;
+	g_xi[u].itf = itf->bInterfaceNumber;
 	uint8_t const *p = (uint8_t const *)itf;
 	uint8_t const *end = p + max_len;
 	uint16_t used = itf->bLength;
@@ -67,17 +77,18 @@ static uint16_t xi_open(uint8_t rhport, tusb_desc_interface_t const *itf,
 			tusb_desc_endpoint_t const *ep = (tusb_desc_endpoint_t const *)p;
 			usbd_edpt_open(rhport, ep);
 			if (tu_edpt_dir(ep->bEndpointAddress) == TUSB_DIR_IN)
-				g_xi.ep_in = ep->bEndpointAddress;
+				g_xi[u].ep_in = ep->bEndpointAddress;
 			else
-				g_xi.ep_out = ep->bEndpointAddress;
+				g_xi[u].ep_out = ep->bEndpointAddress;
 			opened++;
 		}
 		used += blen;
 		p += blen;
 	}
-	g_xi.in_use = true;
-	if (g_xi.ep_out)  // arm OUT (rumble/LED)
-		usbd_edpt_xfer(rhport, g_xi.ep_out, g_xi.out_buf, sizeof(g_xi.out_buf));
+	g_xi[u].in_use = true;
+	if (g_xi[u].ep_out)  // arm OUT (rumble/LED)
+		usbd_edpt_xfer(rhport, g_xi[u].ep_out, g_xi[u].out_buf,
+			       sizeof(g_xi[u].out_buf));
 	return used;
 }
 static bool xi_ctrl(uint8_t rhport, uint8_t stage,
@@ -89,19 +100,21 @@ static bool xi_ctrl(uint8_t rhport, uint8_t stage,
 static bool xi_xfer(uint8_t rhport, uint8_t ep, xfer_result_t res, uint32_t n)
 {
 	(void)res;
-	if (!g_xi.in_use)
-		return true;
-	if (ep == g_xi.ep_out) {
+	for (int u = 0; u < PP_NSLOT; u++) {
+		if (!g_xi[u].in_use || ep != g_xi[u].ep_out)
+			continue;
 		// Rumble packet: [00][08][00][big][small][00][00][00]; LED [01][03][led].
-		if (n >= 5 && g_xi.out_buf[0] == 0x00 && g_xi.out_buf[1] == 0x08) {
-			int slot = -1;
-			for (int s = 0; s < PP_NSLOT; s++)
-				if (g_slot[s].connected) { slot = s; break; }
+		if (n >= 5 && g_xi[u].out_buf[0] == 0x00 &&
+		    g_xi[u].out_buf[1] == 0x08) {
+			int slot = (u < USB_MOUNT_NSLOT) ? g_usb_to_bond[u] : -1;
 			if (slot >= 0 && settings()->type[ET_XBOX].rumble)
-				puck_rumble(slot, (uint16_t)g_xi.out_buf[3] * 257u,
-					    (uint16_t)g_xi.out_buf[4] * 257u);
+				puck_rumble(slot,
+					    (uint16_t)g_xi[u].out_buf[3] * 257u,
+					    (uint16_t)g_xi[u].out_buf[4] * 257u);
 		}
-		usbd_edpt_xfer(rhport, g_xi.ep_out, g_xi.out_buf, sizeof(g_xi.out_buf));
+		usbd_edpt_xfer(rhport, g_xi[u].ep_out, g_xi[u].out_buf,
+			       sizeof(g_xi[u].out_buf));
+		break;
 	}
 	return true;
 }
@@ -149,13 +162,6 @@ static uint16_t code_to_xb(uint8_t c)
 	default: return 0;
 	}
 }
-static int active_slot(void)
-{
-	for (int s = 0; s < PP_NSLOT; s++)
-		if (g_slot[s].connected)
-			return s;
-	return 0;
-}
 static void xi_build(int slot, uint8_t *r)
 {
 	const pp_type_cfg_t *t = &settings()->type[ET_XBOX];
@@ -218,18 +224,26 @@ static void xi_build(int slot, uint8_t *r)
 
 void xinput_task(void)
 {
-	if (!g_xi.in_use || g_xi.ep_in == 0 || !tud_mounted())
+	if (!tud_mounted())
 		return;
 	static uint32_t last_ms;
 	uint32_t now = to_ms_since_boot(get_absolute_time());
-	if (now - last_ms < 4)  // ~250 Hz cap
+	if (now - last_ms < 4)  // ~250 Hz cap (per interface)
 		return;
-	if (usbd_edpt_busy(0, g_xi.ep_in))
-		return;  // a transfer is still in flight — newest state next cycle
 	last_ms = now;
-	xi_build(active_slot(), g_xi.in_buf);
-	if (usbd_edpt_claim(0, g_xi.ep_in)) {
-		if (!usbd_edpt_xfer(0, g_xi.ep_in, g_xi.in_buf, 20))
-			usbd_edpt_release(0, g_xi.ep_in);
+	// Stream every mounted pad from its bond slot (usbSlot u → g_usb_to_bond[u]).
+	for (int u = 0; u < PP_NSLOT; u++) {
+		if (!g_xi[u].in_use || g_xi[u].ep_in == 0)
+			continue;
+		if (usbd_edpt_busy(0, g_xi[u].ep_in))
+			continue;  // in flight — newest state next cycle
+		int slot = (u < USB_MOUNT_NSLOT) ? g_usb_to_bond[u] : -1;
+		if (slot < 0)
+			continue;
+		xi_build(slot, g_xi[u].in_buf);
+		if (usbd_edpt_claim(0, g_xi[u].ep_in)) {
+			if (!usbd_edpt_xfer(0, g_xi[u].ep_in, g_xi[u].in_buf, 20))
+				usbd_edpt_release(0, g_xi[u].ep_in);
+		}
 	}
 }

+ 87 - 59
PicoPuck/src/usb/usb_descriptors.c

@@ -135,25 +135,29 @@ static uint16_t build_emu_config(uint8_t *b, const emu_mode_t *emu, uint8_t nif)
 }
 
 // Build the XInput config: the WebUSB vendor interface FIRST (itf 0), so the
-// built-in vendor class driver claims it and — with only one vendor instance —
-// leaves the XInput interface (itf 1, class 0xFF/0x5D/0x01) for the custom
-// XInput app driver. Endpoints: vendor bulk 0x02/0x82; XInput interrupt IN
-// 0x83 / OUT 0x03. Returns total length.
-static uint16_t build_xinput_config(uint8_t *b)
+// built-in vendor class driver claims it, then `k` XInput interfaces (itf 1..k,
+// class 0xFF/0x5D/0x01) — ONE Xbox-360-wired pad per CONNECTED controller, so
+// multiple controllers each show up on the OS (matches OpenPuck's dynamic XInput
+// mounting; the old build presented a single fixed pad → only one controller
+// ever appeared). Endpoints: vendor bulk 0x02/0x82; XInput pad j uses endpoint
+// number 3+j (interrupt IN 0x83+j / OUT 0x03+j). Returns total length.
+static uint16_t build_xinput_config(uint8_t *b, uint8_t k)
 {
-	const uint16_t total = 9 + (9 + 7 + 7) + (9 + 17 + 7 + 7); // 72
+	if (k > PP_NSLOT)
+		k = PP_NSLOT;
+	const uint16_t total = 9 + (9 + 7 + 7) + k * (9 + 17 + 7 + 7);
 	uint8_t *p = b;
-	// configuration (2 interfaces)
+	// configuration (vendor + k pad interfaces)
 	*p++ = 9;
 	*p++ = TUSB_DESC_CONFIGURATION;
 	*p++ = (uint8_t)(total & 0xFF);
 	*p++ = (uint8_t)(total >> 8);
-	*p++ = 2;
+	*p++ = (uint8_t)(1 + k);
 	*p++ = 1;
 	*p++ = 0;
 	*p++ = 0x80;
 	*p++ = 250;
-	// vendor interface (itf 0) — WebUSB
+	// vendor interface (itf 0) — WebUSB, endpoints 0x02/0x82
 	*p++ = 9;
 	*p++ = TUSB_DESC_INTERFACE;
 	*p++ = 0;
@@ -177,50 +181,55 @@ static uint16_t build_xinput_config(uint8_t *b)
 	*p++ = 64;
 	*p++ = 0;
 	*p++ = 0;
-	// XInput interface (itf 1) — vendor class 0xFF/0x5D/0x01, 2 interrupt eps
-	*p++ = 9;
-	*p++ = TUSB_DESC_INTERFACE;
-	*p++ = 1;
-	*p++ = 0;
-	*p++ = 2;
-	*p++ = 0xFF;
-	*p++ = 0x5D;
-	*p++ = 0x01;
-	*p++ = 0;
-	// XInput "unknown" 0x21 descriptor (verbatim; bytes 6/13 = IN/OUT ep addr)
-	*p++ = 0x11;
-	*p++ = 0x21;
-	*p++ = 0x00;
-	*p++ = 0x01;
-	*p++ = 0x01;
-	*p++ = 0x25;
-	*p++ = 0x83;
-	*p++ = 0x14;
-	*p++ = 0x00;
-	*p++ = 0x00;
-	*p++ = 0x00;
-	*p++ = 0x00;
-	*p++ = 0x13;
-	*p++ = 0x03;
-	*p++ = 0x08;
-	*p++ = 0x00;
-	*p++ = 0x00;
-	// XInput IN endpoint (interrupt, 0x83, 1 ms)
-	*p++ = 7;
-	*p++ = TUSB_DESC_ENDPOINT;
-	*p++ = 0x83;
-	*p++ = 0x03;
-	*p++ = 0x20;
-	*p++ = 0;
-	*p++ = 1;
-	// XInput OUT endpoint (interrupt, 0x03, 8 ms)
-	*p++ = 7;
-	*p++ = TUSB_DESC_ENDPOINT;
-	*p++ = 0x03;
-	*p++ = 0x03;
-	*p++ = 0x20;
-	*p++ = 0;
-	*p++ = 8;
+	// k XInput interfaces (itf 1..k)
+	for (uint8_t j = 0; j < k; j++) {
+		uint8_t itfnum = (uint8_t)(1 + j);
+		uint8_t epin = (uint8_t)(0x83 + j), epout = (uint8_t)(0x03 + j);
+		// XInput interface — vendor class 0xFF/0x5D/0x01, 2 interrupt eps
+		*p++ = 9;
+		*p++ = TUSB_DESC_INTERFACE;
+		*p++ = itfnum;
+		*p++ = 0;
+		*p++ = 2;
+		*p++ = 0xFF;
+		*p++ = 0x5D;
+		*p++ = 0x01;
+		*p++ = 0;
+		// XInput "unknown" 0x21 descriptor (bytes [6]/[13] = IN/OUT ep addr)
+		*p++ = 0x11;
+		*p++ = 0x21;
+		*p++ = 0x00;
+		*p++ = 0x01;
+		*p++ = 0x01;
+		*p++ = 0x25;
+		*p++ = epin;
+		*p++ = 0x14;
+		*p++ = 0x00;
+		*p++ = 0x00;
+		*p++ = 0x00;
+		*p++ = 0x00;
+		*p++ = 0x13;
+		*p++ = epout;
+		*p++ = 0x08;
+		*p++ = 0x00;
+		*p++ = 0x00;
+		// IN endpoint (interrupt, 1 ms)
+		*p++ = 7;
+		*p++ = TUSB_DESC_ENDPOINT;
+		*p++ = epin;
+		*p++ = 0x03;
+		*p++ = 0x20;
+		*p++ = 0;
+		*p++ = 1;
+		// OUT endpoint (interrupt, 8 ms)
+		*p++ = 7;
+		*p++ = TUSB_DESC_ENDPOINT;
+		*p++ = epout;
+		*p++ = 0x03;
+		*p++ = 0x20;
+		*p++ = 0;
+		*p++ = 8;
+	}
 	return total;
 }
 
@@ -412,8 +421,14 @@ void usb_descriptors_init(void)
 		s_dev.idVendor = 0x045E;
 		s_dev.idProduct = 0x028E;
 		s_dev.bcdDevice = 0x0120;
-		build_xinput_config(s_emu_cfg);
+		// Start with NO pad interfaces (nothing connected at boot); the shared
+		// dynamic mounter grows the count to match the CONNECTED controllers and
+		// shrinks it lazily — same as the emulated-HID modes, so 2 controllers
+		// present 2 XInput pads.
+		s_emu_ifaces = 0;
+		build_xinput_config(s_emu_cfg, s_emu_ifaces);
 		s_vendor_itf = 0; // WebUSB vendor is itf 0 in the XInput config
+		rebuild_emu_serial();
 	} else if (s_emu) {
 		s_dev.idVendor = s_emu->vid;
 		s_dev.idProduct = s_emu->pid;
@@ -463,10 +478,18 @@ static void rebuild_emu_serial(void)
 // usb_reenumerate() while detached.
 void usb_descriptors_set_emu_ifaces(uint8_t k)
 {
-	if (!s_emu)
-		return;
 	if (k > PP_NSLOT)
 		k = PP_NSLOT;
+	if (s_xinput) {
+		// XInput: vendor stays itf 0, k pad interfaces follow. The serial encodes
+		// k so the host drops its cached config on every up/down re-enumeration.
+		s_emu_ifaces = k;
+		build_xinput_config(s_emu_cfg, k);
+		rebuild_emu_serial();
+		return;
+	}
+	if (!s_emu)
+		return;
 	s_emu_ifaces = k;
 	build_emu_config(s_emu_cfg, s_emu, k);
 	s_vendor_itf = k; // vendor is the interface after the k HIDs
@@ -588,13 +611,18 @@ const uint16_t *tud_descriptor_string_cb(uint8_t index, uint16_t langid)
 		const char *str;
 		switch (index) {
 		case STRID_MANUFACTURER:
-			str = s_emu ? "PicoPuck" : "Valve Software";
+			str = s_emu	 ? "PicoPuck" :
+			      s_xinput	 ? "Microsoft" :
+					   "Valve Software";
 			break;
 		case STRID_PRODUCT:
-			str = s_emu ? s_emu->product : "Steam Controller Puck";
+			str = s_emu    ? s_emu->product :
+			      s_xinput ? "Controller" :
+					 "Steam Controller Puck";
 			break;
 		case STRID_SERIAL:
-			str = s_emu ? s_emu_serial : g_usb_serial;
+			// XInput uses the count-encoding serial too (re-enum cache bust).
+			str = (s_emu || s_xinput) ? s_emu_serial : g_usb_serial;
 			break;
 		case STRID_VENDOR:
 			str = "PicoPuck WebUSB";