This commit is contained in:
ItsDeidara committed 2026-06-07 21:14:44 -04:00
1 parent ad345113e3
commit c1cb19042d
8 files changed
+5850 -2

No files matched your search

+30
View File
@@ -0,0 +1,30 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# Ghost-Control: USB HID controller → virtual DualSense on PS5
PS5_HOST ?= ps5
PORT ?= 9021
TARGET := ghost-control-ps5.elf
ifndef PS5_PAYLOAD_SDK
$(error PS5_PAYLOAD_SDK is not set)
endif
include $(PS5_PAYLOAD_SDK)/toolchain/prospero.mk
CFLAGS += -D__PROSPERO__ -Wall -Wextra -g -O2 -fPIC -fno-stack-protector
LDFLAGS += -lScePad -lSceUserService -lpthread -ldl
SRC := gc_main.c shellui_pad.c
.PHONY: all clean deploy
all: $(TARGET)
$(TARGET): $(SRC)
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
clean:
rm -f $(TARGET)
deploy: $(TARGET)
nc -w 5 $(PS5_HOST) $(PORT) < $(TARGET)
+267
View File
@@ -0,0 +1,267 @@
# Nintendo Switch Pro Controller USB Protocol Research
This document captures the complete research into making the 8BitDo Ultimate 2 (Nintendo Switch Pro Controller mode) work on PS5 via raw USB FS access. Derived from 12 versions of `usb_handshake_probe` run on the PS5.
---
## Device Identification
| Property | Value |
|----------|-------|
| VID:PID (Nintendo mode) | `0x057e:0x2009` |
| VID:PID (Native mode) | `0x2dc8:0x310b` |
| PS5 device node | `/dev/ugen2.2` |
| USB bus | `usbus2`, depth=1 port=2 speed=FS |
| Kernel driver (pre-detach) | `usb_hid0` |
| IN endpoint | `ep=0x81`, wMaxPacketSize=64, bInterval=1ms |
| OUT endpoint | `ep=0x02`, wMaxPacketSize=64 (**NOTE: 0x02 not 0x01 like genuine Nintendo**) |
---
## PS5 USB Access Method
Direct kernel HID access via `usb_hid0` is blocked (`EPERM`). The working path:
```c
USB_IFACE_DRIVER_DETACH(iface=0) // detach usb_hid0
USB_FS_INIT(ep_index_max=N) // allocate N endpoint slots
USB_FS_OPEN(ep_no=0x81, ep_index=0) // IN endpoint
USB_FS_OPEN(ep_no=0x02, ep_index=1) // OUT endpoint
USB_FS_START(ep_index=0) // start interrupt IN transfer
USB_FS_COMPLETE(ep_index=0) // block until data
```
### Why Two-Pass Init
The IN endpoint (0x81) cannot be opened on the first try in some states. A "pass 1" is required:
```c
// Pass 1: unlock endpoint
FS_INIT(1 slot) → DETACH → FS_OPEN(ep=0x81) → FS_UNINIT → close()
// Pass 2: actual session
open() → DETACH → FS_INIT(2 slots) → FS_OPEN IN(0x81) + OUT(0x02)
```
Pass 1 does nothing functionally — it just exercises the endpoint to unlock it for pass 2.
---
## Critical Finding: Subcmd 0x03 Must Be Sent LAST
**Wrong order (pipe breaks):**
```
[80 02] → [80 04] → subcmd 0x03 0x30 ← USB pipe break, controller disconnects
```
**Correct order (works):**
```
[80 02] → [80 04] ← USB handshake only
respond to 0x81 handshake ← controller responds with 0x81 0x01 / 0x81 0x02
[80 04] again ← handshake ack
subcmd 0x40 0x01 ← enable IMU
subcmd 0x48 0x01 ← enable vibration
subcmd 0x30 0x01 ← set player LED
subcmd 0x03 0x30 ← set input report mode (LAST)
```
Sending `subcmd 0x03` first causes a USB pipe break (`EBUSY` on `USB_FS_COMPLETE`) and the controller enters a reconnect loop that requires 2–3 additional handshake rounds to recover. With the correct order, `subcmd 0x03` is accepted cleanly and streaming starts immediately.
---
## Complete Initialization Sequence
### Phase 1: Two-Pass USB FS Setup
```c
// Pass 1 (unlock)
fd1 = open("/dev/ugen2.2", O_RDWR);
USB_FS_INIT(fd1, ep_index_max=1);
USB_IFACE_DRIVER_DETACH(fd1, iface=0);
USB_FS_OPEN(fd1, ep_index=0, ep_no=0x81, max_bufsize=64);
USB_FS_UNINIT(fd1);
close(fd1);
usleep(30000);
// Pass 2 (working session)
fd = open("/dev/ugen2.2", O_RDWR);
USB_IFACE_DRIVER_DETACH(fd, iface=0);
USB_FS_INIT(fd, ep_index_max=2);
USB_FS_OPEN(fd, ep_index=0, ep_no=0x81, max_bufsize=64); // IN
USB_FS_OPEN(fd, ep_index=1, ep_no=0x02, max_bufsize=64); // OUT
```
### Phase 2: Initial USB Handshake (Blind)
```c
send_out([0x80, 0x02]); // handshake start
usleep(30000);
send_out([0x80, 0x04]); // disable USB timeout
usleep(50000);
```
### Phase 3: Controller-Initiated Handshake Response
The controller sends `0x81` handshake packets. Respond **within ~200ms or the controller gives up:**
```
Controller sends: 0x81 0x01 → Host responds: [0x80, 0x02]
Controller sends: 0x81 0x02 → Host responds: [0x80, 0x04]
```
### Phase 4: Subcmd Init Sequence (MUST be in this order)
```c
send_subcmd(timer=1, subcmd=0x40, data=0x01); // enable IMU
usleep(50000);
send_subcmd(timer=2, subcmd=0x48, data=0x01); // enable vibration
usleep(50000);
send_subcmd(timer=3, subcmd=0x30, data=0x01); // set player LED
usleep(50000);
send_subcmd(timer=4, subcmd=0x03, data=0x30); // input mode = full push (LAST)
```
Subcmd packet format (12 bytes):
```c
uint8_t sc[12] = {0};
sc[0] = 0x01; // output report ID
sc[1] = timer; // 4-bit counter, must increment each packet
// sc[2..9] = neutral rumble = {0,1,0x40,0x40,0,1,0x40,0x40}
sc[10] = subcmd;
sc[11] = data;
```
### Phase 5: Read Loop
After `subcmd 0x03`, the controller immediately sends streaming data:
```c
while (1) {
USB_FS_START(ep_index=0);
USB_FS_COMPLETE(ep_index=0, timeout=200ms);
// process pkt[0..63]
}
```
---
## Streaming Packet Format
After the init sequence, the controller sends `0x00`-prefixed packets at ~60Hz:
```
Byte 0: 0x00 — always 0x00 (report ID stripped by 8BitDo firmware)
Byte 1: timer — 8-bit counter, increments ~6-8 units per frame
Byte 2: 0x70 — battery/connection (0x70 = USB, full charge)
Byte 3: right_btns — Y X B A SR SL R ZR (bits 0-7)
Byte 4: misc_btns — Minus Plus R3 L3 Home Capture (bits 0-5)
Byte 5: left_btns — Down Up Right Left SR SL L ZL (bits 0-7)
Bytes 6-8: left stick (12-bit packed, center ~0x800)
Bytes 9-11: right stick (12-bit packed, center ~0x7FF)
Byte 12: vibration status (0x0b = idle)
Bytes 13-47: IMU data (3 × 6-axis samples, when IMU enabled)
```
**Note:** Genuine Nintendo Switch Pro Controllers send `0x30` as byte 0 (the report ID). The 8BitDo sends `0x00`. The rest of the layout is identical.
### Stick Decoding (12-bit packed)
```c
uint16_t lx = buf[6] | ((buf[7] & 0x0F) << 8); // 0–4095, center ~2048
uint16_t ly = (buf[7] >> 4) | ((uint16_t)buf[8] << 4); // 0–4095, center ~2047
uint16_t rx = buf[9] | ((buf[10] & 0x0F) << 8);
uint16_t ry = (buf[10] >> 4) | ((uint16_t)buf[11] << 4);
// Map to 0–255 for ScePadData:
uint8_t x = (uint8_t)((v * 255u) / 4095u);
```
### Button Bit Mapping
```
buf[3] — right face buttons:
bit 0: Y → PS5 Square
bit 1: X → PS5 Triangle
bit 2: B → PS5 Cross
bit 3: A → PS5 Circle
bit 6: R → PS5 R1
bit 7: ZR → PS5 R2
buf[4] — shared buttons:
bit 0: Minus → PS5 Create/Share
bit 1: Plus → PS5 Options
bit 2: R3 → PS5 R3
bit 3: L3 → PS5 L3
bit 4: Home → PS5 PS button
buf[5] — left buttons:
bit 0: Down → PS5 Down
bit 1: Up → PS5 Up
bit 2: Right → PS5 Right
bit 3: Left → PS5 Left
bit 6: L → PS5 L1
bit 7: ZL → PS5 L2
```
---
## Reconnect Handling
On controller unplug/replug:
- `USB_FS_START` or `USB_FS_COMPLETE` returns `ENXIO` or `ENOTTY`
- Close all endpoints, `USB_FS_UNINIT`, `close(fd)`
- Re-scan `ugen` paths for the device (20s timeout)
- Run the full two-pass init + handshake sequence again
On reconnect without full power cycle:
- First packet may be all-zeros (`00 00 00 00...`) — discard it
- Then `0x81` handshake packets appear — run Phase 3 and 4 again
---
## USB FS COMPLETE Polling
`USB_FS_COMPLETE` is non-blocking. Poll at 50ms intervals for up to `timeout_ms + 300ms`:
```c
int max_polls = (int)((timeout_ms + 300) / 50) + 1;
for (int w = 0; w < max_polls; w++) {
if (ioctl(fd, USB_FS_COMPLETE, &co) == 0) break;
if (errno != EBUSY) { /* error */ break; }
usleep(50000);
}
```
**Do not** use 5-retry × 10ms — the USB transfer timeout is 200ms so you'll stop polling before the transfer can complete, leaving the endpoint stuck in `EBUSY`.
---
## Things That Do Not Work
| Approach | Result |
|----------|--------|
| `USB_GET_REPORT_DESC` | `errno=25 ENOTTY` |
| `read(fd, buf, 64)` without DETACH | `errno=1 EPERM` |
| `libSceHidControl` | Controller not auto-registered; ShellCore not initialized |
| `libSceUsbd` | Not loaded in SceShellCore or SceShellUI |
| Soft-reinit after `[80 04]` | Controller goes silent (wrong approach) |
| Sending `subcmd 0x03` first | USB pipe break → reconnect loop |
| Responding to `0x81 0x02` without subcmds | Controller silent after `[80 04]` |
---
## Research Probe History
Probes run on PS5 (`usb_handshake_probe v1` through `v12`) at port 6986, reports fetched from FTP `/data/ghostpad/`:
| Version | Key Discovery |
|---------|--------------|
| v1 | Confirmed soft-reinit after subcmd 0x03 pipe-break works |
| v2–v3 | Fixed COMPLETE polling (50ms intervals needed, not 10ms) |
| v4–v5 | Identified full USB bus-reset after `[80 04]` in second handshake |
| v6 | Second subcmd 0x03 also causes bus-reset — infinite loop |
| v7–v8 | Discovered controller gives up if host takes >200ms to respond to 0x81 |
| v9 | Tight 200ms loop — controller responds within timing window |
| v10 | Added IMU/vibration subcmds — still no streaming |
| v11 | **BREAKTHROUGH**: subcmd 0x03 sent LAST → immediate 0x30 streaming |
| v12 | Full 64-byte packet dump — confirmed format, verified stick decode |
+89 -2
View File
@@ -1,2 +1,89 @@
# Ghostcontrol - PS5 USB Controller Patcher
A PS5 payload to allow other controllers to work via the front USB port
# Ghostcontrol — by StonedModder
Use third-party USB controllers on PS5. Reads USB HID input from a plugged-in controller and injects it into a virtual DualSense via the PS5's `scePadVirtualDeviceInsertData` path (Ghostpad VDI path).
**Tested controller:** 8BitDo Ultimate 2 in Nintendo Switch Pro Controller mode (VID=0x057e PID=0x2009)
---
## Features
- 60Hz input streaming from USB HID controller → virtual DualSense
- PS5 notifications: startup, controller connect/disconnect, detected controller type
- User assignment: virtual DualSense is bound to the foreground user on startup
- Full button mapping: face buttons, triggers, sticks, dpad, L3/R3, PS button
- Auto-reconnect on controller unplug/replug
---
## Requirements
- PS5 with kernel exploit (tested on jailbroken PS5)
- [ps5-payload-sdk](https://github.com/ps5-payload-dev/sdk)
- USB controller — see supported list below
---
## Supported Controllers
| Controller | Mode | VID:PID | Status |
|-----------|------|---------|--------|
| 8BitDo Ultimate 2 | Nintendo Switch Pro | 057e:2009 | ✅ Working |
| 8BitDo Ultimate 2 | Native | 2dc8:310b | Untested |
See `othercontrollersGuide.md` for adding new controllers.
---
## Build
```sh
export PS5_PAYLOAD_SDK=/path/to/ps5-payload-sdk
make clean all
```
Output: `ghost-control-ps5.elf`
## Deploy
```sh
# Deploy to PS5 (replace IP)
nc -w 5 192.168.1.xxx 9021 < ghost-control-ps5.elf
```
Or set `PS5_HOST` in your environment:
```sh
make deploy PS5_HOST=192.168.1.xxx
```
---
## How It Works
1. **VDA**: Creates a virtual DualSense via `scePadVirtualDeviceAddDevice(type=3)`
2. **klog capture**: Monitors klogsrv TCP to detect the `DEVICE_ADDED` event and get the device handle
3. **force_bind**: Binds the virtual device to the foreground user via ShellUI MBus IPC
4. **USB HID thread**: Detaches `usb_hid0` from the controller, opens raw USB FS endpoints, runs the Nintendo Switch Pro Controller USB handshake, then reads 60Hz input reports
5. **VDI inject**: Parses HID reports into `ScePadData` and calls `scePadVirtualDeviceInsertData` at 60Hz
See `ProControllerResearch.md` for the full research documentation on the USB HID protocol.
---
## Files
| File | Description |
|------|-------------|
| `gc_main.c` | Main payload — VDA, VDI, USB HID thread, button parsing |
| `shellui_pad.c` | ShellUI PT_ATTACH helper for force_bind via MBus |
| `shellui_pad.h` | Header for shellui_pad |
| `Makefile` | Build system |
| `ghost-control-ps5.elf` | Pre-compiled payload (deploy directly) |
| `ProControllerResearch.md` | Full USB protocol research for Nintendo Switch Pro Controller |
| `othercontrollersGuide.md` | Guide for adding other USB HID controllers |
---
## License
GPL-3.0-or-later
+1085
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
+306
View File
@@ -0,0 +1,306 @@
# Adding Other USB HID Controllers
This guide provides the baseline for adding support for controllers other than the 8BitDo in Nintendo Switch Pro mode. Everything below is derived from what was confirmed working on PS5.
---
## Architecture Overview
The payload has two separate paths:
```
USB HID Controller → gc_main USB thread → ScePadData → scePadVirtualDeviceInsertData
(parse) (VDI inject)
```
To add a new controller you only need to:
1. Add VID:PID detection in `ugen_find_target()`
2. Add an initialization sequence in `usb_hid_thread()`
3. Add a parser function that converts the controller's HID report to `ScePadData`
---
## Step 1: Identify the Controller
Plug the controller into the PS5 USB port and watch klog:
```
ugen2.2: <ManufacturerName, ProductName>(VID=0xXXXX PID=0xXXXX) at usbus2
```
Also check:
- Which `ugen` path it appears on (`/dev/ugen2.2`, `/dev/ugen1.2`, etc.)
- What kernel driver claims it (`usb_hid0`, `usb_hid1`, etc.)
Add the VID:PID to `gc_main.c`:
```c
#define VID_MYCTLR 0xXXXXu
#define PID_MYCTLR 0xXXXXu
```
And add detection to `ugen_find_target()`:
```c
if ((di.udi_vendorNo == VID_MYCTLR && di.udi_productNo == PID_MYCTLR)) {
if (out_vid) *out_vid = di.udi_vendorNo;
if (out_pid) *out_pid = di.udi_productNo;
if (out_path) *out_path = UGEN_PATHS[i];
ok = 1;
}
```
---
## Step 2: Find the Endpoints
After `USB_IFACE_DRIVER_DETACH` and `USB_FS_INIT`, try opening common HID endpoints:
```c
// Standard HID: IN=0x81, OUT=0x01 (or 0x02 for some — check your device)
USB_FS_OPEN(ep_no=0x81, ep_index=0, max_bufsize=64) // IN
USB_FS_OPEN(ep_no=0x01, ep_index=1, max_bufsize=64) // OUT (try 0x02 if this fails)
```
Log `max_packet_length` from the open result — it tells you the expected HID report size.
**Common endpoint configurations:**
| Controller Family | IN | OUT | Report Size |
|------------------|----|-----|------------|
| Nintendo Pro / 8BitDo Nintendo mode | 0x81 | 0x02 | 64 bytes |
| Xbox 360 (wired) | 0x81 | 0x02 | 20 bytes (IN), 8 bytes (OUT) |
| Xbox One / Series (wired) | 0x81 | 0x02 | 18 bytes (IN) |
| Generic HID gamepad | 0x81 | 0x01 | varies |
---
## Step 3: Determine Init Sequence
### Native HID Controllers (no init needed)
Most generic HID gamepads (XInput-incompatible mode, standard HID descriptor) start sending reports immediately after you open the IN endpoint — no OUT commands needed.
```c
// No init: just open IN and start reading
USB_FS_OPEN(ep_no=0x81, ep_index=0);
// → start reading, data comes immediately
```
Check if `USB_FS_COMPLETE` returns data within 500ms with no OUT sent. If yes: native HID, skip all init.
### Xbox 360 (Wired)
Xbox 360 wired controller uses XInput protocol. It requires an init packet on the OUT endpoint before it sends input reports:
```c
// No handshake needed — just send one OUT "enable" packet
uint8_t enable[] = {0x01, 0x03, 0x0E}; // XInput enable command (3 bytes)
send_out(fd, &eps[1], enable, sizeof(enable));
// Controller then sends 0x00 input reports at ~125Hz
```
Xbox 360 has no USB mode switching — no handshake like Nintendo. See report format below.
### Xbox One / Series (Wired)
Xbox One protocol (XInput HID variant) requires:
```c
// Enable input reports
uint8_t enable[] = {0x05, 0x20, 0x00, 0x01, 0x00};
send_out(fd, &eps[1], enable, sizeof(enable));
// Optionally: rumble keepalive every 5s (or controller disconnects from idle)
```
### Generic / Unknown
1. Open IN endpoint (no OUT init)
2. Read for 2 seconds
3. If no data: try sending `[0x00]` or `[0x01]` on OUT endpoint
4. Capture and log whatever comes in — inspect the HID report manually
---
## Step 4: Parse the HID Report
### Identify Report Structure
Log the raw bytes while pressing each button individually:
```c
gp_log("PKT: %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x\n",
buf[0],buf[1],buf[2],buf[3],buf[4],buf[5],buf[6],buf[7],
buf[8],buf[9],buf[10],buf[11]);
```
Press each button, record which bit changes. Common patterns:
```
Buttons: one byte per group, each button = 1 bit
Sticks: one byte per axis (0–255 range, center=127–128)
OR two bytes per axis (little-endian 16-bit, center=~32767)
Triggers:one byte per trigger (0–255)
OR in button byte (binary on/off)
Dpad: one nibble (0-8 hat switch) OR four bits in a button byte
```
### Xbox 360 Report Format (20 bytes, report ID = 0x00)
```
Byte 0: 0x00 — report ID
Byte 1: 0x14 — length (20)
Byte 2: buttons lo — A B X Y LB RB Back Start (bits 4-7: 0000)
Byte 3: buttons hi — LS RS Guide (bits 0-2)
Byte 4: left trigger (0-255)
Byte 5: right trigger (0-255)
Byte 6-7: left stick X (int16 LE, center=0, range -32768..32767)
Byte 8-9: left stick Y (int16 LE, center=0, inverted: up=positive)
Byte 10-11: right stick X
Byte 12-13: right stick Y
Byte 14-19: (padding)
```
Dpad is in `buf[2]` bits 0-3:
```
bit 0: Up, bit 1: Down, bit 2: Left, bit 3: Right
```
### Xbox One Report Format (18 bytes, report ID = 0x20)
```
Byte 0: 0x20 — report ID
Byte 1: 0x00 — sequence
Byte 2: buttons lo — A B X Y LB RB View Menu (bits 0-7)
Byte 3: buttons hi — LS RS (bits 0-1), dpad (bits 2-5)
Byte 4: left trigger (0-255, maps to 0-1023 internally)
Byte 5: right trigger (0-255)
Byte 6-7: left stick X (int16 LE, center=0)
Byte 8-9: left stick Y (int16 LE)
Byte 10-11: right stick X
Byte 12-13: right stick Y
Byte 14-17: (varies by firmware)
```
---
## Step 5: Map to ScePadData
Add a parse function in `gc_main.c` following this pattern:
```c
static void parse_xbox360(const uint8_t *b, ScePadData *o) {
uint8_t bl = b[2], bh = b[3];
uint8_t lt = b[4], rt = b[5];
int16_t lx = (int16_t)(b[6] | (b[7] << 8));
int16_t ly = (int16_t)(b[8] | (b[9] << 8));
int16_t rx = (int16_t)(b[10] | (b[11] << 8));
int16_t ry = (int16_t)(b[12] | (b[13] << 8));
uint32_t btn = 0;
/* Face buttons */
if (bl & 0x10) btn |= SCE_PAD_BUTTON_CROSS; // A
if (bl & 0x20) btn |= SCE_PAD_BUTTON_CIRCLE; // B
if (bl & 0x40) btn |= SCE_PAD_BUTTON_SQUARE; // X
if (bl & 0x80) btn |= SCE_PAD_BUTTON_TRIANGLE; // Y
/* Shoulder */
if (bh & 0x01) btn |= SCE_PAD_BUTTON_L1; // LB
if (bh & 0x02) btn |= SCE_PAD_BUTTON_R1; // RB
/* Triggers (analog) */
o->analogButtons.l2 = lt;
o->analogButtons.r2 = rt;
if (lt > 128) btn |= SCE_PAD_BUTTON_L2;
if (rt > 128) btn |= SCE_PAD_BUTTON_R2;
/* Sticks */
if (bh & 0x20) btn |= SCE_PAD_BUTTON_L3;
if (bh & 0x40) btn |= SCE_PAD_BUTTON_R3;
/* Menu */
if (bl & 0x04) btn |= SCE_PAD_BUTTON_CREATE; // Back
if (bl & 0x08) btn |= SCE_PAD_BUTTON_OPTIONS; // Start
/* Dpad */
if (bl & 0x01) btn |= SCE_PAD_BUTTON_UP;
if (bl & 0x02) btn |= SCE_PAD_BUTTON_DOWN;
if (bl & 0x04) btn |= SCE_PAD_BUTTON_LEFT;
if (bl & 0x08) btn |= SCE_PAD_BUTTON_RIGHT;
o->buttons = btn;
/* Map int16 (-32768..32767) → uint8 (0..255), center=127 */
o->leftStick.x = (uint8_t)((lx + 32768) >> 8);
o->leftStick.y = (uint8_t)(255 - ((ly + 32768) >> 8)); // Y axis inverted
o->rightStick.x = (uint8_t)((rx + 32768) >> 8);
o->rightStick.y = (uint8_t)(255 - ((ry + 32768) >> 8));
o->connected = 1;
o->quat.w = 1.0f;
}
```
Then in the main read loop, add a branch:
```c
} else if (pid == PID_XBOX360 && rid == 0x00 && len >= 14) {
parse_xbox360(buf, &pad);
if (g_vdi_ready) inject_pad(&pad);
```
---
## Step 6: Add Init to USB Thread
In `usb_hid_thread()`, after the two-pass setup, add a branch for your controller:
```c
if (pid == PID_SWITCH && out_opened) {
// 8BitDo Nintendo mode — full handshake (existing)
...
} else if (pid == PID_XBOX360 && out_opened) {
uint8_t enable[] = {0x01, 0x03, 0x0E};
usb_fs_send_out_report(fd, &eps[1], enable, sizeof(enable), "xbox360_enable");
hs_state = HS_STREAMING; // Xbox 360 has no handshake
} else {
// Generic HID: no init, just start reading
hs_state = HS_STREAMING;
}
```
---
## Button Mapping Reference
All PS5 `SCE_PAD_BUTTON_*` values used in `gc_main.c`:
```c
SCE_PAD_BUTTON_L3 = 0x00000002
SCE_PAD_BUTTON_R3 = 0x00000004
SCE_PAD_BUTTON_OPTIONS = 0x00000008 // Options / Start / Menu
SCE_PAD_BUTTON_UP = 0x00000010
SCE_PAD_BUTTON_RIGHT = 0x00000020
SCE_PAD_BUTTON_DOWN = 0x00000040
SCE_PAD_BUTTON_LEFT = 0x00000080
SCE_PAD_BUTTON_L2 = 0x00000100
SCE_PAD_BUTTON_R2 = 0x00000200
SCE_PAD_BUTTON_L1 = 0x00000400
SCE_PAD_BUTTON_R1 = 0x00000800
SCE_PAD_BUTTON_TRIANGLE = 0x00001000
SCE_PAD_BUTTON_CIRCLE = 0x00002000
SCE_PAD_BUTTON_CROSS = 0x00004000
SCE_PAD_BUTTON_SQUARE = 0x00008000
SCE_PAD_BUTTON_CREATE = 0x00010000 // Share / Back / View
SCE_PAD_BUTTON_PS = 0x00010000 // PS button / Guide / Home
SCE_PAD_BUTTON_TOUCH_PAD = 0x00100000
```
`ScePadData` stick fields: `uint8_t x, y` — range 0–255, center = 127–128.
`ScePadData` trigger fields: `uint8_t l2, r2` — range 0–255.
---
## Checklist for a New Controller
- [ ] VID:PID added to constants and `ugen_find_target()`
- [ ] Controller type notification added
- [ ] Init sequence determined (native / Xbox / custom handshake)
- [ ] IN endpoint confirmed working (`max_packet_length` logged)
- [ ] Raw bytes logged per button press — report format mapped
- [ ] Parser function written and tested
- [ ] Read loop handles new `pid` and `rid` correctly
- [ ] Reconnect path tested (unplug and replug while payload running)
+3932
View File
File diff suppressed because it is too large. Load diff
+141
View File
@@ -0,0 +1,141 @@
/* SPDX-License-Identifier: GPL-3.0-or-later
* shellui_pad.h — System process pad injection interface
*
* Injects through the confirmed SceShellCore/SceShellUI path: SceShellCore
* creates the virtual controller, and SceShellUI performs Mbus binding plus
* VDI diagnostics.
*
* Key safety rule: PT_ATTACH is attempted FIRST. Nothing is written to the
* target process until attach succeeds, preventing the PS5 freeze caused by
* leaving an INT3 in live code when attach fails.
*/
#pragma once
#include <stdint.h>
/* Persistent status log exported by main.c. It mirrors critical logs to
* /data/ghostpad/ghostpad_status.log so diagnostics survive when /dev/klog
* or the TCP klog bridge are unavailable. */
void ghostpad_status_log(const char *fmt, ...);
void ghostpad_status_log_reset(void);
/* Maximum size of ScePadData we forward to the stub (padded for alignment) */
#define SHELLUI_PAD_DATA_SIZE 256
/* Shared state between our process and the stub running in SceShellUI.
* We write pad_data + increment seq; the stub polls seq and calls InsertData. */
typedef struct {
/* ── function pointers resolved in the target process's address space ── */
int32_t (*fp_gethandle)(int32_t userId, int32_t type, int32_t index);
int32_t (*fp_gethandle_ext)(int32_t userId, int32_t type, int32_t index, uint64_t a4, uint64_t a5, uint64_t a6);
int32_t (*fp_open)(int32_t userId, int32_t type, int32_t index, void *param);
int32_t (*fp_open_ext)(int32_t userId, int32_t type, int32_t index, void *param, uint64_t a5, uint64_t a6);
int32_t (*fp_open_ext2)(int32_t userId, int32_t type, int32_t index, void *param, uint64_t a5, uint64_t a6);
int32_t (*fp_insert)(int32_t handle, const void *data); /* unused legacy slot */
int32_t (*fp_vdi)(int32_t handle, const void *data); /* scePadVirtualDeviceInsertData */
int32_t (*fp_vda)(void *param, int32_t type); /* scePadVirtualDeviceAddDevice */
int32_t (*fp_del)(int32_t handle); /* scePadVirtualDeviceDeleteDevice */
int32_t (*fp_setpriv)(int32_t privilege); /* scePadSetProcessPrivilege */
int32_t (*fp_setloginuser)(int32_t loginUserNumber); /* scePadSetLoginUserNumber */
int32_t (*fp_setusernumber)(int32_t userNumber); /* scePadSetUserNumber */
int32_t (*fp_setfocus)(int32_t focus, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6); /* scePadSetProcessFocus */
void (*fp_usleep)(unsigned int usec);
/* ── parameters ── */
int32_t userId;
int32_t virtual_device_type;
/* ── shared pad state (written by our process, read by stub) ── */
volatile uint32_t seq; /* increment to trigger injection */
uint8_t pad_data[SHELLUI_PAD_DATA_SIZE]; /* ScePadData bytes */
/* ── stub status (written by stub, read by our process) ── */
volatile int32_t pad_handle; /* set by stub after scePadGetHandle */
volatile int32_t ready; /* 1=running, -1=error */
volatile int32_t stop; /* set to 1 to ask stub to exit */
/* ── diagnostic: first-iteration return codes from each probe ── */
volatile int32_t rc_log[16];
} ShellUiPadArgs;
/* Inject the pad stub into SceShellUI.
*
* On success returns 0 and sets:
* *out_shellui_pid — PID of SceShellUI
* *out_args_kaddr — address of ShellUiPadArgs inside SceShellUI's VA space
* (use mdbg_copyin to update pad_data + seq each frame)
*
* Returns -1 on failure (see klog for details).
*/
int shellui_pad_inject(int32_t userId, int force_virtual_vda,
int32_t virtual_device_type, pid_t *out_shellui_pid,
intptr_t *out_args_kaddr);
/* Update pad data inside SceShellUI's stub (call at up to 60 Hz).
* pad_data must point to a ScePadData struct (at most SHELLUI_PAD_DATA_SIZE bytes).
* This uses mdbg_copyin — no ptrace attach needed.
*/
int shellui_pad_update(pid_t shellui_pid, intptr_t args_kaddr,
const void *pad_data, uint32_t pad_data_len);
int shellui_pad_direct_usable(pid_t shellui_pid, intptr_t args_kaddr);
int shellui_pad_direct_mode(pid_t shellui_pid, intptr_t args_kaddr);
int shellui_pad_direct_adopt_vdi_handle(pid_t shellui_pid, intptr_t args_kaddr,
int32_t vdi_handle);
int shellui_pad_direct_recover(pid_t shellui_pid, intptr_t args_kaddr, int32_t userId, int32_t altUserId);
void shellui_pad_direct_get_last_status(int32_t *stage, int64_t *value);
/* Re-launch stub thread with pre-set VDI handle — one-time pt_call, then
* use shellui_pad_update() (mdbg_copyin) for all packets. No per-packet
* ptrace freeze. Requires ptrace authid set permanently in caller. */
int shellui_pad_relaunch_stub_with_handle(int32_t handle);
int shellui_pad_direct_begin(pid_t shellui_pid, intptr_t args_kaddr);
int shellui_pad_direct_send(pid_t shellui_pid, intptr_t args_kaddr,
const void *pad_data, uint32_t pad_data_len);
void shellui_pad_direct_end(pid_t shellui_pid, intptr_t args_kaddr);
/* PT_ATTACH SceShellUI, find virtual device handle via pt_call GetHandle,
* then send Cross × 30 frames + release × 18 frames via pt_call VDI to
* dismiss the assignment screen. Call this ~3s after SceShellCore VDA
* injection so the assignment screen has time to render. */
/* virtualDeviceId: pass the MBUS deviceId (from DEVICE_ADDED klog) so
* dismiss can try GetHandle(deviceId, type, idx) in addition to userId-based
* lookups. SceShellUI's "Open Pad [deviceId,0,0]" suggests GetHandle accepts
* deviceId as first arg when it is small (deviceId << userId range). */
int shellui_pad_dismiss_assignment_screen(int32_t userId, uint64_t virtualDeviceId);
/* PT_ATTACH SceShellUI, resolve sceMbusBindDeviceWithUserId from libSceMbus,
* and call it to directly assign virtualDeviceId to userId without going
* through the assignment screen UI. Returns 0 on success. */
int shellui_pad_force_bind(uint64_t virtualDeviceId, int32_t userId);
/* PT_ATTACH SceShellUI, call sceMbusDisconnectDevice(physicalDeviceId) to evict the
* current physical controller from the user's slot. After eviction the virtual device
* becomes the sole device at slot 0. Returns 0 on success. */
int shellui_pad_disconnect_device(uint64_t physicalDeviceId);
/* PT_ATTACH SceShellUI and call scePadVirtualDeviceInsertData(pad_handle, cross_data)
* directly, bypassing scePadGetHandle. padHandle comes from the CIM log. */
int shellui_pad_test_vdi_cross(int32_t pad_handle);
int shellcore_pad_test_vdi_cross(int32_t pad_handle);
int shellcore_pad_test_vdi_neutral(int32_t pad_handle); /* buttons=0, no UI input */
/* PT_ATTACH SceShellCore and call VDA(userId, type=3) via pt_call after
* assignment. Returns VDA return value; if >= 0 it is the VDI write handle. */
int32_t shellui_pad_retry_vda_shellcore(int32_t userId);
/* Manifest-verified SceShellCore VDA patch.
*
* PS4 firmware fingerprint from vda_probe_report:
* libScePad:scePadVirtualDeviceAddDevice hash256=0xbb22d8acd843d81e
* hash4k=0x346f2b8071895f89, VDA offset +0x5b40
*
* The patch only applies if prologue/hash/callsite/cave all match. It detours
* the dispatcher call at VDA+0xc0 into the verified NOP cave at VDA+0xdd2,
* calls the original dispatcher, forces eax=0, and returns to the original
* canary/epilogue path. Early validation errors are left intact.
* Returns 1 when applicable/applied/already applied, 0 on safe non-match. */
int shellui_pad_patch_vda(int dump_only);
int shellui_pad_patch_vda_self(int dump_only);
int shellui_pad_hook_setpriv(void);
int shellui_pad_unpatch(void);