diff --git a/Ghostcontrol.elf b/Ghostcontrol.elf new file mode 100644 index 0000000..0df1509 Binary files /dev/null and b/Ghostcontrol.elf differ diff --git a/gc_main.c b/gc_main.c deleted file mode 100644 index 49d04b4..0000000 --- a/gc_main.c +++ /dev/null @@ -1,1085 +0,0 @@ -/* SPDX-License-Identifier: GPL-3.0-or-later - * Ghost-Control v3: USB HID controller → virtual DualSense on PS5 - * - * Follows Ghostpad's proven VDA path exactly: - * 1. scePadVirtualDeviceAddDevice(userId=1, type=3) - * 2. Monitor klogsrv:3232 for DEVICE_ADDED [type:1][subType:22] - * 3. shellui_pad_force_bind(vDevId, fgUserId) via PT_ATTACH SceShellUI - * 4. scePadVirtualDeviceInsertData(handle, &padData) directly from this process - * - * USB HID input (new piece in front of Ghostpad's VDI path): - * /dev/ugen2.2 → USB_IFACE_DRIVER_DETACH → USB_FS_OPEN ep=0x81 - * → Nintendo handshake → 0x30/0x3f report loop → parse → VDI - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#ifdef __PROSPERO__ -#include -#include -#include -#endif - -#include "shellui_pad.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); - -/* ------------------------------------------------------------------ - * SCE 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 -#define SCE_PAD_BUTTON_PS 0x00010000u -#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; - -#define VIRTUAL_DEVICE_TYPE_DUALSENSE 3 - -/* ------------------------------------------------------------------ - * Shared state - * ------------------------------------------------------------------ */ - -static volatile int32_t g_vdi_handle = -1; -static volatile int g_vdi_ready = 0; -/* Actual foreground userId — used for force_bind */ -static int32_t g_inject_uid = 0x10000000; - -/* klog monitoring: set when DEVICE_ADDED [type:1][subType:22] seen */ -static pthread_mutex_t g_klog_lock = PTHREAD_MUTEX_INITIALIZER; -static volatile uint64_t g_klog_vdev_id = 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 line parser — extract virtual DualSense DEVICE_ADDED deviceId - * ------------------------------------------------------------------ */ - -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) { - /* Looking for: DEVICE_ADDED ... [type:1][subType:22][capabilityBattery:0] - * That's the virtual DualSense device we created with VDA(type=3). */ - if (!strstr(line, "DEVICE_ADDED")) return; - if (!strstr(line, "subType:22")) return; - if (!strstr(line, "capabilityBattery:0")) return; - - /* Extract DeviceId:0x... or deviceId=0x... */ - const char *p = strstr(line, "DeviceId:0x"); - if (!p) p = strstr(line, "deviceId=0x"); - if (!p) return; - p += 11; /* skip "DeviceId:0x" or "deviceId=0x" */ - - uint64_t dev_id = parse_hex_str(p); - if (!dev_id) return; - - pthread_mutex_lock(&g_klog_lock); - if (!g_klog_vdev_id) { - g_klog_vdev_id = dev_id; - gp_log("klog: VDA device 0x%llx\n", (unsigned long long)dev_id); - } - pthread_mutex_unlock(&g_klog_lock); -} - -/* ------------------------------------------------------------------ - * klog capture thread — connects to klogsrv:3232 (since /dev/klog - * is unavailable to payload processes: errno=16 EBUSY) - * ------------------------------------------------------------------ */ - -static void *klog_capture_thread(void *arg) { - (void)arg; - char buf[512]; - char line[1024]; - size_t line_len = 0; - int fd = -1; - - /* Try /dev/klog first; fall back to klogsrv TCP on port 3232 */ - fd = open("/dev/klog", O_RDONLY); - if (fd < 0) { - gp_log("klog: /dev/klog errno=%d, trying klogsrv TCP 127.0.0.1:3232\n", errno); - struct sockaddr_in sin; - int sock = socket(AF_INET, SOCK_STREAM, 0); - if (sock < 0) { gp_log("klog: socket failed errno=%d\n", errno); 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 retry = 0; retry < 10; retry++) { - if (connect(sock, (struct sockaddr *)&sin, sizeof(sin)) == 0) { - fd = sock; - gp_log("klog: connected to klogsrv TCP 127.0.0.1:3232\n"); - break; - } - usleep(500000); - } - if (fd < 0) { - gp_log("klog: could not connect to klogsrv errno=%d\n", errno); - close(sock); - return NULL; - } - } - - while (1) { - ssize_t len = read(fd, buf, sizeof(buf)); - if (len < 0) { - if (errno == EINTR) continue; - gp_log("klog: read error errno=%d\n", errno); - 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; -} - -/* ------------------------------------------------------------------ - * Nintendo input parsing - * ------------------------------------------------------------------ */ - -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 input after Nintendo handshake - * [3]=right btns [4]=shared [5]=left btns [6-8]=lstick [9-11]=rstick */ -static void 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 input on button change only - * [1]=right btns [2]=shared [3]=HAT [4]=lx [5]=ly [6]=rx [7]=ry [8]=L/ZL */ -static void 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; -} - -/* ------------------------------------------------------------------ - * ugen device detection - * ------------------------------------------------------------------ */ - -static const char *UGEN_PATHS[] = { - "/dev/ugen2.2","/dev/ugen2.3","/dev/ugen2.4","/dev/ugen2.5", - "/dev/ugen1.2","/dev/ugen0.2","/dev/ugen0.3", -}; -#define N_UGEN_PATHS ((int)(sizeof(UGEN_PATHS)/sizeof(UGEN_PATHS[0]))) - -#define VID_NATIVE 0x2dc8u -#define PID_NATIVE 0x310bu -#define VID_SWITCH 0x057eu -#define PID_SWITCH 0x2009u - -static int ugen_find_target(const char **out_path, uint16_t *out_vid, uint16_t *out_pid) { - for (int i = 0; i < N_UGEN_PATHS; i++) { - /* Try O_RDWR — PS5 ugen requires RDWR for USB ioctls. - * USB_GET_DEVICEINFO returns ENOTTY with O_RDONLY on PS5. - * O_NONBLOCK prevents blocking if the device is busy. */ - int fd = open(UGEN_PATHS[i], O_RDWR | O_NONBLOCK); - if (fd < 0) { - if (errno != ENOENT) - gp_log("ugen_find: open(%s) errno=%d\n", UGEN_PATHS[i], errno); - continue; - } - struct usb_device_info di; - memset(&di, 0, sizeof(di)); - int ok = 0; - int r = ioctl(fd, USB_GET_DEVICEINFO, &di); - if (r != 0) { - if (errno == ENOTTY) { - if (strncmp(UGEN_PATHS[i], "/dev/ugen2.", 11) == 0) { - gp_log("ugen_find: %s USB_GET_DEVICEINFO ENOTTY, trying as Nintendo candidate\n", - UGEN_PATHS[i]); - if (out_vid) *out_vid = VID_SWITCH; - if (out_pid) *out_pid = PID_SWITCH; - if (out_path) *out_path = UGEN_PATHS[i]; - close(fd); - return 1; - } - gp_log("ugen_find: %s USB_GET_DEVICEINFO ENOTTY, ignored non-external bus\n", - UGEN_PATHS[i]); - close(fd); - continue; - } - if (errno != ENOENT) - gp_log("ugen_find: %s USB_GET_DEVICEINFO errno=%d\n", - UGEN_PATHS[i], errno); - close(fd); - continue; - } - if (r == 0) { - gp_log("ugen_find: %s VID=0x%04x PID=0x%04x\n", - UGEN_PATHS[i], di.udi_vendorNo, di.udi_productNo); - if ((di.udi_vendorNo == VID_NATIVE && di.udi_productNo == PID_NATIVE) || - (di.udi_vendorNo == VID_SWITCH && di.udi_productNo == PID_SWITCH)) { - 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; - } - } else { - /* ENOTTY = PS5 ugen doesn't support this ioctl in current state. - * Try a USB_FS_INIT probe to check if the device accepts ugen FS ops. - * If it does, assume it's our target (only one USB game controller expected). */ - struct usb_fs_endpoint ep_probe; - struct usb_fs_init init_probe; - memset(&ep_probe, 0, sizeof(ep_probe)); - memset(&init_probe, 0, sizeof(init_probe)); - init_probe.pEndpoints = &ep_probe; - init_probe.ep_index_max = 1; - int ir = ioctl(fd, USB_FS_INIT, &init_probe); - gp_log("ugen_find: %s DEVICEINFO errno=%d FS_INIT=%d\n", - UGEN_PATHS[i], errno, ir); - if (ir == 0) { - /* FS_INIT succeeded. Verify endpoint by opening ep=0x81 and - * checking max_pkt_length == 64 (8BitDo Nintendo IN ep is always 64). */ - struct usb_fs_open probe_open; - memset(&probe_open, 0, sizeof(probe_open)); - probe_open.ep_index = 0; - probe_open.ep_no = 0x81; - probe_open.max_bufsize = 64; - probe_open.max_frames = 1; - if (ioctl(fd, USB_FS_OPEN, &probe_open) == 0 && - probe_open.max_packet_length == 64) { - gp_log("ugen_find: %s ep=0x81 max_pkt=%u — 8BitDo!\n", - UGEN_PATHS[i], (unsigned)probe_open.max_packet_length); - struct usb_fs_close pc; memset(&pc,0,sizeof(pc)); pc.ep_index=0; - ioctl(fd, USB_FS_CLOSE, &pc); - if (out_vid) *out_vid = VID_SWITCH; - if (out_pid) *out_pid = PID_SWITCH; - if (out_path) *out_path = UGEN_PATHS[i]; - ok = 1; - } else { - gp_log("ugen_find: %s ep probe failed or wrong pkt size %u\n", - UGEN_PATHS[i], (unsigned)probe_open.max_packet_length); - } - struct usb_fs_uninit un; memset(&un,0,sizeof(un)); - ioctl(fd, USB_FS_UNINIT, &un); - } - } - close(fd); - if (ok) return 1; - } - return 0; -} - -/* ------------------------------------------------------------------ - * Inject pad data via direct VDI (Ghostpad path) - * ------------------------------------------------------------------ */ - -static void inject_pad(const ScePadData *pad) { - int32_t h = g_vdi_handle; - if (h < 0) return; - int vr = scePadVirtualDeviceInsertData(h, pad); - static uint32_t inject_count = 0; - inject_count++; - if (inject_count <= 8 || (inject_count % 300) == 0) { - gp_log("VDI inject #%u ret=0x%08x buttons=0x%08x ls=%u,%u rs=%u,%u lt=%u rt=%u\n", - inject_count, (uint32_t)vr, pad->buttons, - pad->leftStick.x, pad->leftStick.y, - pad->rightStick.x, pad->rightStick.y, - pad->analogButtons.l2, pad->analogButtons.r2); - } - static int logged = 0; - if (vr != 0 && !logged) { - gp_log("VDI error 0x%08x (logged once)\n", (uint32_t)vr); - logged = 1; - } -} - -static int usb_fs_send_out_report(int fd, struct usb_fs_endpoint *ep, - const uint8_t *data, uint32_t len, - const char *tag) { - void *out_buffers[1] = { (void *)data }; - uint32_t out_lengths[1] = { len }; - struct usb_fs_start start; - struct usb_fs_complete complete; - - ep->ppBuffer = out_buffers; - ep->pLength = out_lengths; - ep->nFrames = 1; - ep->timeout = 100; - 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) { - int e = errno; - gp_log("OUT %s START failed errno=%d ep_status=%d aFrames=%u len=%u\n", - tag, e, ep->status, ep->aFrames, len); - return -errno; - } - - for (int wait = 0; wait < 20; wait++) { - memset(&complete, 0, sizeof(complete)); - complete.ep_index = 1; - if (ioctl(fd, USB_FS_COMPLETE, &complete) == 0) { - gp_log("OUT %s complete ok wait=%d ep_status=%d aFrames=%u len=%u\n", - tag, wait, ep->status, ep->aFrames, out_lengths[0]); - return 0; - } - if (errno != EBUSY) { - int e = errno; - gp_log("OUT %s COMPLETE failed errno=%d ep_status=%d aFrames=%u len=%u\n", - tag, e, ep->status, ep->aFrames, out_lengths[0]); - return -errno; - } - usleep(50000); - } - - gp_log("OUT %s COMPLETE still busy ep_status=%d aFrames=%u len=%u\n", - tag, ep->status, ep->aFrames, out_lengths[0]); - return -EBUSY; -} - -static int usb_fs_send_out_cmd(int fd, struct usb_fs_endpoint *ep, uint8_t a, uint8_t b) { - uint8_t out_buf[2] = { a, b }; - char tag[16]; - snprintf(tag, sizeof(tag), "%02x %02x", a, b); - return usb_fs_send_out_report(fd, ep, out_buf, sizeof(out_buf), tag); -} - -static int nintendo_send_subcmd(int fd, struct usb_fs_endpoint *ep, - uint8_t *seq, uint8_t subcmd, - const uint8_t *data, uint32_t data_len) { - static const uint8_t neutral_rumble[8] = { - 0x00, 0x01, 0x40, 0x40, 0x00, 0x01, 0x40, 0x40 - }; - uint8_t out_buf[64]; - char tag[32]; - uint32_t len = 11 + data_len; - - if (len > sizeof(out_buf)) - return -EINVAL; - - memset(out_buf, 0, sizeof(out_buf)); - out_buf[0] = 0x01; - out_buf[1] = *seq & 0x0f; - memcpy(out_buf + 2, neutral_rumble, sizeof(neutral_rumble)); - out_buf[10] = subcmd; - if (data_len) - memcpy(out_buf + 11, data, data_len); - *seq = (uint8_t)((*seq + 1) & 0x0f); - - snprintf(tag, sizeof(tag), "subcmd %02x", subcmd); - return usb_fs_send_out_report(fd, ep, out_buf, len, tag); -} - -static void nintendo_maybe_poll_state(int fd, struct usb_fs_endpoint *ep, - uint8_t *seq, int enabled) { - static struct timeval last_poll; - struct timeval now; - long elapsed_ms; - - if (!enabled) - return; - gettimeofday(&now, NULL); - elapsed_ms = (now.tv_sec - last_poll.tv_sec) * 1000L + - (now.tv_usec - last_poll.tv_usec) / 1000L; - if (last_poll.tv_sec != 0 && elapsed_ms < 33) - return; - last_poll = now; - (void)nintendo_send_subcmd(fd, ep, seq, 0x00, NULL, 0); -} - -/* ------------------------------------------------------------------ - * USB HID reader thread - * ------------------------------------------------------------------ */ - -/* Handshake states for Nintendo Pro Controller USB init */ -#define HS_WAIT_81_01 0 /* waiting for 0x81 sub=0x01 */ -#define HS_WAIT_81_02 1 /* saw 0x81 0x01, sent [80 02], waiting for 0x81 0x02 */ -#define HS_STREAMING 2 /* subcmds sent, reading 0x00/0x30 data */ - -static void *usb_hid_thread(void *arg) { - (void)arg; - const char *dev_path = NULL; - uint16_t vid=0, pid=0; - struct usb_fs_endpoint eps[2]; - struct usb_fs_init init; - struct usb_fs_open fs_open; - struct usb_fs_open fs_out_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; - int out_opened = 0; - - gp_log("USB thread started\n"); - -retry_find: - dev_path = NULL; vid = 0; pid = 0; - out_opened = 0; - { int _scan=0; - while (!ugen_find_target(&dev_path, &vid, &pid)) { - if (_scan==0 || _scan==5) gp_log("USB scan #%d: no 8BitDo found\n",_scan); - _scan++; usleep(2000000); - } - } - gp_log("USB candidate at %s VID=0x%04x PID=0x%04x\n", dev_path, vid, pid); - { - const char *ctlr_name = - (vid==VID_SWITCH && pid==PID_SWITCH) ? "8BitDo (Nintendo Pro mode)" : - (vid==VID_NATIVE && pid==PID_NATIVE) ? "8BitDo (Native mode)" : - "Unknown controller"; - notify("Ghostcontrol: Detected %s", ctlr_name); - } - - fd = open(dev_path, O_RDWR); - if (fd < 0) { gp_log("open failed errno=%d\n", errno); goto retry_find; } - - /* Probe-matching two-stage USB FS setup. */ - 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("USB_FS_INIT pass1 failed errno=%d\n",errno); - close(fd); goto retry_find; - } - - int iface0 = 0; - int dr0 = ioctl(fd, USB_IFACE_DRIVER_DETACH, &iface0); - int de0 = (dr0 == 0) ? 0 : errno; - int iface1 = 1; - int dr1 = ioctl(fd, USB_IFACE_DRIVER_DETACH, &iface1); - int de1 = (dr1 == 0) ? 0 : errno; - gp_log("USB_IFACE_DRIVER_DETACH pass1(0)=%d errno=%d, pass1(1)=%d errno=%d\n", - dr0, de0, dr1, de1); - - memset(&fs_open,0,sizeof(fs_open)); - fs_open.ep_index=0; fs_open.ep_no=0x81; - fs_open.max_bufsize=sizeof(buf); fs_open.max_frames=1; - if (ioctl(fd,USB_FS_OPEN,&fs_open)!=0) { - gp_log("USB_FS_OPEN pass1 IN failed errno=%d\n",errno); - goto uninit_retry; - } - gp_log("USB_FS_OPEN pass1 IN ok max_pkt=%u\n", - (unsigned)fs_open.max_packet_length); - - memset(&uninit,0,sizeof(uninit)); ioctl(fd,USB_FS_UNINIT,&uninit); - close(fd); fd = -1; - - fd = open(dev_path, O_RDWR); - if (fd < 0) { gp_log("reopen failed errno=%d\n", errno); goto retry_find; } - - iface0 = 0; - dr0 = ioctl(fd, USB_IFACE_DRIVER_DETACH, &iface0); - de0 = (dr0 == 0) ? 0 : errno; - iface1 = 1; - dr1 = ioctl(fd, USB_IFACE_DRIVER_DETACH, &iface1); - de1 = (dr1 == 0) ? 0 : errno; - gp_log("USB_IFACE_DRIVER_DETACH pass2(0)=%d errno=%d, pass2(1)=%d errno=%d\n", - dr0, de0, dr1, de1); - - 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) { - gp_log("USB_FS_INIT pass2 failed errno=%d\n",errno); - close(fd); goto retry_find; - } - - memset(&fs_open,0,sizeof(fs_open)); - fs_open.ep_index=0; fs_open.ep_no=0x81; - fs_open.max_bufsize=sizeof(buf); fs_open.max_frames=1; - if (ioctl(fd,USB_FS_OPEN,&fs_open)!=0) { - gp_log("USB_FS_OPEN pass2 IN failed errno=%d\n",errno); - goto uninit_retry; - } - gp_log("USB_FS_OPEN pass2 IN ok max_pkt=%u\n", - (unsigned)fs_open.max_packet_length); - if (fs_open.max_packet_length != 64) { - gp_log("USB_FS_OPEN IN max_pkt=%u is not controller HID, rejecting %s\n", - (unsigned)fs_open.max_packet_length, dev_path); - goto reinit; - } - - buffers[0]=buf; lengths[0]=sizeof(buf); - 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; - - out_opened = 0; - memset(&fs_out_open,0,sizeof(fs_out_open)); - fs_out_open.ep_index=1; fs_out_open.ep_no=0x02; - fs_out_open.max_bufsize=64; fs_out_open.max_frames=1; - if (ioctl(fd,USB_FS_OPEN,&fs_out_open)==0) { - out_opened = 1; - gp_log("USB_FS_OPEN OUT ep=0x02 ok max_pkt=%u\n", - (unsigned)fs_out_open.max_packet_length); - if (pid == PID_SWITCH && fs_out_open.max_packet_length != 64) { - gp_log("USB_FS_OPEN OUT max_pkt=%u is not Nintendo controller OUT, rejecting %s\n", - (unsigned)fs_out_open.max_packet_length, dev_path); - goto reinit; - } - } else { - gp_log("USB_FS_OPEN OUT ep=0x02 failed errno=%d\n", errno); - } - - /* Proven init sequence (from usb_handshake_probe v11): - * [80 02]+[80 04] blindly, then respond to 0x81 0x01→[80 02] and - * 0x81 0x02→[80 04]+subcmds(0x40,0x48,0x30,0x03 LAST). */ - uint8_t nintendo_seq = 1; - int hs_state = HS_WAIT_81_01; - - if (pid == PID_SWITCH && out_opened) { - int c0 = usb_fs_send_out_cmd(fd, &eps[1], 0x80, 0x02); - usleep(30000); - int c1 = usb_fs_send_out_cmd(fd, &eps[1], 0x80, 0x04); - gp_log("Nintendo blind [80 02]=%d [80 04]=%d; handshake state machine starting\n", c0, c1); - usleep(50000); - } else if (pid == PID_SWITCH) { - gp_log("Nintendo OUT not open — skipping handshake, trying native read\n"); - hs_state = HS_STREAMING; - } - - uint32_t pkt_count = 0; - int usb_ready_notified = 0; - - while (1) { - memset(buf,0,sizeof(buf)); - lengths[0]=sizeof(buf); 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("START errno=%d — device gone, reinit\n",errno); - goto reinit; - } else { - gp_log("START fatal errno=%d\n",errno); goto reinit; - } - continue; - } - - int complete_ok = 0; - int complete_errno = 0; - int complete_wait = 0; - /* Poll COMPLETE for up to 300ms (timeout=200ms + margin) */ - for (complete_wait = 0; complete_wait < 10; complete_wait++) { - memset(&complete,0,sizeof(complete)); complete.ep_index=0; - if (ioctl(fd,USB_FS_COMPLETE,&complete)==0) { - complete_ok = 1; - break; - } - complete_errno = errno; - if (complete_errno==ENXIO||complete_errno==ENOTTY) { - gp_log("COMPLETE errno=%d — device gone, reinit\n",complete_errno); - goto reinit; - } - if (complete_errno != EBUSY) break; - usleep(50000); - } - if (!complete_ok) { - static uint32_t compl_err=0; compl_err++; - if (compl_err==1||compl_err%20==0) - gp_log("COMPLETE failed errno=%d wait=%d count=%u\n", - complete_errno,complete_wait,compl_err); - memset(&stop,0,sizeof(stop)); stop.ep_index=0; - ioctl(fd,USB_FS_STOP,&stop); - continue; - } - - if (lengths[0] < 1) continue; - uint8_t rid = buf[0]; - uint32_t len = lengths[0]; - - if (pkt_count < 10 || (pkt_count % 300 == 0)) { - gp_log("PKT #%u hs=%d id=0x%02x len=%u: " - "%02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x\n", - pkt_count, hs_state, rid, len, - buf[0],buf[1],buf[2],buf[3],buf[4],buf[5], - buf[6],buf[7],buf[8],buf[9],buf[10],buf[11]); - } - pkt_count++; - - ScePadData pad; - memset(&pad, 0, sizeof(pad)); - pad.quat.w = 1.0f; - - /* Data packets: rid=0x00 (8BitDo streaming format, same layout as 0x30) - * or rid=0x30 (first transition packet after subcmd 0x03). - * Require buf[1]!=0 (non-zero timer) — all-zeros is a USB buffer - * artifact after reconnect, not real data. */ - if ((rid == 0x00 || rid == 0x30) && len >= 12) { - if (rid == 0x00 && buf[1] == 0) { - /* All-zeros garbage packet — skip, handshake not done yet */ - continue; - } - if (hs_state != HS_STREAMING) { - gp_log("rid=0x%02x timer=0x%02x — already streaming, skip handshake\n", - rid, buf[1]); - hs_state = HS_STREAMING; - } - if (!usb_ready_notified) { - notify("Ghostcontrol: Controller connected and streaming"); - usb_ready_notified = 1; - } - parse_0x30(buf, &pad); - if (g_vdi_ready) inject_pad(&pad); - continue; - } - - if (rid == 0x3f && len >= 9) { - if (!usb_ready_notified) { - notify("Ghostcontrol: Controller connected and streaming"); - usb_ready_notified = 1; - } - parse_0x3f(buf, &pad); - if (g_vdi_ready) inject_pad(&pad); - continue; - } - - if (rid == 0x21 && len >= 12) { - gp_log("0x21 ACK subcmd=0x%02x hs=%d\n", (buf[12]&0x7f), hs_state); - /* ACK for subcmd 0x03 might carry first input data — try parsing */ - parse_0x30(buf, &pad); - if (g_vdi_ready && hs_state==HS_STREAMING) inject_pad(&pad); - continue; - } - - if (rid == 0x81) { - /* Nintendo handshake. State machine from probe v11: - * 0x81 0x01 → [80 02] → wait for 0x81 0x02 - * 0x81 0x02 → [80 04] → send subcmds 0x40,0x48,0x30,0x03 → streaming - * If 0x81 arrives while HS_STREAMING (reconnect): reset and redo handshake. */ - if (hs_state == HS_STREAMING) { - gp_log("0x81 sub=0x%02x while streaming — reconnect, restarting handshake\n", buf[1]); - hs_state = HS_WAIT_81_01; - usb_ready_notified = 0; - } - if (buf[1] == 0x01 && hs_state == HS_WAIT_81_01) { - int hr = out_opened ? usb_fs_send_out_cmd(fd,&eps[1],0x80,0x02) : -ENODEV; - gp_log("0x81 sub=0x01 → [80 02] ret=%d\n",hr); - hs_state = HS_WAIT_81_02; - } else if (buf[1] == 0x02 && hs_state <= HS_WAIT_81_02) { - /* Complete handshake and send full subcmd init sequence */ - int hr = out_opened ? usb_fs_send_out_cmd(fd,&eps[1],0x80,0x04) : -ENODEV; - gp_log("0x81 sub=0x02 → [80 04] ret=%d; sending subcmds\n",hr); - usleep(50000); - if (out_opened) { - uint8_t d1[]={0x01}; nintendo_send_subcmd(fd,&eps[1],&nintendo_seq,0x40,d1,1); usleep(50000); - uint8_t d2[]={0x01}; nintendo_send_subcmd(fd,&eps[1],&nintendo_seq,0x48,d2,1); usleep(50000); - uint8_t d3[]={0x01}; nintendo_send_subcmd(fd,&eps[1],&nintendo_seq,0x30,d3,1); usleep(50000); - uint8_t d4[]={0x30}; nintendo_send_subcmd(fd,&eps[1],&nintendo_seq,0x03,d4,1); - gp_log("Subcmds sent (0x40,0x48,0x30,0x03) — expecting 0x00 stream\n"); - } - hs_state = HS_STREAMING; - } else { - gp_log("0x81 sub=0x%02x hs=%d (ignored)\n",buf[1],hs_state); - } - continue; - } - - /* Unknown packet — log and skip */ - if (pkt_count < 20) { - gp_log("UNKNOWN rid=0x%02x len=%u hs=%d\n",rid,len,hs_state); - } - } - -reinit: - if (usb_ready_notified) { - notify("Ghostcontrol: Controller disconnected"); - 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_retry: - memset(&uninit,0,sizeof(uninit)); ioctl(fd,USB_FS_UNINIT,&uninit); - close(fd); gp_log("USB: reinit in 300ms\n"); usleep(300000); - goto retry_find; -} - -/* ------------------------------------------------------------------ - * Credential elevation - * ------------------------------------------------------------------ */ - -static void elevate_credentials(void) { - pid_t mypid = getpid(); - uint8_t caps[16] = { - 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, - 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff - }; - kernel_set_ucred_authid(mypid, 0x3800000000010003l); - kernel_set_ucred_caps(mypid, caps); -} - -/* ------------------------------------------------------------------ - * main — follows Ghostpad's VDA + klog + force_bind + direct VDI path - * ------------------------------------------------------------------ */ - -int main(void) { - int32_t userId = -1, fgUser = -1; - int ret; - - ghostpad_status_log_reset(); - gp_log("Ghost-Control v3 starting\n"); - notify("Ghostcontrol - by StonedModder"); - - /* Kill previous instance via pidfile */ - { - int pfd = open(PID_PATH, O_RDONLY); - if (pfd >= 0) { - char pbuf[16] = {0}; - read(pfd, pbuf, sizeof(pbuf)-1); - close(pfd); - pid_t old = (pid_t)atoi(pbuf); - if (old > 0 && old != getpid()) { - gp_log("Killing previous instance 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 pbuf[16]; - snprintf(pbuf, sizeof(pbuf), "%d", getpid()); - write(pfd2, pbuf, strlen(pbuf)); - close(pfd2); - } - } - - /* User service */ - sceUserServiceInitialize(NULL); - sceUserServiceGetInitialUser(&userId); - sceUserServiceGetForegroundUser(&fgUser); - gp_log("userId=0x%08x fgUser=0x%08x\n", (uint32_t)userId, (uint32_t)fgUser); - - /* g_inject_uid: actual foreground user — used for force_bind. - * userId is clamped to [0x10000000,0x1000000F] for direct scePad calls. */ - g_inject_uid = (fgUser > 0) ? fgUser : userId; - if ((uint32_t)userId < 0x10000000u || (uint32_t)userId > 0x1000000Fu) - userId = 0x10000000; - gp_log("inject_uid=0x%08x direct_uid=0x%08x\n", - (uint32_t)g_inject_uid, (uint32_t)userId); - - elevate_credentials(); - - ret = scePadInit(); - gp_log("scePadInit: 0x%08x\n", ret); - ret = scePadSetProcessPrivilege(1); - gp_log("scePadSetProcessPrivilege: 0x%08x\n", ret); - - /* Clean orphaned virtual devices from previous runs */ - for (int dh = 0; dh < 64; dh++) { - int32_t dr = scePadVirtualDeviceDeleteDevice(dh); - if (dr == 0) gp_log("deleteDevice(%d): ok\n", dh); - } - - /* Start klog monitoring BEFORE VDA so we never miss the DEVICE_ADDED event */ - 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 VDA fires the event */ - - /* ── VDA: create virtual DualSense ────────────────────────────── - * PS5 VDA uses userId=1 (not the actual user ID). - * Returns 0x803b0006 on PS5 — device IS created despite error code. - * If ret >= 0, it may encode a handle in the return value or param fields. */ - struct { int32_t size; int32_t userId; int32_t pad[6]; } vdp; - const int32_t SENTINEL = (int32_t)0xDEADBEEF; - memset(&vdp, 0, sizeof(vdp)); - vdp.size = (int32_t)sizeof(vdp); - vdp.userId = 1; /* PS5 VDA always uses userId=1 */ - for (int k=0; k<6; k++) vdp.pad[k] = SENTINEL; - - ret = scePadVirtualDeviceAddDevice(&vdp, VIRTUAL_DEVICE_TYPE_DUALSENSE); - gp_log("VDA ret=0x%08x pad[0-3]=0x%08x 0x%08x 0x%08x 0x%08x\n", - (uint32_t)ret, (uint32_t)vdp.pad[0], (uint32_t)vdp.pad[1], - (uint32_t)vdp.pad[2], (uint32_t)vdp.pad[3]); - - /* Check if VDA returned a direct handle (positive return value) */ - int32_t direct_handle = -1; - if (ret > 0) { - direct_handle = ret; - gp_log("VDA direct handle from ret: %d\n", direct_handle); - } - for (int k=0; k<6; k++) { - if (vdp.pad[k] != SENTINEL && vdp.pad[k] > 0) { - gp_log("VDA handle in pad[%d]=%d\n", k, vdp.pad[k]); - if (direct_handle < 0) direct_handle = vdp.pad[k]; - break; - } - } - - /* ── Wait for klog to confirm DEVICE_ADDED (up to 10s) ─────────── */ - gp_log("Waiting for VDA device ID (klog + GetHandle fallback)...\n"); - uint64_t vdev_id = 0; - for (int t = 0; t < 100 && !vdev_id; t++) { - usleep(100000); - pthread_mutex_lock(&g_klog_lock); - vdev_id = g_klog_vdev_id; - pthread_mutex_unlock(&g_klog_lock); - } - - int32_t vdi_handle = -1; - - if (vdev_id) { - gp_log("VDA device from klog: 0x%llx\n", (unsigned long long)vdev_id); - vdi_handle = (int32_t)(vdev_id & 0xffffffffu); - /* force_bind triggers the PS5 user-assignment dialog. - * Without it the PS5 silently auto-assigns with no screen shown. - * The user then presses a button on the controller to confirm. */ - int br = shellui_pad_force_bind(vdev_id, g_inject_uid); - gp_log("force_bind(0x%llx, 0x%08x) ret=%d — dialog should appear\n", - (unsigned long long)vdev_id, (uint32_t)g_inject_uid, br); - - } else if (direct_handle >= 0) { - gp_log("klog timeout — using direct VDA handle %d\n", direct_handle); - vdi_handle = direct_handle; - } else { - /* Last resort: GetHandle scan for existing virtual DualSense */ - gp_log("klog timeout — GetHandle scan...\n"); - static const int32_t uids[] = {1, 0x10000000, (int32_t)0xffffffff}; - for (int ui=0; ui<3 && vdi_handle<0; ui++) - for (int idx=0; idx<8 && vdi_handle<0; idx++) { - vdi_handle = scePadGetHandle(uids[ui], 3, idx); - if (vdi_handle >= 0) - gp_log("GetHandle uid=0x%08x idx=%d h=%d\n", - (uint32_t)uids[ui], idx, vdi_handle); - } - } - - if (vdi_handle >= 0) { - g_vdi_handle = vdi_handle; - g_vdi_ready = 1; - gp_log("VDI READY handle=0x%x\n", (uint32_t)vdi_handle); - notify("Ghostcontrol: Ready — select your user with the controller"); - } else { - gp_log("ERROR: no VDI handle found\n"); - notify("Ghostcontrol: ERROR — no VDI handle"); - } - - /* Start USB HID reader thread */ - pthread_t usb_tid; - if (pthread_create(&usb_tid, NULL, usb_hid_thread, NULL) == 0) { - pthread_detach(usb_tid); - gp_log("USB thread created\n"); - } - - /* Keep-alive + periodic status */ - uint32_t tick = 0; - while (1) { - usleep(1000000); - tick++; - if (tick % 10 == 0) { - gp_log("alive tick=%u vdi_ready=%d handle=0x%x\n", - tick, g_vdi_ready, (uint32_t)g_vdi_handle); - } - } - return 0; -} diff --git a/ghost-control-ps5.elf b/ghost-control-ps5.elf deleted file mode 100644 index 15ec4c4..0000000 Binary files a/ghost-control-ps5.elf and /dev/null differ diff --git a/Makefile b/payload/Makefile similarity index 63% rename from Makefile rename to payload/Makefile index 9739342..e6de9a5 100644 --- a/Makefile +++ b/payload/Makefile @@ -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) diff --git a/payload/controller_nintendo.c b/payload/controller_nintendo.c new file mode 100644 index 0000000..f4f0811 --- /dev/null +++ b/payload/controller_nintendo.c @@ -0,0 +1,161 @@ +#include "controller_nintendo.h" +#include "usb_helpers.h" +#include +#include + +#ifdef __PROSPERO__ +#include +#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; +} diff --git a/payload/controller_nintendo.h b/payload/controller_nintendo.h new file mode 100644 index 0000000..514a2f3 --- /dev/null +++ b/payload/controller_nintendo.h @@ -0,0 +1,27 @@ +#pragma once +#include +#include +#include +#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); diff --git a/payload/controller_xbox.c b/payload/controller_xbox.c new file mode 100644 index 0000000..24f3552 --- /dev/null +++ b/payload/controller_xbox.c @@ -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 +#include +#include +#include + +#ifdef __PROSPERO__ +#include +#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; +} diff --git a/payload/controller_xbox.h b/payload/controller_xbox.h new file mode 100644 index 0000000..c3eb93b --- /dev/null +++ b/payload/controller_xbox.h @@ -0,0 +1,51 @@ +#pragma once +#include +#include +#include +#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); diff --git a/payload/gc_main.c b/payload/gc_main.c new file mode 100644 index 0000000..42f9491 --- /dev/null +++ b/payload/gc_main.c @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#ifdef __PROSPERO__ +#include +#include +#include +#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=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; +} diff --git a/payload/gc_types.h b/payload/gc_types.h new file mode 100644 index 0000000..739080e --- /dev/null +++ b/payload/gc_types.h @@ -0,0 +1,45 @@ +#pragma once +#include + +/* 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; diff --git a/shellui_pad.c b/payload/shellui_pad.c similarity index 100% rename from shellui_pad.c rename to payload/shellui_pad.c diff --git a/shellui_pad.h b/payload/shellui_pad.h similarity index 100% rename from shellui_pad.h rename to payload/shellui_pad.h diff --git a/payload/usb_helpers.c b/payload/usb_helpers.c new file mode 100644 index 0000000..79bdd83 --- /dev/null +++ b/payload/usb_helpers.c @@ -0,0 +1,56 @@ +#include "usb_helpers.h" +#include +#include +#include +#include +#include + +#ifdef __PROSPERO__ +#include +#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); +} diff --git a/payload/usb_helpers.h b/payload/usb_helpers.h new file mode 100644 index 0000000..fda33d2 --- /dev/null +++ b/payload/usb_helpers.h @@ -0,0 +1,8 @@ +#pragma once +#include +#include +#include + +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); diff --git a/xboxSeriesSButtonBits.md b/xboxSeriesSButtonBits.md new file mode 100644 index 0000000..b5480ed --- /dev/null +++ b/xboxSeriesSButtonBits.md @@ -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.