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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 RhYSo144$rjF6*2UngB!I&yoND 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 //