Pārlūkot izejas kodu

Fix DS4 descriptor, PS Create/Options remap, and PS motion axes

Three PlayStation-mode bugs (#223, #202, #118).

DS4 mode was dead on Windows/macOS: the report descriptor declared the
four stick usages with the HAT's item (Logical Max 7, Size 4, Count 1),
collapsing X/Y/Z/Rz into a single 4-bit field, so input report 0x01
declared 60 bytes while hidGyroBuild() sends 64. Hosts that parse the
descriptor (Windows HIDClass, macOS IOHIDManager) size every read from
the declared length -- shifting all field offsets -- and SDL's PS4
driver drops any USB packet under 64 bytes outright
(HIDAPI_DriverPS4_IsPacketValid), so the pad enumerated and never moved.
Linux hidraw returns the full 64 bytes, which is why it worked there.
Replaced with the genuine DualShock 4 descriptor.

Back-paddle/QAM codes 9 and 10 (the panel's "Create" and "Options", and
Back/Start, Minus/Plus elsewhere) were mapped to TB_VIEW and TB_QAM.
TB_VIEW is the START-side button, and no mode turns a TB_QAM bit into a
host button, so "Create" came out as Options and "Options" did nothing.
Now 9 -> TB_MENU, 10 -> TB_VIEW; this also fixes the same inversion for
a QAM remap in Xbox mode.

PS motion was wrong in two ways. The gyro used SDL's SC2 permutation
(X<-+x, Y<-+z, Z<--y) but the accel was passed through unpermuted,
putting gravity on PS Z instead of PS Y and giving the accel the
opposite handedness to the gyro -- so consumers that FUSE them (Cemu,
rpcs3, console motion) latched into a rotated solution while Steam and
Switch modes read correctly. Both now go through psImuPack(), which also
halves the accel into the PS pads' 8192-counts/g scale. The calibration
feature report's 2844/2844 gyro pair told hid-playstation/hid-sony (and
rpcs3) the gyro was 16x more sensitive than it is; SDL escaped that only
by rejecting the calibration as bad. 4096/256 lands the correct
raw/16 deg/s on every stack.

DS3-on-a-PC is not a firmware bug: our report matches both SDL's PS3
driver offsets and the generic descriptor parse. Windows has no Sixaxis
driver and SDL disables its PS3 HIDAPI driver there by default, so the
host applies a generic DirectInput mapping -- a genuine DualShock 3
misbehaves identically. Documented in the README, along with the fact
that PS4/PS5 consoles reject any pad that can't answer Sony's auth
challenge.

Compile- and format-checked; hardware verification pending.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Jari 2 mēneši atpakaļ
vecāks
revīzija
268dd50d09
5 mainītis faili ar 98 papildinājumiem un 48 dzēšanām
  1. 40 12
      OpenPuck/gamepad_util.cpp
  2. 23 2
      OpenPuck/gamepad_util.h
  3. 19 22
      OpenPuck/mode_hidgyro.cpp
  4. 2 12
      OpenPuck/mode_ps5.cpp
  5. 14 0
      README.md

+ 40 - 12
OpenPuck/gamepad_util.cpp

@@ -4,16 +4,26 @@
 
 void psNeutralCalib(uint8_t *buf)
 {
-	// Payload offsets = kernel buf[] index minus 1. buf[0..5] gyro bias stays zero (caller memset). Symmetric
-	// ranges -> non-zero divisors on the host.
-	le16(buf + 6, 2844);
-	le16(buf + 8, -2844); // gyro pitch +/-
-	le16(buf + 10, 2844);
-	le16(buf + 12, -2844); // gyro yaw +/-
-	le16(buf + 14, 2844);
-	le16(buf + 16, -2844); // gyro roll +/-
-	le16(buf + 18, 2844);
-	le16(buf + 20, 2844); // gyro speed +/- (sum != 0)
+	// Payload offsets = kernel buf[] index minus 1. buf[0..5] gyro bias stays zero (caller memset).
+	//
+	// The numbers are not arbitrary: every consumer turns them into a SENSITIVITY and sanity-checks it.
+	//  - gyro: hid-playstation/hid-sony scale by (speed_plus+speed_minus)*1024 / (|plus|+|minus|), and SDL
+	//    computes the same ratio and REJECTS the whole calibration unless it is within 50% of 64 (its
+	//    divisor). 4096 = 16 * 256 lands the ratio exactly on 64 -> reported deg/s = raw/16, which matches
+	//    the SC2's ~16.384 counts/dps. (The old 2844/2844 pair produced a ratio of 1024 = 16x too fast on
+	//    Linux/SteamOS, and was thrown out as "bad calibration" by SDL.)
+	//  - accel: scale is 2*8192/(plus-minus), so +/-8192 = unity, and psImuPack() already halves the SC2
+	//    accel into the pads' 8192-counts/g scale. Keep the range at +/-8192, NOT +/-16384: SDL computes
+	//    the span in an Sint16 (sAccXPlus - sAccXMinus), so a 32768-wide range overflows and the
+	//    calibration is discarded.
+	le16(buf + 6, 4096);
+	le16(buf + 8, -4096); // gyro pitch +/-
+	le16(buf + 10, 4096);
+	le16(buf + 12, -4096); // gyro yaw +/-
+	le16(buf + 14, 4096);
+	le16(buf + 16, -4096); // gyro roll +/-
+	le16(buf + 18, 256);
+	le16(buf + 20, 256); // gyro speed +/- (sum != 0)
 	le16(buf + 22, 8192);
 	le16(buf + 24, -8192); // accel X +/-
 	le16(buf + 26, 8192);
@@ -22,6 +32,22 @@ void psNeutralCalib(uint8_t *buf)
 	le16(buf + 32, -8192); // accel Z +/-
 }
 
