xbox series S support, official NX controllers + refactoring

added support for official Nintendo Switch pro controllers(not just 8bito ultimate 2) and xbox series S
This commit is contained in:
ItsDeidara committed 2026-06-08 07:55:15 -04:00
1 parent c1cb19042d
commit 62e229775e
15 files changed
+1400 -1092

No files matched your search

BIN
View File
Binary file not shown.
-1085
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
+4 -7
View File
@@ -1,9 +1,6 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# Ghost-Control: USB HID controller → virtual DualSense on PS5
PS5_HOST ?= ps5
PS5_HOST ?= 192.168.69.44
PORT ?= 9021
TARGET := ghost-control-ps5.elf
TARGET := ghost-control-xbox-ps5.elf
ifndef PS5_PAYLOAD_SDK
$(error PS5_PAYLOAD_SDK is not set)
@@ -14,7 +11,7 @@ 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
SRC := gc_main.c shellui_pad.c usb_helpers.c controller_nintendo.c controller_xbox.c
.PHONY: all clean deploy
@@ -27,4 +24,4 @@ clean:
rm -f $(TARGET)
deploy: $(TARGET)
nc -w 5 $(PS5_HOST) $(PORT) < $(TARGET)
nc -w 8 $(PS5_HOST) $(PORT) < $(TARGET)
+161
View File
@@ -0,0 +1,161 @@
#include "controller_nintendo.h"
#include "usb_helpers.h"
#include <stdio.h>
#include <string.h>
#ifdef __PROSPERO__
#include <ps5/klog.h>
#define LOG(...) klog_printf("[GC] " __VA_ARGS__)
#else
#define LOG(...) fprintf(stderr, __VA_ARGS__)
#endif
static uint8_t ntoh_stick(uint16_t v) {
if (v > 4095) v = 4095;
return (uint8_t)((v * 255u) / 4095u);
}
static uint32_t hat_to_dpad(uint8_t hat) {
switch (hat) {
case 0: return SCE_PAD_BUTTON_UP;
case 1: return SCE_PAD_BUTTON_UP | SCE_PAD_BUTTON_RIGHT;
case 2: return SCE_PAD_BUTTON_RIGHT;
case 3: return SCE_PAD_BUTTON_DOWN | SCE_PAD_BUTTON_RIGHT;
case 4: return SCE_PAD_BUTTON_DOWN;
case 5: return SCE_PAD_BUTTON_DOWN | SCE_PAD_BUTTON_LEFT;
case 6: return SCE_PAD_BUTTON_LEFT;
case 7: return SCE_PAD_BUTTON_UP | SCE_PAD_BUTTON_LEFT;
default: return 0;
}
}
/* Report 0x30: full 60Hz stream after handshake
* [3]=right btns [4]=shared [5]=left btns [6-8]=lstick [9-11]=rstick */
void nintendo_parse_0x30(const uint8_t *b, ScePadData *o) {
uint8_t br=b[3], bs=b[4], bl=b[5];
uint16_t lx=(uint16_t)(b[6]|((b[7]&0xF)<<8));
uint16_t ly=(uint16_t)((b[7]>>4)|((uint16_t)b[8]<<4));
uint16_t rx=(uint16_t)(b[9]|((b[10]&0xF)<<8));
uint16_t ry=(uint16_t)((b[10]>>4)|((uint16_t)b[11]<<4));
uint32_t btn=0;
if(br&0x04) btn|=SCE_PAD_BUTTON_CROSS;
if(br&0x08) btn|=SCE_PAD_BUTTON_CIRCLE;
if(br&0x01) btn|=SCE_PAD_BUTTON_SQUARE;
if(br&0x02) btn|=SCE_PAD_BUTTON_TRIANGLE;
if(bl&0x40) btn|=SCE_PAD_BUTTON_L1;
if(bl&0x80) btn|=SCE_PAD_BUTTON_L2;
if(br&0x40) btn|=SCE_PAD_BUTTON_R1;
if(br&0x80) btn|=SCE_PAD_BUTTON_R2;
if(bs&0x08) btn|=SCE_PAD_BUTTON_L3;
if(bs&0x04) btn|=SCE_PAD_BUTTON_R3;
if(bs&0x02) btn|=SCE_PAD_BUTTON_OPTIONS;
if(bs&0x01) btn|=SCE_PAD_BUTTON_CREATE;
if(bs&0x10) btn|=SCE_PAD_BUTTON_PS;
if(bs&0x20) btn|=SCE_PAD_BUTTON_TOUCH_PAD;
if(bl&0x02) btn|=SCE_PAD_BUTTON_UP;
if(bl&0x01) btn|=SCE_PAD_BUTTON_DOWN;
if(bl&0x04) btn|=SCE_PAD_BUTTON_RIGHT;
if(bl&0x08) btn|=SCE_PAD_BUTTON_LEFT;
o->buttons=btn;
o->leftStick.x=ntoh_stick(lx); o->leftStick.y=ntoh_stick(ly);
o->rightStick.x=ntoh_stick(rx); o->rightStick.y=ntoh_stick(ry);
o->analogButtons.l2=(bl&0x80)?255:0;
o->analogButtons.r2=(br&0x80)?255:0;
o->connected=1; o->quat.w=1.0f;
}
/* Report 0x3f: simple button-change report
* [1]=right btns [2]=shared [3]=HAT [4..7]=sticks [8]=L/ZL */
void nintendo_parse_0x3f(const uint8_t *b, ScePadData *o) {
uint8_t b1=b[1],b2=b[2],hat=b[3],b8=b[8];
uint32_t btn=0;
if(b1&0x04) btn|=SCE_PAD_BUTTON_CROSS;
if(b1&0x08) btn|=SCE_PAD_BUTTON_CIRCLE;
if(b1&0x01) btn|=SCE_PAD_BUTTON_SQUARE;
if(b1&0x02) btn|=SCE_PAD_BUTTON_TRIANGLE;
if(b8&0x40) btn|=SCE_PAD_BUTTON_L1;
if(b8&0x80) btn|=SCE_PAD_BUTTON_L2;
if(b1&0x40) btn|=SCE_PAD_BUTTON_R1;
if(b1&0x80) btn|=SCE_PAD_BUTTON_R2;
if(b2&0x08) btn|=SCE_PAD_BUTTON_L3;
if(b2&0x04) btn|=SCE_PAD_BUTTON_R3;
if(b2&0x02) btn|=SCE_PAD_BUTTON_OPTIONS;
if(b2&0x01) btn|=SCE_PAD_BUTTON_CREATE;
if(b2&0x10) btn|=SCE_PAD_BUTTON_PS;
if(b2&0x20) btn|=SCE_PAD_BUTTON_TOUCH_PAD;
btn|=hat_to_dpad(hat);
o->buttons=btn;
o->leftStick.x=b[4]; o->leftStick.y=b[5];
o->rightStick.x=b[6]; o->rightStick.y=b[7];
o->analogButtons.l2=(b8&0x80)?255:0;
o->analogButtons.r2=(b1&0x80)?255:0;
o->connected=1; o->quat.w=1.0f;
}
int nintendo_send_subcmd(int fd, struct usb_fs_endpoint *eps,
uint8_t *seq, uint8_t subcmd,
const uint8_t *data, uint32_t data_len) {
static const uint8_t rumble[8] = {0x00,0x01,0x40,0x40,0x00,0x01,0x40,0x40};
uint8_t buf[64];
uint32_t len = 11 + data_len;
if (len > sizeof(buf)) return -1;
memset(buf, 0, sizeof(buf));
buf[0] = 0x01; buf[1] = *seq & 0x0f;
memcpy(buf+2, rumble, 8);
buf[10] = subcmd;
if (data_len) memcpy(buf+11, data, data_len);
*seq = (uint8_t)((*seq+1) & 0x0f);
char tag[16]; snprintf(tag, sizeof(tag), "sc%02x", subcmd);
return usb_send_out(fd, &eps[1], buf, len, tag);
}
int nintendo_handle_packet(int fd, struct usb_fs_endpoint *eps,
const uint8_t *buf, uint32_t len,
int *hs_state, uint8_t *seq,
ScePadData *out_pad) {
uint8_t rid = buf[0];
/* Data packets */
if ((rid == 0x00 || rid == 0x30) && len >= 12) {
if (rid == 0x00 && buf[1] == 0) return 0; /* all-zero artifact */
if (*hs_state != HS_STREAMING) *hs_state = HS_STREAMING;
nintendo_parse_0x30(buf, out_pad);
return 1;
}
if (rid == 0x3f && len >= 9) {
nintendo_parse_0x3f(buf, out_pad);
return 1;
}
if (rid == 0x21 && len >= 12) {
LOG("0x21 ACK subcmd=0x%02x hs=%d\n", (buf[12]&0x7f), *hs_state);
if (*hs_state == HS_STREAMING) {
nintendo_parse_0x30(buf, out_pad);
return 1;
}
return 0;
}
if (rid == 0x81) {
if (*hs_state == HS_STREAMING) {
LOG("0x81 sub=0x%02x while streaming — reconnect\n", buf[1]);
*hs_state = HS_WAIT_81_01;
return 0;
}
if (buf[1] == 0x01 && *hs_state == HS_WAIT_81_01) {
usb_send_cmd(fd, &eps[1], 0x80, 0x02);
LOG("0x81 0x01 → [80 02]\n");
*hs_state = HS_WAIT_81_02;
} else if (buf[1] == 0x02 && *hs_state <= HS_WAIT_81_02) {
usb_send_cmd(fd, &eps[1], 0x80, 0x04);
LOG("0x81 0x02 → [80 04] + subcmds\n");
uint8_t d[]={0x01};
nintendo_send_subcmd(fd,eps,seq,0x40,d,1);
nintendo_send_subcmd(fd,eps,seq,0x48,d,1);
nintendo_send_subcmd(fd,eps,seq,0x30,d,1);
uint8_t d2[]={0x30};
nintendo_send_subcmd(fd,eps,seq,0x03,d2,1);
*hs_state = HS_STREAMING;
}
return 0;
}
return 0;
}
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <stdint.h>
#include <dev/usb/usb.h>
#include <dev/usb/usb_ioctl.h>
#include "gc_types.h"
/* Handshake states */
#define HS_WAIT_81_01 0
#define HS_WAIT_81_02 1
#define HS_STREAMING 2
void nintendo_parse_0x30(const uint8_t *b, ScePadData *o);
void nintendo_parse_0x3f(const uint8_t *b, ScePadData *o);
/* Send Nintendo subcommand on OUT ep (eps[1]).
* seq: rolling counter, incremented per call. */
int nintendo_send_subcmd(int fd, struct usb_fs_endpoint *eps,
uint8_t *seq, uint8_t subcmd,
const uint8_t *data, uint32_t data_len);
/* Handle one IN packet in the Nintendo state machine.
* Returns 1 if pad was updated and should be injected, 0 otherwise.
* hs_state and seq are in/out: updated by the function. */
int nintendo_handle_packet(int fd, struct usb_fs_endpoint *eps,
const uint8_t *buf, uint32_t len,
int *hs_state, uint8_t *seq,
ScePadData *out_pad);
+198
View File
@@ -0,0 +1,198 @@
/* controller_xbox.c — Xbox One S GIP controller for Ghost-Control
* All button bit positions hardware-confirmed on PS5 via live GIP probe.
* Reference: xboxSeriesSButtonBits.md
*/
#include "controller_xbox.h"
#include "usb_helpers.h"
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/ioctl.h>
#ifdef __PROSPERO__
#include <ps5/klog.h>
#define LOG(...) klog_printf("[GC] " __VA_ARGS__)
#else
#define LOG(...) fprintf(stderr, __VA_ARGS__)
#endif
/* Counts GIP INPUT packets — gates Guide button injection at startup */
static uint32_t g_input_count = 0;
/* ── helpers ──────────────────────────────────────────────────────────── */
static uint8_t trig_scale(uint16_t v) {
if (v > 1023u) v = 1023u;
return (uint8_t)((v * 255u) / 1023u);
}
#define DEADZONE 7849
static uint8_t stick_x(int16_t v) {
return (v > DEADZONE || v < -DEADZONE) ? (uint8_t)((v + 32768) >> 8) : 128u;
}
static uint8_t stick_y(int16_t v) {
return (v > DEADZONE || v < -DEADZONE) ? (uint8_t)(255 - ((v + 32768) >> 8)) : 128u;
}
/* ── input parsing ────────────────────────────────────────────────────── */
/* Parse GIP INPUT (cmd=0x20, 18 bytes) into ScePadData.
* Hardware-confirmed bit positions — see xboxSeriesSButtonBits.md */
void xbox_parse_input(const uint8_t *b, ScePadData *o) {
uint8_t b4 = b[4];
uint8_t b5 = b[5];
uint16_t lt16 = (uint16_t)b[6] | ((uint16_t)b[7] << 8);
uint16_t rt16 = (uint16_t)b[8] | ((uint16_t)b[9] << 8);
int16_t lx = (int16_t)((uint16_t)b[10] | ((uint16_t)b[11] << 8));
int16_t ly = (int16_t)((uint16_t)b[12] | ((uint16_t)b[13] << 8));
int16_t rx = (int16_t)((uint16_t)b[14] | ((uint16_t)b[15] << 8));
int16_t ry = (int16_t)((uint16_t)b[16] | ((uint16_t)b[17] << 8));
uint8_t lt = trig_scale(lt16);
uint8_t rt = trig_scale(rt16);
o->leftStick.x = stick_x(lx);
o->leftStick.y = stick_y(ly);
o->rightStick.x = stick_x(rx);
o->rightStick.y = stick_y(ry);
o->analogButtons.l2 = lt;
o->analogButtons.r2 = rt;
uint32_t btn = 0;
/* b[4]: system + face buttons */
if (b4 & 0x04u) btn |= SCE_PAD_BUTTON_OPTIONS; /* Menu (≡) → Options */
if (b4 & 0x08u) btn |= SCE_PAD_BUTTON_SHARE; /* View (⧉) → Share */
if (b4 & 0x10u) btn |= SCE_PAD_BUTTON_CROSS; /* A → Cross */
if (b4 & 0x20u) btn |= SCE_PAD_BUTTON_CIRCLE; /* B → Circle */
if (b4 & 0x40u) btn |= SCE_PAD_BUTTON_SQUARE; /* X → Square */
if (b4 & 0x80u) btn |= SCE_PAD_BUTTON_TRIANGLE; /* Y → Triangle */
/* b[5]: dpad + bumpers + stick clicks */
if (b5 & 0x01u) btn |= SCE_PAD_BUTTON_UP;
if (b5 & 0x02u) btn |= SCE_PAD_BUTTON_DOWN;
if (b5 & 0x04u) btn |= SCE_PAD_BUTTON_LEFT;
if (b5 & 0x08u) btn |= SCE_PAD_BUTTON_RIGHT;
if (b5 & 0x10u) btn |= SCE_PAD_BUTTON_L1; /* LB → L1 */
if (b5 & 0x20u) btn |= SCE_PAD_BUTTON_R1; /* RB → R1 */
if (b5 & 0x40u) btn |= SCE_PAD_BUTTON_L3; /* LS → L3 */
if (b5 & 0x80u) btn |= SCE_PAD_BUTTON_R3; /* RS → R3 */
/* Triggers: analog + digital threshold */
if (lt > 16u) btn |= SCE_PAD_BUTTON_L2;
if (rt > 16u) btn |= SCE_PAD_BUTTON_R2;
o->buttons = btn;
o->connected = 1;
o->quat.w = 1.0f;
}
/* ── GIP protocol ─────────────────────────────────────────────────────── */
static int read_one(int fd, struct usb_fs_endpoint *eps,
uint8_t *buf, uint32_t timeout_ms) {
void *b[1] = {buf}; uint32_t l[1] = {64};
eps[0].ppBuffer = b; eps[0].pLength = l; eps[0].nFrames = 1;
eps[0].timeout = timeout_ms; eps[0].aFrames = 0; eps[0].status = 0;
eps[0].flags = USB_FS_FLAG_SINGLE_SHORT_OK | USB_FS_FLAG_MULTI_SHORT_OK;
struct usb_fs_start st; memset(&st, 0, sizeof(st)); st.ep_index = 0;
if (ioctl(fd, USB_FS_START, &st) != 0) return -errno;
int polls = (int)((timeout_ms + 300) / 50) + 1;
for (int w = 0; w < polls; w++) {
struct usb_fs_complete co; memset(&co, 0, sizeof(co)); co.ep_index = 0;
if (ioctl(fd, USB_FS_COMPLETE, &co) == 0) {
if (eps[0].aFrames == 0 || l[0] == 0) return 0;
return (int)l[0];
}
if (errno != EBUSY) {
struct usb_fs_stop sp; memset(&sp, 0, sizeof(sp)); sp.ep_index = 0;
ioctl(fd, USB_FS_STOP, &sp);
return -errno;
}
usleep(50000);
}
struct usb_fs_stop sp; memset(&sp, 0, sizeof(sp)); sp.ep_index = 0;
ioctl(fd, USB_FS_STOP, &sp);
return 0;
}
static void send_ack(int fd, struct usb_fs_endpoint *eps, uint8_t orig_seq) {
uint8_t ack[8] = {GIP_CMD_ACK, 0x00, 0x00, 0x04,
orig_seq, GIP_CMD_ANNOUNCE, 0x00, 0x00};
usb_send_out(fd, &eps[1], ack, 8, "ack");
}
void xbox_gip_handshake(int fd, struct usb_fs_endpoint *eps) {
static const uint8_t power[] = {0x05, 0x20, 0x00, 0x01, 0x00};
uint8_t buf[64];
int announced = 0;
g_input_count = 0;
/* Pass 0: wait for ANNOUNCE; pass 1: send hello to re-trigger */
for (int pass = 0; pass < 2 && !announced; pass++) {
if (pass == 1) {
uint8_t hello[4] = {GIP_CMD_ACK, 0x00, 0x00, 0x00};
usb_send_out(fd, &eps[1], hello, 4, "hello");
}
/* Pass 0: 3 reads — if ANNOUNCE was already sent during probe, we miss it fast.
* Pass 1: send hello to re-trigger, wait longer. Handles both direct + hub. */
int reads = (pass == 0) ? 3 : 60;
for (int i = 0; i < reads && !announced; i++) {
int n = read_one(fd, eps, buf, 150);
if (n > 0 && buf[0] == GIP_CMD_ANNOUNCE) {
send_ack(fd, eps, buf[1]);
announced = 1;
} else if (n > 0 && buf[0] == GIP_CMD_INPUT) {
announced = 1;
}
}
}
usb_send_out(fd, &eps[1], power, 5, "power");
LOG("Xbox handshake done (announced=%d)\n", announced);
}
int xbox_handle_packet(int fd, struct usb_fs_endpoint *eps,
const uint8_t *buf, uint32_t len,
ScePadData *out_pad) {
(void)fd; (void)eps;
uint8_t cmd = buf[0];
/* Player input */
if (cmd == GIP_CMD_INPUT && len >= 18) {
g_input_count++;
xbox_parse_input(buf, out_pad);
return 1;
}
/* Guide button (Xbox logo): cmd=0x07, b[4]=0x01 pressed, 0x00 released.
* Controller auto-sends this at connect — gate behind 10 INPUT packets
* so the startup auto-send does not inject a PS button press. */
if (cmd == 0x07) {
out_pad->connected = 1;
out_pad->quat.w = 1.0f;
out_pad->leftStick.x = 128;
out_pad->leftStick.y = 128;
out_pad->rightStick.x = 128;
out_pad->rightStick.y = 128;
if (g_input_count > 10 && len >= 5 && (buf[4] & 0x01u))
out_pad->buttons = SCE_PAD_BUTTON_PS;
return 1;
}
/* Re-announce during streaming */
if (cmd == GIP_CMD_ANNOUNCE && len >= 4) {
static const uint8_t power[] = {0x05, 0x20, 0x00, 0x01, 0x00};
send_ack(fd, eps, buf[1]);
usb_send_out(fd, &eps[1], power, 5, "repower");
return 0;
}
return 0;
}
+51
View File
@@ -0,0 +1,51 @@
#pragma once
#include <stdint.h>
#include <dev/usb/usb.h>
#include <dev/usb/usb_ioctl.h>
#include "gc_types.h"
/*
* Xbox One (VID=0x045E PID=0x02EA) — GIP protocol over USB
*
* Endpoints: IN=0x82 OUT=0x02 (interface 1, FS speed, maxpkt=64)
*
* GIP input report format (18 bytes total):
* [0]=0x20 cmd [1]=seq [2]=opts [3]=0x0E (payload=14)
* [4..5] = buttons uint16 LE
* [6..7] = LT uint16 (0-1023)
* [8..9] = RT uint16 (0-1023)
* [10..11]= LX int16 (center=0)
* [12..13]= LY int16
* [14..15]= RX int16
* [16..17]= RY int16
*
* GIP wire button layout (from Linux xpad.c, xpad_process_packet_xboxone):
* b[4] GIP_BTN1: bit2=View(→Create) bit3=Menu(→Options)
* bit4=A(→Cross) bit5=B(→Circle) bit6=X(→Square) bit7=Y(→Triangle)
* b[5] GIP_BTN2: bit0=DUp bit1=DDn bit2=DLt bit3=DRt
* bit4=LB(→L1) bit5=RB(→R1) bit6=LS(→L3) bit7=RS(→R3)
* NOTE: GIP wire bits differ from XInput wButtons constants (XInput driver remaps).
* Guide arrives as separate GIP cmd=0x07 packet.
*/
#define XBOX_EP_IN 0x82
#define XBOX_EP_OUT 0x02
/* GIP command bytes */
#define GIP_CMD_ACK 0x01
#define GIP_CMD_ANNOUNCE 0x02
#define GIP_CMD_STATUS 0x03
#define GIP_CMD_INPUT 0x20
/* Parse GIP input report into ScePadData */
void xbox_parse_input(const uint8_t *buf, ScePadData *o);
/* GIP handshake: catch ANNOUNCE → ACK → POWER.
* Call immediately after opening IN+OUT endpoints.
* eps[0]=IN eps[1]=OUT. */
void xbox_gip_handshake(int fd, struct usb_fs_endpoint *eps);
/* Handle one IN packet. Returns 1 if pad updated, 0 to skip, -1 to reinit. */
int xbox_handle_packet(int fd, struct usb_fs_endpoint *eps,
const uint8_t *buf, uint32_t len,
ScePadData *out_pad);
+783
View File
@@ -0,0 +1,783 @@
/* SPDX-License-Identifier: GPL-3.0-or-later
* Ghost-Control v5: Multi-controller support
* USB HID controllers → virtual DualSense devices on PS5
*
* Supports up to MAX_SLOTS (4) simultaneous controllers:
* - 8BitDo / Nintendo Switch Pro (VID=057E PID=2009)
* - Xbox One S (VID=045E PID=02EA)
*
* Each detected controller gets its own VDA device + force_bind
* assignment dialog + VDI injection thread.
* Hotplug: plug in any time, disconnect any time.
*/
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <pthread.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <dev/usb/usb.h>
#include <dev/usb/usb_ioctl.h>
#include <dev/usb/usb_endian.h>
#ifdef __PROSPERO__
#include <ps5/kernel.h>
#include <ps5/klog.h>
#include <ps5/mdbg.h>
#endif
#include "shellui_pad.h"
#include "gc_types.h"
#include "usb_helpers.h"
#include "controller_nintendo.h"
#include "controller_xbox.h"
/* ── Logging ──────────────────────────────────────────────────────────── */
#define LOG_DIR "/data/ghostpad"
#define LOG_PATH "/data/ghostpad/gc_status.log"
#define PID_PATH "/data/ghostpad/gc_main.pid"
#define LOG_MAX 480
static pthread_mutex_t g_log_lock = PTHREAD_MUTEX_INITIALIZER;
static int g_log_fd = -1;
void ghostpad_status_log_reset(void) {
pthread_mutex_lock(&g_log_lock);
if (g_log_fd >= 0) { close(g_log_fd); g_log_fd = -1; }
mkdir(LOG_DIR, 0755);
g_log_fd = open(LOG_PATH, O_WRONLY|O_CREAT|O_TRUNC, 0600);
pthread_mutex_unlock(&g_log_lock);
}
void ghostpad_status_log(const char *fmt, ...) {
char buf[LOG_MAX]; va_list ap;
va_start(ap, fmt); vsnprintf(buf, sizeof(buf)-1, fmt, ap); va_end(ap);
buf[LOG_MAX-1] = '\0';
klog_printf("%s", buf);
pthread_mutex_lock(&g_log_lock);
if (g_log_fd >= 0) {
size_t n = strnlen(buf, sizeof(buf));
write(g_log_fd, buf, n);
if (n && buf[n-1] != '\n') write(g_log_fd, "\n", 1);
}
pthread_mutex_unlock(&g_log_lock);
}
#define gp_log(...) ghostpad_status_log("[GC] " __VA_ARGS__)
/* ── SCE stubs ────────────────────────────────────────────────────────── */
extern int32_t sceUserServiceInitialize(void *params);
extern int32_t sceUserServiceGetInitialUser(int32_t *outUserId);
extern int32_t sceUserServiceGetForegroundUser(int32_t *outUserId);
extern int32_t scePadInit(void);
extern int32_t scePadSetProcessPrivilege(int32_t privilege);
extern int32_t scePadGetHandle(int32_t userId, int32_t type, int32_t index);
extern int32_t scePadVirtualDeviceAddDevice(void *param, int32_t deviceType);
extern int32_t scePadVirtualDeviceDeleteDevice(int32_t handle);
extern int32_t scePadVirtualDeviceInsertData(int32_t handle, const void *padData);
extern int32_t sceKernelSendNotificationRequest(int unk0, void *req, size_t size, int unk1);
#define VIRTUAL_DEVICE_TYPE_DUALSENSE 3
/* ── Multi-controller slots ───────────────────────────────────────────── */
#define MAX_SLOTS 4
typedef struct {
volatile int32_t handle; /* VDA handle, -1 = slot free */
volatile int vdi_ready;
volatile int usb_active; /* USB reader thread is running */
volatile int confirmed; /* user pressed a button — assignment done */
char dev_path[32]; /* ugen path claimed by this slot */
uint16_t vid, pid;
volatile uint32_t inject_count;
} ctrl_slot_t;
static ctrl_slot_t g_slots[MAX_SLOTS];
static pthread_mutex_t g_slot_lock = PTHREAD_MUTEX_INITIALIZER;
static int32_t g_inject_uid = 0x10000000;
/* Assignment serialization: only ONE controller may show its assignment
* dialog at a time. g_assign_slot = slot awaiting user confirmation, or -1.
* Without this, multiple dialogs stack and all bind to the same user. */
static volatile int g_assign_slot = -1;
/* ── klog device-ID queue ─────────────────────────────────────────────── */
#define KLOG_QSIZE 16
static uint64_t g_klog_q[KLOG_QSIZE];
static int g_klog_qw = 0, g_klog_qr = 0;
static pthread_mutex_t g_klog_lock = PTHREAD_MUTEX_INITIALIZER;
static void klog_enqueue(uint64_t id) {
pthread_mutex_lock(&g_klog_lock);
int next = (g_klog_qw + 1) % KLOG_QSIZE;
if (next != g_klog_qr) { g_klog_q[g_klog_qw] = id; g_klog_qw = next; }
pthread_mutex_unlock(&g_klog_lock);
}
static uint64_t klog_dequeue_ms(int ms) {
for (int t = 0; t < ms; t += 100) {
pthread_mutex_lock(&g_klog_lock);
if (g_klog_qw != g_klog_qr) {
uint64_t id = g_klog_q[g_klog_qr];
g_klog_qr = (g_klog_qr + 1) % KLOG_QSIZE;
pthread_mutex_unlock(&g_klog_lock);
return id;
}
pthread_mutex_unlock(&g_klog_lock);
usleep(100000);
}
return 0;
}
/* ── Notification ─────────────────────────────────────────────────────── */
typedef struct { char _unk[45]; char message[3075]; } NotifyRequest;
static void notify(const char *fmt, ...) {
NotifyRequest req; va_list ap;
memset(&req, 0, sizeof(req));
va_start(ap, fmt); vsnprintf(req.message, sizeof(req.message), fmt, ap); va_end(ap);
sceKernelSendNotificationRequest(0, &req, sizeof(req), 0);
}
/* ── klog capture thread ──────────────────────────────────────────────── */
static uint64_t parse_hex_str(const char *s) {
uint64_t v = 0;
while (*s) {
char c = *s++;
if (c >= '0' && c <= '9') v = (v<<4)|(c-'0');
else if (c >= 'a' && c <= 'f') v = (v<<4)|(c-'a'+10);
else if (c >= 'A' && c <= 'F') v = (v<<4)|(c-'A'+10);
else break;
}
return v;
}
static void parse_klog_line(const char *line) {
if (!strstr(line, "DEVICE_ADDED")) return;
if (!strstr(line, "subType:22")) return;
if (!strstr(line, "capabilityBattery:0")) return;
const char *p = strstr(line, "DeviceId:0x");
if (!p) p = strstr(line, "deviceId=0x");
if (!p) return;
p += 11;
uint64_t id = parse_hex_str(p);
if (!id) return;
gp_log("klog: VDA device 0x%llx\n", (unsigned long long)id);
klog_enqueue(id);
}
static void *klog_capture_thread(void *arg) {
(void)arg;
char buf[512], line[1024]; size_t line_len = 0;
int fd = open("/dev/klog", O_RDONLY);
if (fd < 0) {
gp_log("klog: /dev/klog errno=%d, trying TCP 127.0.0.1:3232\n", errno);
struct sockaddr_in sin; int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) return NULL;
memset(&sin,0,sizeof(sin)); sin.sin_family=AF_INET;
sin.sin_addr.s_addr=inet_addr("127.0.0.1"); sin.sin_port=htons(3232);
for (int r=0; r<10; r++) {
if (connect(sock,(struct sockaddr*)&sin,sizeof(sin))==0){fd=sock;gp_log("klog: connected\n");break;}
usleep(500000);
}
if (fd<0){close(sock);return NULL;}
}
while (1) {
ssize_t len = read(fd, buf, sizeof(buf));
if (len < 0) { if (errno==EINTR) continue; break; }
if (len == 0) { usleep(10000); continue; }
for (ssize_t i=0; i<len; i++) {
char c = buf[i];
if (c=='\n'||line_len>=sizeof(line)-1){line[line_len]='\0';parse_klog_line(line);line_len=0;}
else if (c!='\r') line[line_len++]=c;
}
}
close(fd); return NULL;
}
/* ── VDI injection ────────────────────────────────────────────────────── */
static void inject_pad(int slot, const ScePadData *pad) {
int32_t h = g_slots[slot].handle;
if (h < 0 || !g_slots[slot].vdi_ready) return;
int vr = scePadVirtualDeviceInsertData(h, pad);
uint32_t n = ++g_slots[slot].inject_count;
if ((n % 600) == 0)
gp_log("slot[%d] VDI #%u ret=0x%08x\n", slot, n, (uint32_t)vr);
static int vdi_err_logged = 0;
if (vr != 0 && !vdi_err_logged) { gp_log("VDI error 0x%08x\n",(uint32_t)vr); vdi_err_logged=1; }
}
/* ── ugen detection ───────────────────────────────────────────────────── */
#define VID_NATIVE 0x2dc8u
#define PID_NATIVE 0x310bu
#define VID_SWITCH 0x057eu
#define PID_SWITCH 0x2009u
#define VID_XBOX 0x045eu
#define PID_XBOX 0x02eau
static const char *UGEN_PATHS[] = {
"/dev/ugen2.2","/dev/ugen2.3","/dev/ugen2.4","/dev/ugen2.5",
"/dev/ugen2.6","/dev/ugen2.7","/dev/ugen2.8","/dev/ugen2.9",
"/dev/ugen1.2","/dev/ugen0.2","/dev/ugen0.3",
};
#define N_UGEN_PATHS ((int)(sizeof(UGEN_PATHS)/sizeof(UGEN_PATHS[0])))
/* Probe one ugen2.x path to identify controller type.
* Returns 1 with vid/pid set, 0 if not a known controller.
* Skips non-ugen2 paths (not on external USB bus). */
static int probe_one_path(const char *path, uint16_t *out_vid, uint16_t *out_pid) {
if (strncmp(path, "/dev/ugen2.", 11) != 0) return 0;
int fd = open(path, O_RDWR|O_NONBLOCK);
if (fd < 0) return 0;
struct usb_fs_endpoint ep;
struct usb_fs_init ini;
struct usb_fs_uninit u;
memset(&ep,0,sizeof(ep)); memset(&ini,0,sizeof(ini));
ini.pEndpoints=&ep; ini.ep_index_max=1;
if (ioctl(fd,USB_FS_INIT,&ini)!=0) { close(fd); return 0; }
int ii=0; ioctl(fd,USB_IFACE_DRIVER_DETACH,&ii);
ii=1; ioctl(fd,USB_IFACE_DRIVER_DETACH,&ii);
ii=2; ioctl(fd,USB_IFACE_DRIVER_DETACH,&ii);
struct usb_fs_open po;
memset(&po,0,sizeof(po)); po.ep_index=0; po.max_bufsize=64; po.max_frames=1;
int found = 0;
/* Nintendo: ep=0x81, maxpkt=64 */
po.ep_no=0x81;
if (ioctl(fd,USB_FS_OPEN,&po)==0 && po.max_packet_length==64) {
gp_log("probe: %s ep=0x81 mpkt=%u → Nintendo\n", path,(unsigned)po.max_packet_length);
struct usb_fs_close pc; memset(&pc,0,sizeof(pc)); pc.ep_index=0; ioctl(fd,USB_FS_CLOSE,&pc);
*out_vid=VID_SWITCH; *out_pid=PID_SWITCH;
found = 1;
goto done;
}
/* Xbox One: ep=0x82, maxpkt in (0,64] */
memset(&po,0,sizeof(po)); po.ep_index=0; po.max_bufsize=64; po.max_frames=1;
po.ep_no=0x82;
if (ioctl(fd,USB_FS_OPEN,&po)==0 && po.max_packet_length>0 && po.max_packet_length<=64) {
gp_log("probe: %s ep=0x82 mpkt=%u → Xbox One\n", path,(unsigned)po.max_packet_length);
struct usb_fs_close pc; memset(&pc,0,sizeof(pc)); pc.ep_index=0; ioctl(fd,USB_FS_CLOSE,&pc);
*out_vid=VID_XBOX; *out_pid=PID_XBOX;
found = 1;
goto done;
}
done:
memset(&u,0,sizeof(u)); ioctl(fd,USB_FS_UNINIT,&u);
close(fd);
return found;
}
/* ── Create VDA and force_bind for a slot ─────────────────────────────── */
static int32_t create_vda_for_slot(int slot) {
struct { int32_t size; int32_t userId; int32_t pad[6]; } vdp;
const int32_t SEN = (int32_t)0xDEADBEEFu;
memset(&vdp,0,sizeof(vdp)); vdp.size=sizeof(vdp); vdp.userId=1;
for(int k=0;k<6;k++) vdp.pad[k]=SEN;
int ret = scePadVirtualDeviceAddDevice(&vdp, VIRTUAL_DEVICE_TYPE_DUALSENSE);
gp_log("slot[%d] VDA ret=0x%08x\n", slot, (uint32_t)ret);
int32_t handle = (ret > 0) ? ret : -1;
for(int k=0;k<6;k++){
if(vdp.pad[k]!=SEN && vdp.pad[k]>0){if(handle<0)handle=vdp.pad[k];break;}
}
uint64_t dev_id = klog_dequeue_ms(10000);
if (dev_id) {
handle = (int32_t)(dev_id & 0xffffffffu);
int br = shellui_pad_force_bind(dev_id, g_inject_uid);
gp_log("slot[%d] force_bind(0x%llx, 0x%08x) ret=%d\n",
slot, (unsigned long long)dev_id, (uint32_t)g_inject_uid, br);
} else if (handle >= 0) {
gp_log("slot[%d] klog timeout — using direct handle %d\n", slot, handle);
} else {
gp_log("slot[%d] GetHandle scan...\n", slot);
static const int32_t uids[]={1,0x10000000,(int32_t)0xffffffff};
for(int ui=0;ui<3&&handle<0;ui++)
for(int idx=0;idx<8&&handle<0;idx++){
handle=scePadGetHandle(uids[ui],3,idx);
if(handle>=0) gp_log("slot[%d] GetHandle uid=0x%08x idx=%d h=%d\n",
slot,(uint32_t)uids[ui],idx,handle);
}
}
if (handle>=0)
gp_log("slot[%d] VDI handle=0x%x ready\n", slot, (uint32_t)handle);
else
gp_log("slot[%d] ERROR: no VDA handle\n", slot);
return handle;
}
/* ── USB HID thread ───────────────────────────────────────────────────── */
/* Single-session: receives slot+path+vid+pid, runs until disconnect, then exits.
* Manager thread handles re-detection after exit. */
typedef struct {
int slot;
char dev_path[32];
uint16_t vid, pid;
} usb_thread_arg_t;
static void *usb_hid_thread(void *arg) {
usb_thread_arg_t *targ = (usb_thread_arg_t *)arg;
int slot = targ->slot;
char dev_path[32]; memcpy(dev_path, targ->dev_path, sizeof(dev_path));
uint16_t vid = targ->vid, pid = targ->pid;
free(targ);
struct usb_fs_endpoint eps[2];
struct usb_fs_init init;
struct usb_fs_open fs_open;
struct usb_fs_start start;
struct usb_fs_complete complete;
struct usb_fs_stop stop;
struct usb_fs_close fs_close;
struct usb_fs_uninit uninit;
uint8_t buf[64];
void *buffers[1]; uint32_t lengths[1];
int fd = -1, out_opened = 0;
int usb_ready_notified = 0;
gp_log("slot[%d] USB thread: %s VID=0x%04x PID=0x%04x\n",
slot, dev_path, vid, pid);
/* ── Xbox One: single-pass ─────────────────────────────────────────── */
if (pid == PID_XBOX) {
fd = open(dev_path, O_RDWR);
if (fd < 0) { gp_log("slot[%d] Xbox open fail errno=%d\n", slot, errno); goto exit_slot; }
{ int ii; for(ii=0;ii<4;ii++){int i2=ii; ioctl(fd,USB_IFACE_DRIVER_DETACH,&i2);} }
usleep(120000); /* hub settle: give USB hub time to propagate detach */
memset(eps,0,sizeof(eps)); memset(&init,0,sizeof(init));
init.pEndpoints=eps; init.ep_index_max=4;
if (ioctl(fd,USB_FS_INIT,&init)!=0){
gp_log("slot[%d] Xbox FS_INIT fail errno=%d\n",slot,errno);
close(fd); goto exit_slot;
}
memset(&fs_open,0,sizeof(fs_open));
fs_open.ep_index=0; fs_open.ep_no=XBOX_EP_IN;
fs_open.max_bufsize=64; fs_open.max_frames=1;
if (ioctl(fd,USB_FS_OPEN,&fs_open)!=0){
gp_log("slot[%d] Xbox IN fail errno=%d\n",slot,errno);
goto uninit_exit;
}
gp_log("slot[%d] Xbox IN ep=0x%02x ok maxpkt=%u\n",
slot, XBOX_EP_IN, (unsigned)fs_open.max_packet_length);
buffers[0]=buf; lengths[0]=64;
eps[0].ppBuffer=buffers; eps[0].pLength=lengths; eps[0].nFrames=1;
eps[0].timeout=50; eps[0].flags=USB_FS_FLAG_SINGLE_SHORT_OK|USB_FS_FLAG_MULTI_SHORT_OK;
memset(&fs_open,0,sizeof(fs_open));
fs_open.ep_index=1; fs_open.ep_no=XBOX_EP_OUT;
fs_open.max_bufsize=64; fs_open.max_frames=1;
out_opened = (ioctl(fd,USB_FS_OPEN,&fs_open)==0) ? 1 : 0;
gp_log("slot[%d] Xbox OUT ep=0x%02x opened=%d\n", slot, XBOX_EP_OUT, out_opened);
xbox_gip_handshake(fd, eps);
goto main_loop;
}
/* ── Nintendo: two-pass ────────────────────────────────────────────── */
fd = open(dev_path, O_RDWR);
if (fd < 0) { gp_log("slot[%d] open fail errno=%d\n", slot, errno); goto exit_slot; }
memset(eps,0,sizeof(eps)); memset(&init,0,sizeof(init));
init.pEndpoints=eps; init.ep_index_max=1;
if (ioctl(fd,USB_FS_INIT,&init)!=0){
gp_log("slot[%d] Nintendo FS_INIT p1 fail\n",slot); close(fd); goto exit_slot;
}
{ int i0=0,i1=1; ioctl(fd,USB_IFACE_DRIVER_DETACH,&i0); ioctl(fd,USB_IFACE_DRIVER_DETACH,&i1); }
memset(&fs_open,0,sizeof(fs_open));
fs_open.ep_index=0; fs_open.ep_no=0x81; fs_open.max_bufsize=64; fs_open.max_frames=1;
if (ioctl(fd,USB_FS_OPEN,&fs_open)!=0){
gp_log("slot[%d] Nintendo IN p1 fail errno=%d\n",slot,errno); goto uninit_exit;
}
gp_log("slot[%d] Nintendo p1 IN ok maxpkt=%u\n",slot,(unsigned)fs_open.max_packet_length);
memset(&uninit,0,sizeof(uninit)); ioctl(fd,USB_FS_UNINIT,&uninit);
close(fd); fd=-1;
/* Pass 2: retry DETACH+OPEN up to 5 times to beat usb_hid0 re-attach
* (real Switch Pro is claimed by PS5 native HID driver after probe releases it) */
{ int detach_try;
for (detach_try = 0; detach_try < 5; detach_try++) {
fd = open(dev_path, O_RDWR);
if (fd < 0) { gp_log("slot[%d] reopen fail attempt %d\n",slot,detach_try); goto exit_slot; }
{ int i0=0,i1=1,i2=2;
ioctl(fd,USB_IFACE_DRIVER_DETACH,&i0);
ioctl(fd,USB_IFACE_DRIVER_DETACH,&i1);
ioctl(fd,USB_IFACE_DRIVER_DETACH,&i2); }
memset(eps,0,sizeof(eps)); memset(&init,0,sizeof(init));
init.pEndpoints=eps; init.ep_index_max=2;
if (ioctl(fd,USB_FS_INIT,&init)!=0){
close(fd); fd=-1; usleep(50000); continue;
}
memset(&fs_open,0,sizeof(fs_open));
fs_open.ep_index=0; fs_open.ep_no=0x81; fs_open.max_bufsize=64; fs_open.max_frames=1;
if (ioctl(fd,USB_FS_OPEN,&fs_open)==0) break; /* claimed it */
memset(&uninit,0,sizeof(uninit)); ioctl(fd,USB_FS_UNINIT,&uninit);
close(fd); fd=-1;
gp_log("slot[%d] Nintendo p2 OPEN retry %d errno=%d\n",slot,detach_try,errno);
usleep(50000);
}
if (fd < 0) { gp_log("slot[%d] Nintendo p2 give up\n",slot); goto exit_slot; }
}
gp_log("slot[%d] Nintendo p2 IN ok maxpkt=%u\n",slot,(unsigned)fs_open.max_packet_length);
if (fs_open.max_packet_length != 64){ gp_log("slot[%d] wrong maxpkt, reinit\n",slot); goto reinit; }
buffers[0]=buf; lengths[0]=64;
eps[0].ppBuffer=buffers; eps[0].pLength=lengths; eps[0].nFrames=1;
eps[0].timeout=200; eps[0].flags=USB_FS_FLAG_SINGLE_SHORT_OK|USB_FS_FLAG_MULTI_SHORT_OK;
/* 8BitDo uses ep=0x02; real Nintendo Switch Pro Controller uses ep=0x01 */
memset(&fs_open,0,sizeof(fs_open));
fs_open.ep_index=1; fs_open.ep_no=0x02; fs_open.max_bufsize=64; fs_open.max_frames=1;
out_opened = (ioctl(fd,USB_FS_OPEN,&fs_open)==0) ? 1 : 0;
if (!out_opened) {
memset(&fs_open,0,sizeof(fs_open));
fs_open.ep_index=1; fs_open.ep_no=0x01; fs_open.max_bufsize=64; fs_open.max_frames=1;
out_opened = (ioctl(fd,USB_FS_OPEN,&fs_open)==0) ? 1 : 0;
if (out_opened) gp_log("slot[%d] Nintendo OUT ep=0x01 (real Switch Pro)\n", slot);
}
gp_log("slot[%d] Nintendo OUT opened=%d\n", slot, out_opened);
if (out_opened) {
usb_send_cmd(fd,&eps[1],0x80,0x02); usleep(30000);
usb_send_cmd(fd,&eps[1],0x80,0x04); usleep(50000);
gp_log("slot[%d] Nintendo [80 02]+[80 04] sent\n", slot);
}
main_loop: ;
int hs_state = (pid==PID_XBOX) ? HS_STREAMING : HS_WAIT_81_01;
uint8_t nintendo_seq = 1;
while (1) {
memset(buf,0,64);
buffers[0]=buf; lengths[0]=64;
eps[0].ppBuffer=buffers; eps[0].pLength=lengths;
eps[0].aFrames=0; eps[0].status=0;
memset(&start,0,sizeof(start)); start.ep_index=0;
if (ioctl(fd,USB_FS_START,&start)!=0) {
if (errno==EBUSY){
memset(&stop,0,sizeof(stop)); stop.ep_index=0; ioctl(fd,USB_FS_STOP,&stop);
usleep(5000);
} else if (errno==ENXIO||errno==ENOTTY){
gp_log("slot[%d] START errno=%d — device gone\n",slot,errno); goto reinit;
} else {
gp_log("slot[%d] START fatal errno=%d\n",slot,errno); goto reinit;
}
continue;
}
int ok=0, cerr=0, cw=0;
for(cw=0;cw<60;cw++){
memset(&complete,0,sizeof(complete)); complete.ep_index=0;
if(ioctl(fd,USB_FS_COMPLETE,&complete)==0){ok=1;break;}
cerr=errno;
if(cerr==ENXIO||cerr==ENOTTY){gp_log("slot[%d] COMPLETE errno=%d — gone\n",slot,cerr);goto reinit;}
if(cerr!=EBUSY) break;
usleep(500);
}
if(!ok){
memset(&stop,0,sizeof(stop)); stop.ep_index=0; ioctl(fd,USB_FS_STOP,&stop);
continue;
}
if(lengths[0]<1) continue;
uint32_t len = lengths[0];
ScePadData pad; memset(&pad,0,sizeof(pad)); pad.quat.w=1.0f;
int injected = 0;
if (pid == PID_XBOX) {
injected = xbox_handle_packet(fd, eps, buf, len, &pad);
} else {
injected = nintendo_handle_packet(fd, eps, buf, len, &hs_state, &nintendo_seq, &pad);
}
if (injected > 0) {
if (!usb_ready_notified) {
notify("Ghostcontrol: slot[%d] streaming — controller active", slot);
usb_ready_notified = 1;
}
/* First real button press confirms the assignment — release the gate
* so the manager can start the next controller's dialog. */
if (!g_slots[slot].confirmed && pad.buttons != 0) {
g_slots[slot].confirmed = 1;
if (g_assign_slot == slot) g_assign_slot = -1;
gp_log("slot[%d] assignment confirmed (button press)\n", slot);
}
inject_pad(slot, &pad);
}
}
reinit:
if (usb_ready_notified) { notify("Ghostcontrol: slot[%d] controller disconnected", slot); usb_ready_notified=0; }
memset(&stop,0,sizeof(stop)); stop.ep_index=0; ioctl(fd,USB_FS_STOP,&stop);
if (out_opened) {
memset(&fs_close,0,sizeof(fs_close)); fs_close.ep_index=1; ioctl(fd,USB_FS_CLOSE,&fs_close);
out_opened=0;
}
memset(&fs_close,0,sizeof(fs_close)); fs_close.ep_index=0; ioctl(fd,USB_FS_CLOSE,&fs_close);
uninit_exit:
memset(&uninit,0,sizeof(uninit)); ioctl(fd,USB_FS_UNINIT,&uninit);
close(fd); fd=-1;
exit_slot:
gp_log("slot[%d] USB thread exiting — freeing slot\n", slot);
scePadVirtualDeviceDeleteDevice(g_slots[slot].handle);
pthread_mutex_lock(&g_slot_lock);
g_slots[slot].handle = -1;
g_slots[slot].vdi_ready = 0;
g_slots[slot].usb_active= 0;
g_slots[slot].dev_path[0] = '\0';
pthread_mutex_unlock(&g_slot_lock);
return NULL;
}
/* ── Controller manager thread ────────────────────────────────────────── */
static void *controller_manager_thread(void *arg) {
(void)arg;
int scan = 0;
gp_log("Manager thread started (MAX_SLOTS=%d)\n", MAX_SLOTS);
while (1) {
for (int i = 0; i < N_UGEN_PATHS; i++) {
const char *path = UGEN_PATHS[i];
/* Skip non-external-bus paths */
if (strncmp(path, "/dev/ugen2.", 11) != 0) continue;
/* Serialize assignment: if a controller is still awaiting the user's
* button press to confirm its dialog, do not start another one. */
if (g_assign_slot >= 0) break;
/* Skip if already claimed by an active slot */
int busy = 0;
pthread_mutex_lock(&g_slot_lock);
for (int s = 0; s < MAX_SLOTS; s++) {
if (g_slots[s].usb_active && strcmp(g_slots[s].dev_path, path) == 0) {
busy = 1; break;
}
}
pthread_mutex_unlock(&g_slot_lock);
if (busy) continue;
/* Try to identify controller */
uint16_t vid=0, pid=0;
if (!probe_one_path(path, &vid, &pid)) continue;
/* Find free slot */
int slot = -1;
pthread_mutex_lock(&g_slot_lock);
for (int s = 0; s < MAX_SLOTS; s++) {
if (g_slots[s].handle < 0 && !g_slots[s].usb_active) { slot=s; break; }
}
pthread_mutex_unlock(&g_slot_lock);
if (slot < 0) {
if ((scan % 5) == 0) gp_log("manager: all %d slots full\n", MAX_SLOTS);
continue;
}
const char *name =
(vid==VID_SWITCH && pid==PID_SWITCH) ? "Nintendo Switch Pro / 8BitDo" :
(vid==VID_NATIVE && pid==PID_NATIVE) ? "8BitDo Native" :
(vid==VID_XBOX && pid==PID_XBOX) ? "Xbox One S" : "Unknown";
gp_log("manager: %s at %s → slot[%d]\n", name, path, slot);
notify("Ghostcontrol: %s detected — assign user on screen", name);
/* Claim the slot path before VDA so manager skips it if we retry.
* Set the assignment gate — released when user confirms (button press). */
pthread_mutex_lock(&g_slot_lock);
strncpy(g_slots[slot].dev_path, path, sizeof(g_slots[slot].dev_path)-1);
g_slots[slot].usb_active = 1; /* tentatively claimed */
g_slots[slot].confirmed = 0;
g_slots[slot].vid = vid;
g_slots[slot].pid = pid;
pthread_mutex_unlock(&g_slot_lock);
g_assign_slot = slot;
/* Create VDA and force_bind (shows PS5 assignment dialog) */
int32_t handle = create_vda_for_slot(slot);
if (handle < 0) {
gp_log("manager: slot[%d] VDA failed — releasing\n", slot);
pthread_mutex_lock(&g_slot_lock);
g_slots[slot].usb_active = 0;
g_slots[slot].dev_path[0] = '\0';
g_slots[slot].handle = -1;
pthread_mutex_unlock(&g_slot_lock);
g_assign_slot = -1;
continue;
}
pthread_mutex_lock(&g_slot_lock);
g_slots[slot].handle = handle;
g_slots[slot].vdi_ready = 1;
g_slots[slot].inject_count = 0;
pthread_mutex_unlock(&g_slot_lock);
/* Launch USB reader thread */
usb_thread_arg_t *targ = malloc(sizeof(*targ));
if (!targ) {
gp_log("manager: malloc fail for slot[%d]\n", slot);
scePadVirtualDeviceDeleteDevice(handle);
pthread_mutex_lock(&g_slot_lock);
g_slots[slot].handle=-1; g_slots[slot].vdi_ready=0;
g_slots[slot].usb_active=0; g_slots[slot].dev_path[0]='\0';
pthread_mutex_unlock(&g_slot_lock);
g_assign_slot = -1;
continue;
}
targ->slot = slot;
strncpy(targ->dev_path, path, sizeof(targ->dev_path)-1);
targ->vid=vid; targ->pid=pid;
pthread_t tid;
if (pthread_create(&tid, NULL, usb_hid_thread, targ) != 0) {
gp_log("manager: pthread_create fail slot[%d]\n", slot);
free(targ);
scePadVirtualDeviceDeleteDevice(handle);
pthread_mutex_lock(&g_slot_lock);
g_slots[slot].handle=-1; g_slots[slot].vdi_ready=0;
g_slots[slot].usb_active=0; g_slots[slot].dev_path[0]='\0';
pthread_mutex_unlock(&g_slot_lock);
g_assign_slot = -1;
} else {
pthread_detach(tid);
gp_log("manager: slot[%d] USB thread started handle=0x%x\n",
slot, (uint32_t)handle);
notify("Ghostcontrol: slot[%d] ready — press a button to assign", slot);
}
/* One controller per scan pass — assignment gate blocks the rest
* until the user confirms this one with a button press. */
break;
}
/* Assignment timeout: if the user never presses a button, release the
* gate after ~30s so the queue does not stall forever. */
static int assign_wait = 0;
if (g_assign_slot >= 0) {
if (++assign_wait > 15) { /* 15 * 2s = 30s */
gp_log("manager: assignment timeout slot[%d] — releasing gate\n", g_assign_slot);
g_assign_slot = -1;
assign_wait = 0;
}
} else {
assign_wait = 0;
}
scan++;
if ((scan % 5) == 0) {
/* Log active slots every 10s */
int active = 0;
for (int s = 0; s < MAX_SLOTS; s++)
if (g_slots[s].usb_active) active++;
if (active == 0 && (scan % 10) == 0)
gp_log("manager: scan #%d — no controllers\n", scan);
}
usleep(2000000);
}
return NULL;
}
/* ── Credential elevation ─────────────────────────────────────────────── */
static void elevate_credentials(void) {
pid_t p = getpid();
uint8_t caps[16]; memset(caps,0xff,sizeof(caps));
kernel_set_ucred_authid(p, 0x3800000000010003l);
kernel_set_ucred_caps(p, caps);
}
/* ── main ─────────────────────────────────────────────────────────────── */
int main(void) {
int32_t userId=-1, fgUser=-1; int ret;
ghostpad_status_log_reset();
gp_log("Ghost-Control v5 starting — %d slots\n", MAX_SLOTS);
notify("Ghostcontrol by StonedModder — plug in controllers now");
/* Kill previous instance */
{ int pfd=open(PID_PATH,O_RDONLY);
if(pfd>=0){char pb[16]={0};read(pfd,pb,15);close(pfd);
pid_t old=(pid_t)atoi(pb);
if(old>0&&old!=getpid()){gp_log("Killing prev pid=%d\n",old);kill(old,SIGTERM);usleep(600000);}
}
int pfd2=open(PID_PATH,O_WRONLY|O_CREAT|O_TRUNC,0600);
if(pfd2>=0){char pb[16];snprintf(pb,sizeof(pb),"%d",getpid());write(pfd2,pb,strlen(pb));close(pfd2);}
}
/* Init slots */
for (int s = 0; s < MAX_SLOTS; s++) {
g_slots[s].handle = -1;
g_slots[s].vdi_ready = 0;
g_slots[s].usb_active = 0;
g_slots[s].confirmed = 0;
g_slots[s].dev_path[0]= '\0';
}
g_assign_slot = -1;
sceUserServiceInitialize(NULL);
sceUserServiceGetInitialUser(&userId);
sceUserServiceGetForegroundUser(&fgUser);
gp_log("userId=0x%08x fgUser=0x%08x\n", (uint32_t)userId, (uint32_t)fgUser);
g_inject_uid = (fgUser > 0) ? fgUser : userId;
if ((uint32_t)userId<0x10000000u||(uint32_t)userId>0x1000000Fu) userId=0x10000000;
gp_log("inject_uid=0x%08x\n", (uint32_t)g_inject_uid);
elevate_credentials();
ret=scePadInit(); gp_log("scePadInit: 0x%08x\n", ret);
ret=scePadSetProcessPrivilege(1); gp_log("scePadSetProcessPrivilege: 0x%08x\n", ret);
/* Clean up any orphaned VDA devices */
for (int dh=0; dh<64; dh++) {
if (scePadVirtualDeviceDeleteDevice(dh)==0) gp_log("deleteDevice(%d)\n", dh);
}
/* Start klog capture thread first — must be running before any VDA call */
pthread_t klog_tid;
if (pthread_create(&klog_tid, NULL, klog_capture_thread, NULL)==0) {
pthread_detach(klog_tid);
gp_log("klog thread started\n");
}
usleep(300000); /* let klog thread connect before first VDA */
/* Start controller manager — handles all detection, VDA creation, USB threads */
pthread_t mgr_tid;
if (pthread_create(&mgr_tid, NULL, controller_manager_thread, NULL)==0) {
pthread_detach(mgr_tid);
gp_log("Manager thread started\n");
}
/* Keep-alive */
uint32_t tick = 0;
while (1) {
usleep(1000000);
tick++;
if (tick % 10 == 0) {
int active = 0;
for (int s = 0; s < MAX_SLOTS; s++) if (g_slots[s].usb_active) active++;
gp_log("alive tick=%u active_slots=%d\n", tick, active);
}
}
return 0;
}
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include <stdint.h>
/* SCE pad button constants */
#define SCE_PAD_BUTTON_L3 0x00000002u
#define SCE_PAD_BUTTON_R3 0x00000004u
#define SCE_PAD_BUTTON_OPTIONS 0x00000008u
#define SCE_PAD_BUTTON_UP 0x00000010u
#define SCE_PAD_BUTTON_RIGHT 0x00000020u
#define SCE_PAD_BUTTON_DOWN 0x00000040u
#define SCE_PAD_BUTTON_LEFT 0x00000080u
#define SCE_PAD_BUTTON_L2 0x00000100u
#define SCE_PAD_BUTTON_R2 0x00000200u
#define SCE_PAD_BUTTON_L1 0x00000400u
#define SCE_PAD_BUTTON_R1 0x00000800u
#define SCE_PAD_BUTTON_TRIANGLE 0x00001000u
#define SCE_PAD_BUTTON_CIRCLE 0x00002000u
#define SCE_PAD_BUTTON_CROSS 0x00004000u
#define SCE_PAD_BUTTON_SQUARE 0x00008000u
#define SCE_PAD_BUTTON_CREATE 0x00010000u /* = PS button — triggers home screen via VDI */
#define SCE_PAD_BUTTON_PS 0x00010000u
#define SCE_PAD_BUTTON_SHARE 0x00000001u /* DualSense Create/Share (DS4 SELECT/SHARE bit) */
#define SCE_PAD_BUTTON_TOUCH_PAD 0x00100000u
typedef struct { uint16_t x; uint16_t y; uint8_t finger; uint8_t pad[3]; } ScePadTouch;
typedef struct {
uint8_t fingers; uint8_t pad1[3]; uint32_t pad2; ScePadTouch touch[2];
} ScePadTouchData;
typedef struct {
uint32_t buttons;
struct { uint8_t x; uint8_t y; } leftStick;
struct { uint8_t x; uint8_t y; } rightStick;
struct { uint8_t l2; uint8_t r2; } analogButtons;
uint16_t padding;
struct { float x, y, z, w; } quat;
struct { float x, y, z; } vel;
struct { float x, y, z; } accel;
ScePadTouchData touchData;
uint8_t connected;
uint8_t _align[3];
uint64_t timestamp;
uint8_t ext[16];
uint8_t count;
uint8_t unknown[15];
} ScePadData;
File renamed without changes.
File renamed without changes.
+56
View File
@@ -0,0 +1,56 @@
#include "usb_helpers.h"
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#ifdef __PROSPERO__
#include <ps5/klog.h>
#define LOG(...) klog_printf("[GC] " __VA_ARGS__)
#else
#define LOG(...) fprintf(stderr, __VA_ARGS__)
#endif
int usb_send_out(int fd, struct usb_fs_endpoint *ep,
const uint8_t *data, uint32_t len, const char *tag) {
void *bufs[1] = { (void *)data };
uint32_t lens[1] = { len };
struct usb_fs_start start;
struct usb_fs_complete complete;
ep->ppBuffer = bufs;
ep->pLength = lens;
ep->nFrames = 1;
ep->timeout = 150;
ep->flags = 0;
ep->aFrames = 0;
ep->status = 0;
memset(&start, 0, sizeof(start));
start.ep_index = 1;
if (ioctl(fd, USB_FS_START, &start) != 0) {
LOG("OUT %s START fail errno=%d\n", tag, errno);
return -errno;
}
for (int w = 0; w < 20; w++) {
memset(&complete, 0, sizeof(complete));
complete.ep_index = 1;
if (ioctl(fd, USB_FS_COMPLETE, &complete) == 0)
return 0;
if (errno != EBUSY) {
LOG("OUT %s COMPLETE fail errno=%d\n", tag, errno);
return -errno;
}
usleep(50000);
}
LOG("OUT %s timeout\n", tag);
return -EBUSY;
}
int usb_send_cmd(int fd, struct usb_fs_endpoint *ep, uint8_t a, uint8_t b) {
uint8_t buf[2] = { a, b };
char tag[8];
snprintf(tag, sizeof(tag), "%02x%02x", a, b);
return usb_send_out(fd, ep, buf, 2, tag);
}
+8
View File
@@ -0,0 +1,8 @@
#pragma once
#include <stdint.h>
#include <dev/usb/usb.h>
#include <dev/usb/usb_ioctl.h>
int usb_send_out(int fd, struct usb_fs_endpoint *ep,
const uint8_t *data, uint32_t len, const char *tag);
int usb_send_cmd(int fd, struct usb_fs_endpoint *ep, uint8_t a, uint8_t b);
+67
View File
@@ -0,0 +1,67 @@
# Xbox One S / Series S GIP Wire Button Bits
**VID=0x045E PID=0x02EA — hardware-confirmed on PS5 via live GIP probe**
## GIP INPUT packet (cmd=0x20) layout
```
b[0]=0x20 b[1]=seq b[2]=opts b[3]=0x0E (payload len=14)
b[4] = button byte 1 (digital buttons)
b[5] = button byte 2 (dpad + bumpers + sticks)
b[6..7] = LT uint16 LE 0-1023 → scale to 0-255
b[8..9] = RT uint16 LE 0-1023 → scale to 0-255
b[10..11]= LX int16 LE center≈0
b[12..13]= LY int16 LE center≈0
b[14..15]= RX int16 LE center≈0
b[16..17]= RY int16 LE center≈0
```
## b[4] — face buttons + system buttons
| Bit | Mask | Physical button | PS5 target |
|------|------|-----------------|-------------------|
| bit2 | 0x04 | Menu (≡) | OPTIONS (0x0008) |
| bit3 | 0x08 | View (⧉) | SHARE (0x20000)|
| bit4 | 0x10 | A | CROSS (0x4000) |
| bit5 | 0x20 | B | CIRCLE (0x2000) |
| bit6 | 0x40 | X | SQUARE (0x8000) |
| bit7 | 0x80 | Y | TRIANGLE (0x1000) |
## b[5] — dpad + bumpers + stick clicks
| Bit | Mask | Physical button | PS5 target |
|------|------|-----------------|----------------|
| bit0 | 0x01 | DPad Up | UP (0x0010) |
| bit1 | 0x02 | DPad Down | DOWN (0x0040) |
| bit2 | 0x04 | DPad Left | LEFT (0x0080) |
| bit3 | 0x08 | DPad Right | RIGHT (0x0020) |
| bit4 | 0x10 | LB | L1 (0x0400) |
| bit5 | 0x20 | RB | R1 (0x0800) |
| bit6 | 0x40 | L3 (left click) | L3 (0x0002) |
| bit7 | 0x80 | R3 (right click)| R3 (0x0004) |
## Analog
| Field | Range | PS5 field |
|-------------|--------|---------------------|
| LT (b[6..7])| 0-1023 | analogButtons.l2 (0-255), digital L2 bit if >16 |
| RT (b[8..9])| 0-1023 | analogButtons.r2 (0-255), digital R2 bit if >16 |
| LX (b[10..11])| int16 center≈0 | leftStick.x = (lx+32768)>>8, deadzone ±7849 |
| LY (b[12..13])| int16 center≈0 | leftStick.y = 255-((ly+32768)>>8), deadzone ±7849 |
| RX (b[14..15])| int16 center≈0 | rightStick.x = (rx+32768)>>8, deadzone ±7849 |
| RY (b[16..17])| int16 center≈0 | rightStick.y = 255-((ry+32768)>>8), deadzone ±7849 |
## Guide button (Xbox logo) — HARDWARE CONFIRMED
```
GIP cmd=0x07 len=6
b[4]=0x01 = pressed
b[4]=0x00 = released
b[5]=0x5b (constant — controller status byte, ignore)
```
Maps to SCE_PAD_BUTTON_PS (0x10000).
Current code: `if (cmd == 0x07 && buf[4] & 0x01u)` — correct.
## Notes
- xpad.c (Linux) labels bit2=View, bit3=Menu — WRONG for this hardware.
Confirmed physically: bit2=Menu(≡), bit3=View(⧉).
- SCE_PAD_BUTTON_CREATE = SCE_PAD_BUTTON_PS = 0x10000 — injecting this
triggers PS home screen. View maps to SHARE (0x20000) instead.
- GIP wire bits differ completely from XInput wButtons API constants.
XInput is a Windows abstraction layer; these are raw USB GIP bytes.
- Idle stick drift observed: lx≈-1863, ly≈-149, rx≈2007, ry≈-771.
Deadzone of ±7849 covers this entirely.