mirror of
https://github.com/H4zeyaf/Ghostcontrol-PS5-USB-Controller-Patcher-Cyclone2.git
synced 2026-10-06 07:00:21 +02:00
initial
This commit is contained in:
1 parent
ad345113e3
commit
c1cb19042d
8 files changed
+5850
-2
No files matched your search
@@ -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)
|
||||
@@ -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 |
|
||||
@@ -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
|
||||
Binary file not shown.
@@ -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
File diff suppressed because it is too large.
Load diff
+141
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user