From 191bc40f7069fb09c3cbb709cb7125019315e9c9 Mon Sep 17 00:00:00 2001 From: Fred Hallock Date: Sat, 25 Jan 2025 22:48:58 -0500 Subject: [PATCH] xid: Add Xbox Controller S --- config_spec.yml | 4 + data/controller_mask_s.png | Bin 0 -> 46409 bytes data/controller_mask_s.svg | 86 +++++++++++ data/meson.build | 1 + hw/xbox/meson.build | 1 + hw/xbox/xid-gamepad.c | 293 +++++++++++++++++++++++++++++++++++++ hw/xbox/xid.c | 280 +---------------------------------- hw/xbox/xid.h | 137 +++++++++++++++++ ui/xemu-input.c | 37 ++++- ui/xemu-input.h | 7 + ui/xui/gl-helpers.cc | 191 +++++++++++++++++++++++- ui/xui/main-menu.cc | 43 ++++++ 12 files changed, 802 insertions(+), 278 deletions(-) create mode 100644 data/controller_mask_s.png create mode 100644 data/controller_mask_s.svg create mode 100644 hw/xbox/xid-gamepad.c create mode 100644 hw/xbox/xid.h diff --git a/config_spec.yml b/config_spec.yml index 087d255fae..9d96b1f48f 100644 --- a/config_spec.yml +++ b/config_spec.yml @@ -21,9 +21,13 @@ general: input: bindings: + port1_driver: string port1: string + port2_driver: string port2: string + port3_driver: string port3: string + port4_driver: string port4: string peripherals: port1: diff --git a/data/controller_mask_s.png b/data/controller_mask_s.png new file mode 100644 index 0000000000000000000000000000000000000000..8406effbca949e4d0de7059ffe7611ca728f525a GIT binary patch literal 46409 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 literal 0 HcmV?d00001 diff --git a/data/controller_mask_s.svg b/data/controller_mask_s.svg new file mode 100644 index 0000000000..a3cbf84c23 --- /dev/null +++ b/data/controller_mask_s.svg @@ -0,0 +1,86 @@ + + + Controller S + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + X + + + + B + + + + A + + + + Y + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/data/meson.build b/data/meson.build index e1a7ebedf4..bb2084bc1c 100644 --- a/data/meson.build +++ b/data/meson.build @@ -1,5 +1,6 @@ pfiles = [ 'controller_mask.png', + 'controller_mask_s.png', 'xmu_mask.png', 'logo_sdf.png', 'xemu_64x64.png', diff --git a/hw/xbox/meson.build b/hw/xbox/meson.build index b8199e55a8..dce2d3729e 100644 --- a/hw/xbox/meson.build +++ b/hw/xbox/meson.build @@ -16,6 +16,7 @@ specific_ss.add(files( 'xbox_pci.c', 'xid.c', 'xblc.c', + 'xid-gamepad.c', )) subdir('nv2a') subdir('mcpx') diff --git a/hw/xbox/xid-gamepad.c b/hw/xbox/xid-gamepad.c new file mode 100644 index 0000000000..5e27e4912a --- /dev/null +++ b/hw/xbox/xid-gamepad.c @@ -0,0 +1,293 @@ +/* + * QEMU USB XID Devices + * + * Copyright (c) 2013 espes + * Copyright (c) 2017 Jannik Vogel + * Copyright (c) 2018-2021 Matt Borgerson + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#include "xid.h" + +// #define DEBUG_XID +#ifdef DEBUG_XID +#define DPRINTF printf +#else +#define DPRINTF(...) +#endif + +#define USB_VENDOR_MICROSOFT 0x045e + +#define GAMEPAD_IN_ENDPOINT_ID 0x02 +#define GAMEPAD_OUT_ENDPOINT_ID 0x02 + +#define USB_XID(obj) \ + OBJECT_CHECK(USBXIDGamepadState, (obj), TYPE_USB_XID_GAMEPAD) +#define USB_XID_S(obj) \ + OBJECT_CHECK(USBXIDGamepadState, (obj), TYPE_USB_XID_GAMEPAD_S) + +static const USBDescIface desc_iface_xbox_gamepad = { + .bInterfaceNumber = 0, + .bNumEndpoints = 2, + .bInterfaceClass = USB_CLASS_XID, + .bInterfaceSubClass = 0x42, + .bInterfaceProtocol = 0x00, + .eps = + (USBDescEndpoint[]){ + { + .bEndpointAddress = USB_DIR_IN | GAMEPAD_IN_ENDPOINT_ID, + .bmAttributes = USB_ENDPOINT_XFER_INT, + .wMaxPacketSize = 0x20, + .bInterval = 4, + }, + { + .bEndpointAddress = USB_DIR_OUT | GAMEPAD_OUT_ENDPOINT_ID, + .bmAttributes = USB_ENDPOINT_XFER_INT, + .wMaxPacketSize = 0x20, + .bInterval = 4, + }, + }, +}; + +static const USBDescDevice desc_device_xbox_gamepad = { + .bcdUSB = 0x0110, + .bMaxPacketSize0 = 0x40, + .bNumConfigurations = 1, + .confs = + (USBDescConfig[]){ + { + .bNumInterfaces = 1, + .bConfigurationValue = 1, + .bmAttributes = USB_CFG_ATT_ONE, + .bMaxPower = 50, + .nif = 1, + .ifs = &desc_iface_xbox_gamepad, + }, + }, +}; + +static const USBDesc desc_xbox_gamepad = { + .id = { + .idVendor = USB_VENDOR_MICROSOFT, + .idProduct = 0x0202, + .bcdDevice = 0x0100, + .iManufacturer = STR_MANUFACTURER, + .iProduct = STR_PRODUCT, + .iSerialNumber = STR_SERIALNUMBER, + }, + .full = &desc_device_xbox_gamepad, + .str = desc_strings, +}; + +static const USBDesc desc_xbox_gamepad_s = { + .id = { + .idVendor = USB_VENDOR_MICROSOFT, + .idProduct = 0x0289, + .bcdDevice = 0x0100, + .iManufacturer = STR_MANUFACTURER, + .iProduct = STR_PRODUCT, + .iSerialNumber = STR_SERIALNUMBER, + }, + .full = &desc_device_xbox_gamepad, + .str = desc_strings, +}; + +static const XIDDesc desc_xid_xbox_gamepad = { + .bLength = 0x10, + .bDescriptorType = USB_DT_XID, + .bcdXid = 0x100, + .bType = XID_DEVICETYPE_GAMEPAD, + .bSubType = XID_DEVICESUBTYPE_GAMEPAD, + .bMaxInputReportSize = 20, + .bMaxOutputReportSize = 6, + .wAlternateProductIds = { 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF }, +}; + +static const XIDDesc desc_xid_xbox_gamepad_s = { + .bLength = 0x10, + .bDescriptorType = USB_DT_XID, + .bcdXid = 0x100, + .bType = XID_DEVICETYPE_GAMEPAD, + .bSubType = XID_DEVICESUBTYPE_GAMEPAD_S, + .bMaxInputReportSize = 20, + .bMaxOutputReportSize = 6, + .wAlternateProductIds = { 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF }, +}; + +static void usb_xid_gamepad_handle_data(USBDevice *dev, USBPacket *p) +{ + USBXIDGamepadState *s = DO_UPCAST(USBXIDGamepadState, dev, dev); + + DPRINTF("xid handle_gamepad_data 0x%x %d 0x%zx\n", p->pid, p->ep->nr, + p->iov.size); + + switch (p->pid) { + case USB_TOKEN_IN: + if (p->ep->nr == GAMEPAD_IN_ENDPOINT_ID) { + update_input(s); + usb_packet_copy(p, &s->in_state, s->in_state.bLength); + } else { + assert(false); + } + break; + case USB_TOKEN_OUT: + if (p->ep->nr == GAMEPAD_OUT_ENDPOINT_ID) { + usb_packet_copy(p, &s->out_state, s->out_state.length); + update_output(s); + } else { + assert(false); + } + break; + default: + p->status = USB_RET_STALL; + assert(false); + break; + } +} + +static void usb_xid_gamepad_class_initfn(ObjectClass *klass, void *data) +{ + USBDeviceClass *uc = USB_DEVICE_CLASS(klass); + + uc->handle_reset = usb_xid_handle_reset; + uc->handle_control = usb_xid_handle_control; + uc->handle_data = usb_xid_gamepad_handle_data; + // uc->handle_destroy = usb_xid_handle_destroy; + uc->handle_attach = usb_desc_attach; +} + +static void usb_xbox_gamepad_realize(USBDevice *dev, Error **errp) +{ + USBXIDGamepadState *s = USB_XID(dev); + usb_desc_create_serial(dev); + usb_desc_init(dev); + s->intr = usb_ep_get(dev, USB_TOKEN_IN, 2); + + s->in_state.bLength = sizeof(s->in_state); + s->in_state.bReportId = 0; + + s->out_state.length = sizeof(s->out_state); + s->out_state.report_id = 0; + + s->xid_desc = &desc_xid_xbox_gamepad; + + memset(&s->in_state_capabilities, 0xFF, sizeof(s->in_state_capabilities)); + s->in_state_capabilities.bLength = sizeof(s->in_state_capabilities); + s->in_state_capabilities.bReportId = 0; + + memset(&s->out_state_capabilities, 0xFF, sizeof(s->out_state_capabilities)); + s->out_state_capabilities.length = sizeof(s->out_state_capabilities); + s->out_state_capabilities.report_id = 0; +} + +static void usb_xbox_gamepad_s_realize(USBDevice *dev, Error **errp) +{ + USBXIDGamepadState *s = USB_XID_S(dev); + usb_desc_create_serial(dev); + usb_desc_init(dev); + s->intr = usb_ep_get(dev, USB_TOKEN_IN, 2); + + s->in_state.bLength = sizeof(s->in_state); + s->in_state.bReportId = 0; + + s->out_state.length = sizeof(s->out_state); + s->out_state.report_id = 0; + + s->xid_desc = &desc_xid_xbox_gamepad_s; + + memset(&s->in_state_capabilities, 0xFF, sizeof(s->in_state_capabilities)); + s->in_state_capabilities.bLength = sizeof(s->in_state_capabilities); + s->in_state_capabilities.bReportId = 0; + + memset(&s->out_state_capabilities, 0xFF, sizeof(s->out_state_capabilities)); + s->out_state_capabilities.length = sizeof(s->out_state_capabilities); + s->out_state_capabilities.report_id = 0; +} + +static Property xid_properties[] = { + DEFINE_PROP_UINT8("index", USBXIDGamepadState, device_index, 0), + DEFINE_PROP_END_OF_LIST(), +}; + +static const VMStateDescription vmstate_usb_xbox = { + .name = TYPE_USB_XID_GAMEPAD, + .version_id = 1, + .minimum_version_id = 1, + .fields = (VMStateField[]){ VMSTATE_USB_DEVICE(dev, USBXIDGamepadState), + // FIXME + VMSTATE_END_OF_LIST() }, +}; + +static const VMStateDescription vmstate_usb_xbox_s = { + .name = TYPE_USB_XID_GAMEPAD_S, + .minimum_version_id = 1, + .fields = (VMStateField[]){ VMSTATE_USB_DEVICE(dev, USBXIDGamepadState), + // FIXME + VMSTATE_END_OF_LIST() }, +}; + +static void usb_xbox_gamepad_class_initfn(ObjectClass *klass, void *data) +{ + DeviceClass *dc = DEVICE_CLASS(klass); + USBDeviceClass *uc = USB_DEVICE_CLASS(klass); + + uc->product_desc = "Microsoft Xbox Controller"; + uc->usb_desc = &desc_xbox_gamepad; + uc->realize = usb_xbox_gamepad_realize; + uc->unrealize = usb_xbox_gamepad_unrealize; + usb_xid_gamepad_class_initfn(klass, data); + set_bit(DEVICE_CATEGORY_INPUT, dc->categories); + dc->vmsd = &vmstate_usb_xbox; + device_class_set_props(dc, xid_properties); + dc->desc = "Microsoft Xbox Controller"; +} + +static void usb_xbox_gamepad_s_class_initfn(ObjectClass *klass, void *data) +{ + DeviceClass *dc = DEVICE_CLASS(klass); + USBDeviceClass *uc = USB_DEVICE_CLASS(klass); + + uc->product_desc = "Microsoft Xbox Controller S"; + uc->usb_desc = &desc_xbox_gamepad_s; + uc->realize = usb_xbox_gamepad_s_realize; + uc->unrealize = usb_xbox_gamepad_unrealize; + usb_xid_gamepad_class_initfn(klass, data); + set_bit(DEVICE_CATEGORY_INPUT, dc->categories); + dc->vmsd = &vmstate_usb_xbox_s; + device_class_set_props(dc, xid_properties); + dc->desc = "Microsoft Xbox Controller S"; +} + +static const TypeInfo usb_xbox_gamepad_info = { + .name = TYPE_USB_XID_GAMEPAD, + .parent = TYPE_USB_DEVICE, + .instance_size = sizeof(USBXIDGamepadState), + .class_init = usb_xbox_gamepad_class_initfn, +}; + +static const TypeInfo usb_xbox_gamepad_s_info = { + .name = TYPE_USB_XID_GAMEPAD_S, + .parent = TYPE_USB_DEVICE, + .instance_size = sizeof(USBXIDGamepadState), + .class_init = usb_xbox_gamepad_s_class_initfn, +}; + +static void usb_xid_register_types(void) +{ + type_register_static(&usb_xbox_gamepad_info); + type_register_static(&usb_xbox_gamepad_s_info); +} + +type_init(usb_xid_register_types) \ No newline at end of file diff --git a/hw/xbox/xid.c b/hw/xbox/xid.c index 86f34f7de2..c37142bc0d 100644 --- a/hw/xbox/xid.c +++ b/hw/xbox/xid.c @@ -19,22 +19,7 @@ * License along with this library; if not, see . */ -#include "qemu/osdep.h" -#include "hw/qdev-properties.h" -#include "migration/vmstate.h" -#include "sysemu/sysemu.h" -#include "hw/hw.h" -#include "ui/console.h" -#include "hw/usb.h" -#include "hw/usb/desc.h" -#include "ui/xemu-input.h" - -//#define DEBUG_XID -#ifdef DEBUG_XID -#define DPRINTF printf -#else -#define DPRINTF(...) -#endif +#include "xid.h" /* * http://xbox-linux.cvs.sourceforge.net/viewvc/xbox-linux/kernel-2.6/drivers/usb/input/xpad.c @@ -42,156 +27,18 @@ * http://euc.jp/periphs/xbox-pad-desc.txt */ -#define USB_CLASS_XID 0x58 -#define USB_DT_XID 0x42 - -#define HID_GET_REPORT 0x01 -#define HID_SET_REPORT 0x09 -#define XID_GET_CAPABILITIES 0x01 - -#define TYPE_USB_XID "usb-xbox-gamepad" -#define USB_XID(obj) OBJECT_CHECK(USBXIDState, (obj), TYPE_USB_XID) - -enum { - STR_MANUFACTURER = 1, - STR_PRODUCT, - STR_SERIALNUMBER, -}; - typedef enum HapticEmulationMode { EMU_NONE, EMU_HAPTIC_LEFT_RIGHT } HapticEmulationMode; -static const USBDescStrings desc_strings = { +const USBDescStrings desc_strings = { [STR_MANUFACTURER] = "QEMU", [STR_PRODUCT] = "Microsoft Xbox Controller", [STR_SERIALNUMBER] = "1", }; -typedef struct XIDDesc { - uint8_t bLength; - uint8_t bDescriptorType; - uint16_t bcdXid; - uint8_t bType; - uint8_t bSubType; - uint8_t bMaxInputReportSize; - uint8_t bMaxOutputReportSize; - uint16_t wAlternateProductIds[4]; -} QEMU_PACKED XIDDesc; - -typedef struct XIDGamepadReport { - uint8_t bReportId; - uint8_t bLength; - uint16_t wButtons; - uint8_t bAnalogButtons[8]; - int16_t sThumbLX; - int16_t sThumbLY; - int16_t sThumbRX; - int16_t sThumbRY; -} QEMU_PACKED XIDGamepadReport; - -typedef struct XIDGamepadOutputReport { - uint8_t report_id; //FIXME: is this correct? - uint8_t length; - uint16_t left_actuator_strength; - uint16_t right_actuator_strength; -} QEMU_PACKED XIDGamepadOutputReport; - -typedef struct USBXIDState { - USBDevice dev; - USBEndpoint *intr; - const XIDDesc *xid_desc; - XIDGamepadReport in_state; - XIDGamepadReport in_state_capabilities; - XIDGamepadOutputReport out_state; - XIDGamepadOutputReport out_state_capabilities; - uint8_t device_index; -} USBXIDState; - -static const USBDescIface desc_iface_xbox_gamepad = { - .bInterfaceNumber = 0, - .bNumEndpoints = 2, - .bInterfaceClass = USB_CLASS_XID, - .bInterfaceSubClass = 0x42, - .bInterfaceProtocol = 0x00, - .eps = (USBDescEndpoint[]) { - { - .bEndpointAddress = USB_DIR_IN | 0x02, - .bmAttributes = USB_ENDPOINT_XFER_INT, - .wMaxPacketSize = 0x20, - .bInterval = 4, - }, - { - .bEndpointAddress = USB_DIR_OUT | 0x02, - .bmAttributes = USB_ENDPOINT_XFER_INT, - .wMaxPacketSize = 0x20, - .bInterval = 4, - }, - }, -}; - -static const USBDescDevice desc_device_xbox_gamepad = { - .bcdUSB = 0x0110, - .bMaxPacketSize0 = 0x40, - .bNumConfigurations = 1, - .confs = (USBDescConfig[]) { - { - .bNumInterfaces = 1, - .bConfigurationValue = 1, - .bmAttributes = USB_CFG_ATT_ONE, - .bMaxPower = 50, - .nif = 1, - .ifs = &desc_iface_xbox_gamepad, - }, - }, -}; - -static const USBDesc desc_xbox_gamepad = { - .id = { - .idVendor = 0x045e, - .idProduct = 0x0202, - .bcdDevice = 0x0100, - .iManufacturer = STR_MANUFACTURER, - .iProduct = STR_PRODUCT, - .iSerialNumber = STR_SERIALNUMBER, - }, - .full = &desc_device_xbox_gamepad, - .str = desc_strings, -}; - -static const XIDDesc desc_xid_xbox_gamepad = { - .bLength = 0x10, - .bDescriptorType = USB_DT_XID, - .bcdXid = 0x100, - .bType = 1, - .bSubType = 1, - .bMaxInputReportSize = 20, - .bMaxOutputReportSize = 6, - .wAlternateProductIds = { 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF }, -}; - -#define GAMEPAD_A 0 -#define GAMEPAD_B 1 -#define GAMEPAD_X 2 -#define GAMEPAD_Y 3 -#define GAMEPAD_BLACK 4 -#define GAMEPAD_WHITE 5 -#define GAMEPAD_LEFT_TRIGGER 6 -#define GAMEPAD_RIGHT_TRIGGER 7 - -#define GAMEPAD_DPAD_UP 8 -#define GAMEPAD_DPAD_DOWN 9 -#define GAMEPAD_DPAD_LEFT 10 -#define GAMEPAD_DPAD_RIGHT 11 -#define GAMEPAD_START 12 -#define GAMEPAD_BACK 13 -#define GAMEPAD_LEFT_THUMB 14 -#define GAMEPAD_RIGHT_THUMB 15 - -#define BUTTON_MASK(button) (1 << ((button) - GAMEPAD_DPAD_UP)) - -static void update_output(USBXIDState *s) +void update_output(USBXIDGamepadState *s) { if (xemu_input_get_test_mode()) { // Don't report changes if we are testing the controller while running @@ -205,7 +52,7 @@ static void update_output(USBXIDState *s) xemu_input_update_rumble(state); } -static void update_input(USBXIDState *s) +void update_input(USBXIDGamepadState *s) { if (xemu_input_get_test_mode()) { // Don't report changes if we are testing the controller while running @@ -256,15 +103,15 @@ static void update_input(USBXIDState *s) s->in_state.sThumbRY = state->axis[CONTROLLER_AXIS_RSTICK_Y]; } -static void usb_xid_handle_reset(USBDevice *dev) +void usb_xid_handle_reset(USBDevice *dev) { DPRINTF("xid reset\n"); } -static void usb_xid_handle_control(USBDevice *dev, USBPacket *p, +void usb_xid_handle_control(USBDevice *dev, USBPacket *p, int request, int value, int index, int length, uint8_t *data) { - USBXIDState *s = (USBXIDState *)dev; + USBXIDGamepadState *s = (USBXIDGamepadState *)dev; DPRINTF("xid handle_control 0x%x 0x%x\n", request, value); @@ -368,36 +215,6 @@ static void usb_xid_handle_control(USBDevice *dev, USBPacket *p, } } -static void usb_xid_handle_data(USBDevice *dev, USBPacket *p) -{ - USBXIDState *s = DO_UPCAST(USBXIDState, dev, dev); - - DPRINTF("xid handle_data 0x%x %d 0x%zx\n", p->pid, p->ep->nr, p->iov.size); - - switch (p->pid) { - case USB_TOKEN_IN: - if (p->ep->nr == 2) { - update_input(s); - usb_packet_copy(p, &s->in_state, s->in_state.bLength); - } else { - assert(false); - } - break; - case USB_TOKEN_OUT: - if (p->ep->nr == 2) { - usb_packet_copy(p, &s->out_state, s->out_state.length); - update_output(s); - } else { - assert(false); - } - break; - default: - p->status = USB_RET_STALL; - assert(false); - break; - } -} - #if 0 static void usb_xid_handle_destroy(USBDevice *dev) { @@ -406,87 +223,6 @@ static void usb_xid_handle_destroy(USBDevice *dev) } #endif -static void usb_xbox_gamepad_unrealize(USBDevice *dev) +void usb_xbox_gamepad_unrealize(USBDevice *dev) { } - -static void usb_xid_class_initfn(ObjectClass *klass, void *data) -{ - USBDeviceClass *uc = USB_DEVICE_CLASS(klass); - - uc->handle_reset = usb_xid_handle_reset; - uc->handle_control = usb_xid_handle_control; - uc->handle_data = usb_xid_handle_data; - // uc->handle_destroy = usb_xid_handle_destroy; - uc->handle_attach = usb_desc_attach; -} - -static void usb_xbox_gamepad_realize(USBDevice *dev, Error **errp) -{ - USBXIDState *s = USB_XID(dev); - usb_desc_create_serial(dev); - usb_desc_init(dev); - s->intr = usb_ep_get(dev, USB_TOKEN_IN, 2); - - s->in_state.bLength = sizeof(s->in_state); - s->in_state.bReportId = 0; - - s->out_state.length = sizeof(s->out_state); - s->out_state.report_id = 0; - - s->xid_desc = &desc_xid_xbox_gamepad; - - memset(&s->in_state_capabilities, 0xFF, sizeof(s->in_state_capabilities)); - s->in_state_capabilities.bLength = sizeof(s->in_state_capabilities); - s->in_state_capabilities.bReportId = 0; - - memset(&s->out_state_capabilities, 0xFF, sizeof(s->out_state_capabilities)); - s->out_state_capabilities.length = sizeof(s->out_state_capabilities); - s->out_state_capabilities.report_id = 0; -} - -static Property xid_properties[] = { - DEFINE_PROP_UINT8("index", USBXIDState, device_index, 0), - DEFINE_PROP_END_OF_LIST(), -}; - -static const VMStateDescription vmstate_usb_xbox = { - .name = TYPE_USB_XID, - .version_id = 1, - .minimum_version_id = 1, - .fields = (VMStateField[]) { - VMSTATE_USB_DEVICE(dev, USBXIDState), - // FIXME - VMSTATE_END_OF_LIST() - }, -}; - -static void usb_xbox_gamepad_class_initfn(ObjectClass *klass, void *data) -{ - DeviceClass *dc = DEVICE_CLASS(klass); - USBDeviceClass *uc = USB_DEVICE_CLASS(klass); - - uc->product_desc = "Microsoft Xbox Controller"; - uc->usb_desc = &desc_xbox_gamepad; - uc->realize = usb_xbox_gamepad_realize; - uc->unrealize = usb_xbox_gamepad_unrealize; - usb_xid_class_initfn(klass, data); - set_bit(DEVICE_CATEGORY_INPUT, dc->categories); - dc->vmsd = &vmstate_usb_xbox; - device_class_set_props(dc, xid_properties); - dc->desc = "Microsoft Xbox Controller"; -} - -static const TypeInfo usb_xbox_gamepad_info = { - .name = TYPE_USB_XID, - .parent = TYPE_USB_DEVICE, - .instance_size = sizeof(USBXIDState), - .class_init = usb_xbox_gamepad_class_initfn, -}; - -static void usb_xid_register_types(void) -{ - type_register_static(&usb_xbox_gamepad_info); -} - -type_init(usb_xid_register_types) diff --git a/hw/xbox/xid.h b/hw/xbox/xid.h new file mode 100644 index 0000000000..44406eaf91 --- /dev/null +++ b/hw/xbox/xid.h @@ -0,0 +1,137 @@ +#ifndef __XID_H__ +#define __XID_H__ + +/* + * QEMU USB XID Devices + * + * Copyright (c) 2013 espes + * Copyright (c) 2017 Jannik Vogel + * Copyright (c) 2018-2021 Matt Borgerson + * Copyright (c) 2023 Fred Hallock + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#include "qemu/osdep.h" +#include "hw/hw.h" +#include "hw/qdev-properties.h" +#include "hw/usb.h" +#include "hw/usb/desc.h" +#include "migration/vmstate.h" +#include "sysemu/sysemu.h" +#include "ui/console.h" +#include "ui/xemu-input.h" + +// #define DEBUG_XID +#ifdef DEBUG_XID +#define DPRINTF printf +#else +#define DPRINTF(...) +#endif + +#define USB_CLASS_XID 0x58 +#define USB_DT_XID 0x42 + +#define HID_GET_REPORT 0x01 +#define HID_SET_REPORT 0x09 +#define XID_GET_CAPABILITIES 0x01 + +#define XID_DEVICETYPE_GAMEPAD 0x01 + +#define XID_DEVICESUBTYPE_GAMEPAD 0x01 +#define XID_DEVICESUBTYPE_GAMEPAD_S 0x02 + +#define TYPE_USB_XID_GAMEPAD "usb-xbox-gamepad" +#define TYPE_USB_XID_GAMEPAD_S "usb-xbox-gamepad-s" + +#define GAMEPAD_A 0 +#define GAMEPAD_B 1 +#define GAMEPAD_X 2 +#define GAMEPAD_Y 3 +#define GAMEPAD_BLACK 4 +#define GAMEPAD_WHITE 5 +#define GAMEPAD_LEFT_TRIGGER 6 +#define GAMEPAD_RIGHT_TRIGGER 7 + +#define GAMEPAD_DPAD_UP 8 +#define GAMEPAD_DPAD_DOWN 9 +#define GAMEPAD_DPAD_LEFT 10 +#define GAMEPAD_DPAD_RIGHT 11 +#define GAMEPAD_START 12 +#define GAMEPAD_BACK 13 +#define GAMEPAD_LEFT_THUMB 14 +#define GAMEPAD_RIGHT_THUMB 15 + +#define BUTTON_MASK(button) (1 << ((button) - GAMEPAD_DPAD_UP)) + +enum { + STR_MANUFACTURER = 1, + STR_PRODUCT, + STR_SERIALNUMBER, +}; + +extern const USBDescStrings desc_strings; + +typedef struct XIDDesc { + uint8_t bLength; + uint8_t bDescriptorType; + uint16_t bcdXid; + uint8_t bType; + uint8_t bSubType; + uint8_t bMaxInputReportSize; + uint8_t bMaxOutputReportSize; + uint16_t wAlternateProductIds[4]; +} QEMU_PACKED XIDDesc; + +typedef struct XIDGamepadReport { + uint8_t bReportId; + uint8_t bLength; + uint16_t wButtons; + uint8_t bAnalogButtons[8]; + int16_t sThumbLX; + int16_t sThumbLY; + int16_t sThumbRX; + int16_t sThumbRY; +} QEMU_PACKED XIDGamepadReport; + +typedef struct XIDGamepadOutputReport { + uint8_t report_id; // FIXME: is this correct? + uint8_t length; + uint16_t left_actuator_strength; + uint16_t right_actuator_strength; +} QEMU_PACKED XIDGamepadOutputReport; + +typedef struct USBXIDGamepadState { + USBDevice dev; + USBEndpoint *intr; + const XIDDesc *xid_desc; + XIDGamepadReport in_state; + XIDGamepadReport in_state_capabilities; + XIDGamepadOutputReport out_state; + XIDGamepadOutputReport out_state_capabilities; + uint8_t device_index; +} USBXIDGamepadState; + +void update_input(USBXIDGamepadState *s); +void update_output(USBXIDGamepadState *s); +void usb_xid_handle_reset(USBDevice *dev); +void usb_xid_handle_control(USBDevice *dev, USBPacket *p, int request, + int value, int index, int length, uint8_t *data); +void usb_xbox_gamepad_unrealize(USBDevice *dev); + +#if 0 +void usb_xid_handle_destroy(USBDevice *dev); +#endif + +#endif \ No newline at end of file diff --git a/ui/xemu-input.c b/ui/xemu-input.c index d9181fe2a6..31a51eda9d 100644 --- a/ui/xemu-input.c +++ b/ui/xemu-input.c @@ -86,6 +86,8 @@ static void xemu_input_print_controller_state(ControllerState *state) ControllerStateList available_controllers = QTAILQ_HEAD_INITIALIZER(available_controllers); ControllerState *bound_controllers[4] = { NULL, NULL, NULL, NULL }; +const char *bound_drivers[4] = { DRIVER_DUKE, DRIVER_DUKE, DRIVER_DUKE, + DRIVER_DUKE }; int test_mode; static const char **port_index_to_settings_key_map[] = { @@ -95,6 +97,13 @@ static const char **port_index_to_settings_key_map[] = { &g_config.input.bindings.port4, }; +static const char **port_index_to_driver_settings_key_map[] = { + &g_config.input.bindings.port1_driver, + &g_config.input.bindings.port2_driver, + &g_config.input.bindings.port3_driver, + &g_config.input.bindings.port4_driver +}; + static int *peripheral_types_settings_map[4][2] = { { &g_config.input.peripherals.port1.peripheral_type_0, &g_config.input.peripherals.port1.peripheral_type_1 }, @@ -119,6 +128,25 @@ static const char **peripheral_params_settings_map[4][2] = { static int sdl_kbd_scancode_map[25]; +static const char *get_bound_driver(int port) +{ + assert(port >= 0 && port <= 3); + const char *driver = *port_index_to_driver_settings_key_map[port]; + + // If the driver in the config is NULL, empty, or unrecognized + // then default to DRIVER_DUKE + if (driver == NULL) + return DRIVER_DUKE; + if (strlen(driver) == 0) + return DRIVER_DUKE; + if (strcmp(driver, DRIVER_DUKE) == 0) + return DRIVER_DUKE; + if (strcmp(driver, DRIVER_S) == 0) + return DRIVER_S; + + return DRIVER_DUKE; +} + static const int port_map[4] = { 3, 4, 1, 2 }; void xemu_input_init(void) @@ -177,6 +205,11 @@ void xemu_input_init(void) } } + bound_drivers[0] = get_bound_driver(0); + bound_drivers[1] = get_bound_driver(1); + bound_drivers[2] = get_bound_driver(2); + bound_drivers[3] = get_bound_driver(3); + // Check to see if we should auto-bind the keyboard int port = xemu_input_get_controller_default_bind_port(new_con, 0); if (port >= 0) { @@ -520,6 +553,8 @@ void xemu_input_bind(int index, ControllerState *state, int save) } } xemu_settings_set_string(port_index_to_settings_key_map[index], guid_buf); + xemu_settings_set_string(port_index_to_driver_settings_key_map[index], + bound_drivers[index]); } // Bind new controller @@ -548,7 +583,7 @@ void xemu_input_bind(int index, ControllerState *state, int save) QDict *qdict = qdict_new(); // Specify device driver - qdict_put_str(qdict, "driver", "usb-xbox-gamepad"); + qdict_put_str(qdict, "driver", bound_drivers[index]); // Specify device identifier static int id_counter = 0; diff --git a/ui/xemu-input.h b/ui/xemu-input.h index 330ae58a7c..23c1a9f91b 100644 --- a/ui/xemu-input.h +++ b/ui/xemu-input.h @@ -30,6 +30,12 @@ #include "qemu/queue.h" +#define DRIVER_DUKE "usb-xbox-gamepad" +#define DRIVER_S "usb-xbox-gamepad-s" + +#define DRIVER_DUKE_DISPLAY_NAME "Xbox Controller" +#define DRIVER_S_DISPLAY_NAME "Xbox Controller S" + enum controller_state_buttons_mask { CONTROLLER_BUTTON_A = (1 << 0), CONTROLLER_BUTTON_B = (1 << 1), @@ -107,6 +113,7 @@ typedef struct ControllerState { typedef QTAILQ_HEAD(, ControllerState) ControllerStateList; extern ControllerStateList available_controllers; extern ControllerState *bound_controllers[4]; +extern const char *bound_drivers[4]; #ifdef __cplusplus extern "C" { diff --git a/ui/xui/gl-helpers.cc b/ui/xui/gl-helpers.cc index 6c07d087b4..0a06b7f16a 100644 --- a/ui/xui/gl-helpers.cc +++ b/ui/xui/gl-helpers.cc @@ -20,6 +20,7 @@ #include "gl-helpers.hh" #include "common.hh" #include "data/controller_mask.png.h" +#include "data/controller_mask_s.png.h" #include "data/logo_sdf.png.h" #include "data/xemu_64x64.png.h" #include "data/xmu_mask.png.h" @@ -33,7 +34,7 @@ #include "ui/shader/xemu-logo-frag.h" Fbo *controller_fbo, *xmu_fbo, *logo_fbo; -GLuint g_controller_tex, g_logo_tex, g_icon_tex, g_xmu_tex; +GLuint g_controller_duke_tex, g_controller_s_tex, g_logo_tex, g_icon_tex, g_xmu_tex; enum class ShaderType { Blit, @@ -439,8 +440,10 @@ enum tex_item_names { void InitCustomRendering(void) { glActiveTexture(GL_TEXTURE0); - g_controller_tex = + g_controller_duke_tex = LoadTextureFromMemory(controller_mask_data, controller_mask_size); + g_controller_s_tex = + LoadTextureFromMemory(controller_mask_s_data, controller_mask_s_size); g_decal_shader = NewDecalShader(ShaderType::Mask); controller_fbo = new Fbo(512, 512); @@ -464,7 +467,7 @@ static void RenderMeter(DecalShader *s, float x, float y, float width, RenderDecal(s, x, y, width * p, height, 0, 0, 1, 1, 0, 0, color_fg); } -void RenderController(float frame_x, float frame_y, uint32_t primary_color, +static void RenderDukeController(float frame_x, float frame_y, uint32_t primary_color, uint32_t secondary_color, ControllerState *state) { // Location within the controller texture of masked button locations, @@ -494,7 +497,7 @@ void RenderController(float frame_x, float frame_y, uint32_t primary_color, glUseProgram(g_decal_shader->prog); glBindVertexArray(g_decal_shader->vao); glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, g_controller_tex); + glBindTexture(GL_TEXTURE_2D, g_controller_duke_tex); // Add a 5 pixel space around the controller so we can wiggle the controller // around to visualize rumble in action @@ -623,13 +626,191 @@ void RenderController(float frame_x, float frame_y, uint32_t primary_color, glUseProgram(0); } +static void RenderControllerS(float frame_x, float frame_y, uint32_t primary_color, + uint32_t secondary_color, ControllerState *state) +{ + // Location within the controller texture of masked button locations, + // relative to the origin of the controller + const struct rect jewel = { 194, 213, 84, 84 }; + const struct rect lstick_ctr = { 103, 254, 0, 0 }; + const struct rect rstick_ctr = { 295, 176, 0, 0 }; + const struct rect buttons[12] = { + { 347, 200, 34, 34 }, // A + { 381, 235, 34, 34 }, // B + { 313, 235, 34, 34 }, // X + { 347, 270, 34, 34 }, // Y + { 123, 165, 31, 26 }, // D-Left + { 150, 187, 26, 31 }, // D-Up + { 173, 165, 31, 26 }, // D-Right + { 150, 135, 26, 31 }, // D-Down + { 45, 195, 20, 24 }, // Back + { 70, 163, 26, 26 }, // Start + { 352, 145, 30, 30 }, // White + { 388, 172, 30, 30 }, // Black + }; + + uint8_t alpha = 0; + uint32_t now = SDL_GetTicks(); + float t; + + glUseProgram(g_decal_shader->prog); + glBindVertexArray(g_decal_shader->vao); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, g_controller_s_tex); + + // Add a 5 pixel space around the controller so we can wiggle the controller + // around to visualize rumble in action + frame_x += 5; + frame_y += 5; + float original_frame_x = frame_x; + float original_frame_y = frame_y; + + // Floating point versions that will get scaled + float rumble_l = 0; + float rumble_r = 0; + + glBlendEquation(GL_FUNC_ADD); + glBlendFunc(GL_ONE, GL_ZERO); + + uint32_t jewel_color = secondary_color; + + // Check to see if the guide button is pressed + const uint32_t animate_guide_button_duration = 2000; + if (state->buttons & CONTROLLER_BUTTON_GUIDE) { + state->animate_guide_button_end = + now + animate_guide_button_duration; + } + + if (now < state->animate_guide_button_end) { + t = 1.0f - (float)(state->animate_guide_button_end - now) / + (float)animate_guide_button_duration; + float sin_wav = (1 - sin(M_PI * t / 2.0f)); + + // Animate guide button by highlighting logo jewel and fading out over + // time + alpha = sin_wav * 255.0f; + jewel_color = primary_color + alpha; + + // Add a little extra flare: wiggle the frame around while we rumble + frame_x += ((float)(rand() % 5) - 2.5) * (1 - t); + frame_y += ((float)(rand() % 5) - 2.5) * (1 - t); + rumble_l = rumble_r = sin_wav; + } + + // Render controller texture + RenderDecal(g_decal_shader, frame_x + 0, frame_y + 0, + tex_items[obj_controller].w, tex_items[obj_controller].h, + tex_items[obj_controller].x, tex_items[obj_controller].y, + tex_items[obj_controller].w, tex_items[obj_controller].h, + primary_color, secondary_color, 0); + + glBlendFunc(GL_ONE_MINUS_DST_ALPHA, + GL_ONE); // Blend with controller cutouts + RenderDecal(g_decal_shader, frame_x + jewel.x, frame_y + jewel.y, jewel.w, + jewel.h, 0, 0, 1, 1, 0, 0, jewel_color); + + // The controller has alpha cutouts where the buttons are. Draw a surface + // behind the buttons if they are activated + for (int i = 0; i < 12; i++) { + if (state->buttons & (1 << i)) { + RenderDecal(g_decal_shader, frame_x + buttons[i].x, + frame_y + buttons[i].y, buttons[i].w, buttons[i].h, 0, + 0, 1, 1, 0, 0, primary_color + 0xff); + } + } + + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Blend with controller + + // Render left thumbstick + float w = tex_items[obj_lstick].w; + float h = tex_items[obj_lstick].h; + float c_x = frame_x + lstick_ctr.x; + float c_y = frame_y + lstick_ctr.y; + float lstick_x = (float)state->axis[CONTROLLER_AXIS_LSTICK_X] / 32768.0; + float lstick_y = (float)state->axis[CONTROLLER_AXIS_LSTICK_Y] / 32768.0; + RenderDecal( + g_decal_shader, (int)(c_x - w / 2.0f + 10.0f * lstick_x), + (int)(c_y - h / 2.0f + 10.0f * lstick_y), w, h, tex_items[obj_lstick].x, + tex_items[obj_lstick].y, w, h, + (state->buttons & CONTROLLER_BUTTON_LSTICK) ? secondary_color : + primary_color, + (state->buttons & CONTROLLER_BUTTON_LSTICK) ? primary_color : + secondary_color, + 0); + + // Render right thumbstick + w = tex_items[obj_rstick].w; + h = tex_items[obj_rstick].h; + c_x = frame_x + rstick_ctr.x; + c_y = frame_y + rstick_ctr.y; + float rstick_x = (float)state->axis[CONTROLLER_AXIS_RSTICK_X] / 32768.0; + float rstick_y = (float)state->axis[CONTROLLER_AXIS_RSTICK_Y] / 32768.0; + RenderDecal( + g_decal_shader, (int)(c_x - w / 2.0f + 10.0f * rstick_x), + (int)(c_y - h / 2.0f + 10.0f * rstick_y), w, h, tex_items[obj_rstick].x, + tex_items[obj_rstick].y, w, h, + (state->buttons & CONTROLLER_BUTTON_RSTICK) ? secondary_color : + primary_color, + (state->buttons & CONTROLLER_BUTTON_RSTICK) ? primary_color : + secondary_color, + 0); + + glBlendFunc(GL_ONE, + GL_ZERO); // Don't blend, just overwrite values in buffer + + // Render trigger bars + float ltrig = state->axis[CONTROLLER_AXIS_LTRIG] / 32767.0; + float rtrig = state->axis[CONTROLLER_AXIS_RTRIG] / 32767.0; + const uint32_t animate_trigger_duration = 1000; + if ((ltrig > 0) || (rtrig > 0)) { + state->animate_trigger_end = now + animate_trigger_duration; + rumble_l = fmax(rumble_l, ltrig); + rumble_r = fmax(rumble_r, rtrig); + } + + // Animate trigger alpha down after a period of inactivity + alpha = 0x80; + if (state->animate_trigger_end > now) { + t = 1.0f - (float)(state->animate_trigger_end - now) / + (float)animate_trigger_duration; + float sin_wav = (1 - sin(M_PI * t / 2.0f)); + alpha += fmin(sin_wav * 0x40, 0x80); + } + + RenderMeter(g_decal_shader, original_frame_x + 10, + original_frame_y + tex_items[obj_controller].h + 20, 150, 5, + ltrig, primary_color + alpha, primary_color + 0xff); + RenderMeter(g_decal_shader, + original_frame_x + tex_items[obj_controller].w - 160, + original_frame_y + tex_items[obj_controller].h + 20, 150, 5, + rtrig, primary_color + alpha, primary_color + 0xff); + + // Apply rumble updates + state->rumble_l = (int)(rumble_l * (float)0xffff); + state->rumble_r = (int)(rumble_r * (float)0xffff); + + glBindVertexArray(0); + glUseProgram(0); +} + +void RenderController(float frame_x, float frame_y, uint32_t primary_color, + uint32_t secondary_color, ControllerState *state) +{ + if (strcmp(bound_drivers[state->bound], DRIVER_S) == 0) + RenderControllerS(frame_x, frame_y, primary_color, secondary_color, + state); + else if (strcmp(bound_drivers[state->bound], DRIVER_DUKE) == 0) + RenderDukeController(frame_x, frame_y, primary_color, secondary_color, + state); +} + void RenderControllerPort(float frame_x, float frame_y, int i, uint32_t port_color) { glUseProgram(g_decal_shader->prog); glBindVertexArray(g_decal_shader->vao); glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, g_controller_tex); + glBindTexture(GL_TEXTURE_2D, g_controller_duke_tex); glBlendFunc(GL_ONE, GL_ZERO); // Render port socket diff --git a/ui/xui/main-menu.cc b/ui/xui/main-menu.cc index 2347ded770..d34c08b00e 100644 --- a/ui/xui/main-menu.cc +++ b/ui/xui/main-menu.cc @@ -158,6 +158,49 @@ void MainMenuInputView::Draw() ImGui::PopStyleVar(); // ItemSpacing ImGui::Columns(1); + // + // Render device driver combo + // + + // List available device drivers + const char *driver = bound_drivers[active]; + + if (strcmp(driver, DRIVER_DUKE) == 0) + driver = DRIVER_DUKE_DISPLAY_NAME; + else if (strcmp(driver, DRIVER_S) == 0) + driver = DRIVER_S_DISPLAY_NAME; + + ImGui::SetNextItemWidth(-FLT_MIN); + if (ImGui::BeginCombo("###InputDrivers", driver, + ImGuiComboFlags_NoArrowButton)) { + const char *available_drivers[] = { DRIVER_DUKE, DRIVER_S }; + const char *driver_display_names[] = { + DRIVER_DUKE_DISPLAY_NAME, + DRIVER_S_DISPLAY_NAME + }; + bool is_selected = false; + int num_drivers = sizeof(driver_display_names) / sizeof(driver_display_names[0]); + for (int i = 0; i < num_drivers; i++) { + const char *iter = driver_display_names[i]; + is_selected = strcmp(driver, iter) == 0; + ImGui::PushID(iter); + if (ImGui::Selectable(iter, is_selected)) { + for (int j = 0; j < num_drivers; j++) { + if (iter == driver_display_names[j]) + bound_drivers[active] = available_drivers[j]; + } + xemu_input_bind(active, bound_controllers[active], 1); + } + if (is_selected) { + ImGui::SetItemDefaultFocus(); + } + ImGui::PopID(); + } + + ImGui::EndCombo(); + } + DrawComboChevron(); + // // Render input device combo //