+// Negate without the -32768 trap (its positive twin does not exist in int16).
+static inline int16_t neg16(int16_t v)
+{
+	return v == -32768 ? (int16_t)32767 : (int16_t)-v;
+}
+
+void psImuPack(uint8_t *out, const PuckInput &in)
+{
+	le16(out + 0, in.gx);
+	le16(out + 2, in.gz);
+	le16(out + 4, neg16(in.gy));
+	le16(out + 6, (int16_t)(in.ax / 2));
+	le16(out + 8, (int16_t)(in.az / 2));
+	le16(out + 10, (int16_t)(-(in.ay / 2)));
+}
+
 uint8_t swStick(int16_t v, bool invert)
 {
 	int32_t a = 0x80 + (invert ? -((int32_t)v >> 8) : ((int32_t)v >> 8));
@@ -182,11 +208,13 @@ static void psOrBackCode(uint32_t *b, uint8_t c)
 	case 8:
 		*b |= TB_R3;
 		break;
+	// 9 = Select-side (Create/Share), 10 = Start-side (Options) -- TB_VIEW/TB_MENU are named backwards
+	// with respect to the physical buttons, see tritonFromCode() and triton.h.
 	case 9:
-		*b |= TB_VIEW;
+		*b |= TB_MENU;
 		break;
 	case 10:
-		*b |= TB_QAM;
+		*b |= TB_VIEW;
 		break;
 	case 11:
 		*b |= TB_STEAM;

+ 23 - 2
OpenPuck/gamepad_util.h

@@ -25,6 +25,22 @@ static inline void le16(uint8_t *p, int16_t v)
 // and 0x05 (DualSense) -- identical layout; the caller returns size-1 and leaves trailing bytes zero.
 void psNeutralCalib(uint8_t *buf);
 
+// SC2 IMU -> PlayStation (DualSense / DS4) IMU, written as six little-endian int16s at out[0..11]:
+// gyro X/Y/Z then accel X/Y/Z (contiguous in both the DualSense report at payload 15 and the DS4 report at
+// payload 12).
+//
+// TWO things have to line up or motion reads wrong on the host:
+//  - FRAME. The host expects the axes SDL's own SC2 driver produces (SDL_hidapi_steam_triton.c):
+//    X <- +x, Y <- +z, Z <- -y, the SAME signed permutation for gyro AND accel. Using the SC2 frame
+//    verbatim for the accel (as this code used to) puts gravity on PS Z instead of PS Y and gives the accel
+//    the opposite handedness to the gyro -- so anything that FUSES them (Cemu, rpcs3, console motion) latches
+//    into a rotated solution. See mode_switch_pro's IMU comment for the same lesson on the Switch side.
+//  - SCALE. The SC2 accelerometer is +/-2g over the full int16 (~16384 counts/g); a DualSense/DS4 reports
+//    8192 counts/g (DS_ACCEL_RES_PER_G), so halve it. Gyro needs no scaling: SC2 is 2000 dps full-scale
+//    (~16.384 counts/dps) against the PS pads' 16 counts/dps -- 0.4% apart. psNeutralCalib() presents the
+//    matching calibration so hosts that trust it land on the same numbers as hosts that fall back to raw.
+void psImuPack(uint8_t *out, const PuckInput &in);
+
 // Steam trackpad s16 coords -> absolute touch surface. TOUCH_PAD_W is split into left/right halves so both
 // pads can co-exist as two contacts on a single DualSense/DS4 touchpad.
 #define TOUCH_PAD_W 1920u
@@ -74,10 +90,15 @@ static inline uint32_t tritonFromCode(uint8_t c)
 		return TB_L3;
 	case 8:
 		return TB_R3;
+	// 9 = the Select-side button (Xbox Back, Switch Minus, PlayStation Create/Share) and 10 = the
+	// Start-side one (Xbox Start, Switch Plus, PlayStation Options). TB_VIEW / TB_MENU are named
+	// BACKWARDS with respect to those positions (see triton.h), so 9 must produce TB_MENU. Mapping 10
+	// to TB_QAM was a dead end: no mode turns a TB_QAM bit into a host button, so "Options" did
+	// nothing while "Create" came out as Options.
 	case 9:
-		return TB_VIEW;
+		return TB_MENU;
 	case 10:
-		return TB_QAM;
+		return TB_VIEW;
 	case 11:
 		return TB_STEAM;
 	case 12:

+ 19 - 22
OpenPuck/mode_hidgyro.cpp

@@ -12,17 +12,24 @@
 
 HidGyroController g_hidGyroCtl;
 
+// Genuine DualShock 4 (054C:05C4) USB report descriptor. Input report 0x01 declares 63 payload bytes
+// (4 stick bytes, hat + 14 buttons + 6-bit counter, 2 trigger bytes, 54 vendor bytes) = 64 on the wire with
+// the id, matching what hidGyroBuild() sends. Do NOT collapse the stick block into the hat item: hosts that
+// parse the descriptor (Windows RawInput/HIDClass, macOS IOHIDManager) size every read from the DECLARED
+// length, so a short declaration both shifts all field offsets and makes SDL's PS4 driver drop the report
+// (HIDAPI_DriverPS4_IsPacketValid requires size >= 64) -- the whole pad reads as dead.
 static const uint8_t GYRO_HID_DESC[] = {
-	0x05, 0x01, 0x09, 0x05, 0xA1, 0x01, 0x85, 0x01, 0x09, 0x30, 0x09,
-	0x31, 0x09, 0x32, 0x09, 0x35, 0x15, 0x00, 0x25, 0x07, 0x75, 0x04,
-	0x95, 0x01, 0x81, 0x42, 0x65, 0x00, 0x05, 0x09, 0x19, 0x01, 0x29,
-	0x0E, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x0E, 0x81, 0x02,
-	0x06, 0x00, 0xFF, 0x09, 0x20, 0x75, 0x06, 0x95, 0x01, 0x15, 0x00,
-	0x25, 0x7F, 0x81, 0x02, 0x05, 0x01, 0x09, 0x33, 0x09, 0x34, 0x15,
-	0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x02, 0x81, 0x02, 0x06,
-	0x00, 0xFF, 0x09, 0x21, 0x95, 0x36, 0x81, 0x02, 0x85, 0x05, 0x09,
-	0x22, 0x95, 0x1F, 0x91, 0x02, 0x85, 0x04, 0x09, 0x23, 0x95, 0x24,
-	0xB1, 0x02, 0x85, 0x02, 0x09, 0x24, 0x95, 0x24, 0xB1, 0x02, 0xC0
+	0x05, 0x01, 0x09, 0x05, 0xA1, 0x01, 0x85, 0x01, 0x09, 0x30, 0x09, 0x31,
+	0x09, 0x32, 0x09, 0x35, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95,
+	0x04, 0x81, 0x02, 0x09, 0x39, 0x15, 0x00, 0x25, 0x07, 0x35, 0x00, 0x46,
+	0x3B, 0x01, 0x65, 0x14, 0x75, 0x04, 0x95, 0x01, 0x81, 0x42, 0x65, 0x00,
+	0x05, 0x09, 0x19, 0x01, 0x29, 0x0E, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01,
+	0x95, 0x0E, 0x81, 0x02, 0x06, 0x00, 0xFF, 0x09, 0x20, 0x75, 0x06, 0x95,
+	0x01, 0x15, 0x00, 0x25, 0x7F, 0x81, 0x02, 0x05, 0x01, 0x09, 0x33, 0x09,
+	0x34, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x02, 0x81, 0x02,
+	0x06, 0x00, 0xFF, 0x09, 0x21, 0x95, 0x36, 0x81, 0x02, 0x85, 0x05, 0x09,
+	0x22, 0x95, 0x1F, 0x91, 0x02, 0x85, 0x04, 0x09, 0x23, 0x95, 0x24, 0xB1,
+	0x02, 0x85, 0x02, 0x09, 0x24, 0x95, 0x24, 0xB1, 0x02, 0xC0
 };
 #define DS4_TOUCH_H 942
 #define DS4_STATUS_USB 0x1B // cable + level 11 (full)
@@ -164,18 +171,8 @@ static void hidGyroBuild(uint8_t usbSlot, uint8_t slot, uint8_t out[63])
 		 ((b & TB_STEAM) ? 0x01 : 0);
 	out[7] = g_in[slot].lt;
 	out[8] = g_in[slot].rt;
-	out[12] = g_in[slot].gx & 0xFF;
-	out[13] = g_in[slot].gx >> 8;
-	out[14] = g_in[slot].gz & 0xFF;
-	out[15] = g_in[slot].gz >> 8;
-	out[16] = (-g_in[slot].gy) & 0xFF;
-	out[17] = (-g_in[slot].gy) >> 8;
-	out[18] = g_in[slot].ax & 0xFF;
-	out[19] = g_in[slot].ax >> 8;
-	out[20] = g_in[slot].ay & 0xFF;
-	out[21] = g_in[slot].ay >> 8;
-	out[22] = g_in[slot].az & 0xFF;
-	out[23] = g_in[slot].az >> 8;
+	// gyro X/Y/Z then accel X/Y/Z, 6 x le16 (hid-sony: rd[13..] / rd[19..])
+	psImuPack(out + 12, g_in[slot]);
 	out[29] = DS4_STATUS_USB;
 	if (lTouch || rTouch) {
 		uint16_t tlx, tly, trx, trry;

+ 2 - 12
OpenPuck/mode_ps5.cpp

@@ -186,18 +186,8 @@ static void ps5Build(uint8_t usbSlot, uint8_t slot, uint8_t out[63])
 	out[9] = ((b & TB_STEAM) ? 0x01 : 0) |
 		 ((b & TB_TOUCH || b & TB_LPADC || b & TB_RPADC) ? 0x02 : 0) |
 		 ((b & TB_MUTE) ? 0x04 : 0);
-	out[15] = g_in[slot].gx & 0xFF;
-	out[16] = g_in[slot].gx >> 8;
-	out[17] = g_in[slot].gz & 0xFF;
-	out[18] = g_in[slot].gz >> 8;
-	out[19] = (-g_in[slot].gy) & 0xFF;
-	out[20] = (-g_in[slot].gy) >> 8;
-	out[21] = g_in[slot].ax & 0xFF;
-	out[22] = g_in[slot].ax >> 8;
-	out[23] = g_in[slot].ay & 0xFF;
-	out[24] = g_in[slot].ay >> 8;
-	out[25] = g_in[slot].az & 0xFF;
-	out[26] = g_in[slot].az >> 8;
+	// gyro X/Y/Z then accel X/Y/Z, 6 x le16 (hid-playstation: dualsense_input_report gyro[]/accel[])
+	psImuPack(out + 15, g_in[slot]);
 	uint16_t tlx, tly, trx, trry;
 	steamPadsToTouch(b, PS5_TOUCH_H, g_in[slot].lpx, g_in[slot].lpy,
 			 g_in[slot].rpx, g_in[slot].rpy, &tlx, &tly, &trx,

+ 14 - 0
README.md

@@ -44,6 +44,20 @@ Similarly you can hold all 4 back buttons and press Y to switch (teehee) over to
 | WebUSB panel → mode 11 | DirectInput (flight/space sims) | Every axis at once, as two DirectInput joysticks |
 | WebUSB panel → mode 12 | SInput (SDL-native) | Sticks + analog triggers + gyro + both trackpads + battery |
 
+**PlayStation modes.** Modes 5 (DualSense) and 6 (DS4) also come in "game/clean" variants — panel modes 7 and
+8 — which drop the wake-mouse and WebUSB interfaces so the device enumerates as *nothing but* a PlayStation
+pad, which is what titles that classify controllers by their interface list want. A few host-side caveats:
+
+* **PS4 / PS5 consoles reject them.** Those consoles only accept pads that pass Sony's authentication
+  challenge, which needs a real auth chip; the DS4/DS5 modes are for PCs (and PC emulators). On a PS4 the
+  "Mouse connected" pop-up you see with mode 6 is the puck's wake-mouse interface — mode 8 has no mouse, but
+  the pad still won't authenticate.
+* **PS3 mode is for the PS3** (and for Linux, where `hid-sony` drives it out of the box). Windows and macOS
+  have no built-in Sixaxis driver, and SDL disables its own PS3 HIDAPI driver on Windows by default, so the
+  host falls back to a *generic* DirectInput mapping and the buttons come out scrambled — exactly as a genuine
+  DualShock 3 does. Install [DsHidMini](https://github.com/nefarius/DsHidMini) (or Sony's `sixaxis.sys`), or
+  set `SDL_JOYSTICK_HIDAPI_PS3=1`, if you want DS3 mode on a PC; otherwise use mode 5/6/7/8 there.
+
 **DirectInput mode** exists because Steam Input funnels everything through XInput, so only a handful of the
 controller's analog inputs can be live at once — a problem for flight and space sims, which bind axes through
 DirectInput. DirectInput itself caps a device at 8 axes, so this mode presents the controller as **two**