From 0eddb3ead943aa082a223b9484d75678940dcad9 Mon Sep 17 00:00:00 2001 From: Erik Abair Date: Tue, 14 Jan 2025 16:01:34 -0800 Subject: [PATCH 1/7] build: Allow CFLAGS to be passed through on macos. --- build.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/build.sh b/build.sh index 23e4210068..3e0ef7e8f2 100755 --- a/build.sh +++ b/build.sh @@ -213,12 +213,14 @@ case "$platform" in # Adjust compilation options based on platform python3 ./scripts/download-macos-libs.py ${target_arch} lib_prefix=${PWD}/macos-libs/${target_arch}/opt/local - export CFLAGS="-arch ${target_arch} \ + export CFLAGS="${CFLAGS} \ + -arch ${target_arch} \ -target ${target_arch}-apple-macos${macos_min_ver} \ -isysroot ${sdk} \ -I${lib_prefix}/include \ -mmacosx-version-min=$macos_min_ver" - export LDFLAGS="-arch ${target_arch} \ + export LDFLAGS="${LDFLAGS} \ + -arch ${target_arch} \ -isysroot ${sdk}" if [ "$target_arch" == "x86_64" ]; then sys_cflags='-march=ivybridge' From 6de26b0c2cfb503892e905618d471573e4b6e209 Mon Sep 17 00:00:00 2001 From: Matt Borgerson Date: Tue, 14 Jan 2025 23:42:03 -0700 Subject: [PATCH 2/7] ac97: Disable pi/mc reads for now --- hw/audio/ac97.c | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/hw/audio/ac97.c b/hw/audio/ac97.c index 8c0cd38b10..29490d9968 100644 --- a/hw/audio/ac97.c +++ b/hw/audio/ac97.c @@ -149,8 +149,11 @@ typedef struct AC97DeviceState { OBJECT_CHECK(AC97DeviceState, (obj), "AC97") static void po_callback(void *opaque, int free); + +#ifndef XBOX static void pi_callback(void *opaque, int avail); static void mc_callback(void *opaque, int avail); +#endif static void fetch_bd(AC97LinkState *s, AC97BusMasterRegs *r) { @@ -218,7 +221,9 @@ static void voice_set_active(AC97LinkState *s, int bm_index, int on) { switch (bm_index) { case PI_INDEX: +#ifndef XBOX AUD_set_active_in(s->voice_pi, on); +#endif break; case PO_INDEX: @@ -226,7 +231,9 @@ static void voice_set_active(AC97LinkState *s, int bm_index, int on) break; case MC_INDEX: +#ifndef XBOX AUD_set_active_in(s->voice_mc, on); +#endif break; case SO_INDEX: @@ -294,6 +301,7 @@ static void open_voice(AC97LinkState *s, int index, int freq) s->invalid_freq[index] = 0; switch (index) { case PI_INDEX: +#ifndef XBOX s->voice_pi = AUD_open_in( &s->card, s->voice_pi, @@ -302,6 +310,7 @@ static void open_voice(AC97LinkState *s, int index, int freq) pi_callback, &as ); +#endif break; case PO_INDEX: @@ -316,6 +325,7 @@ static void open_voice(AC97LinkState *s, int index, int freq) break; case MC_INDEX: +#ifndef XBOX s->voice_mc = AUD_open_in( &s->card, s->voice_mc, @@ -324,6 +334,7 @@ static void open_voice(AC97LinkState *s, int index, int freq) mc_callback, &as ); +#endif break; case SO_INDEX: @@ -333,7 +344,9 @@ static void open_voice(AC97LinkState *s, int index, int freq) s->invalid_freq[index] = freq; switch (index) { case PI_INDEX: +#ifndef XBOX AUD_close_in(&s->card, s->voice_pi); +#endif s->voice_pi = NULL; break; @@ -343,7 +356,9 @@ static void open_voice(AC97LinkState *s, int index, int freq) break; case MC_INDEX: +#ifndef XBOX AUD_close_in(&s->card, s->voice_mc); +#endif s->voice_mc = NULL; break; @@ -357,17 +372,21 @@ static void reset_voices(AC97LinkState *s, uint8_t active[LAST_INDEX]) { uint16_t freq; +#ifndef XBOX freq = mixer_load(s, AC97_PCM_LR_ADC_Rate); open_voice(s, PI_INDEX, freq); AUD_set_active_in(s->voice_pi, active[PI_INDEX]); +#endif freq = mixer_load(s, AC97_PCM_Front_DAC_Rate); open_voice(s, PO_INDEX, freq); AUD_set_active_out(s->voice_po, active[PO_INDEX]); +#ifndef XBOX freq = mixer_load(s, AC97_MIC_ADC_Rate); open_voice(s, MC_INDEX, freq); AUD_set_active_in(s->voice_mc, active[MC_INDEX]); +#endif } static void get_volume(uint16_t vol, uint16_t mask, int inverse, @@ -408,7 +427,9 @@ static void update_volume_in(AC97LinkState *s) get_volume(mixer_load(s, AC97_Record_Gain_Mute), 0x0f, 0, &mute, &lvol, &rvol); +#ifndef XBOX AUD_set_volume_in(s->voice_pi, mute, lvol, rvol); +#endif } static void set_volume(AC97LinkState *s, int index, uint32_t val) @@ -972,6 +993,10 @@ static int read_audio(AC97LinkState *s, AC97BusMasterRegs *r, int to_copy = 0; SWVoiceIn *voice = (r - s->bm_regs) == MC_INDEX ? s->voice_mc : s->voice_pi; +#ifdef XBOX + return 0; +#endif + temp = MIN(temp, max); if (!temp) { @@ -1081,6 +1106,7 @@ static void transfer_audio(AC97LinkState *s, int index, int elapsed) } } +#ifndef XBOX static void pi_callback(void *opaque, int avail) { transfer_audio(opaque, PI_INDEX, avail); @@ -1090,6 +1116,7 @@ static void mc_callback(void *opaque, int avail) { transfer_audio(opaque, MC_INDEX, avail); } +#endif static void po_callback(void *opaque, int free) { From 6f63e3c4afb7affda32c5167bd62080f8476f5dd Mon Sep 17 00:00:00 2001 From: Matt Borgerson Date: Wed, 15 Jan 2025 02:20:05 -0700 Subject: [PATCH 3/7] meson: Add SDL to audio driver priority list on Linux --- meson.build | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/meson.build b/meson.build index 6d6aadd0d7..24ee8ac655 100644 --- a/meson.build +++ b/meson.build @@ -2406,8 +2406,7 @@ if have_system # Default to native drivers first, OSS second, SDL third audio_drivers_priority = \ - [ 'pa', 'coreaudio', 'dsound', 'sndio', 'oss' ] + \ - (host_os == 'linux' ? [] : [ 'sdl' ]) + [ 'pa', 'coreaudio', 'dsound', 'sndio', 'oss', 'sdl' ] audio_drivers_default = [] foreach k: audio_drivers_priority if audio_drivers_available[k] From 4f71be78e27cdc82c55e7be4a4ce8da70fc572b8 Mon Sep 17 00:00:00 2001 From: Matt Borgerson Date: Wed, 15 Jan 2025 02:29:28 -0700 Subject: [PATCH 4/7] Info.plist: Add NSMicrophoneUsageDescription key Required to access microphone on macOS. --- Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Info.plist b/Info.plist index 5303f0fa0e..ec4b235564 100644 --- a/Info.plist +++ b/Info.plist @@ -32,5 +32,7 @@ com.apple.security.cs.allow-jit + NSMicrophoneUsageDescription + Microphone used for peripheral emulation, e.g. Xbox Live Communicator. From e293f6ba670c0efb4ecd9a75fba6116ae6ae0862 Mon Sep 17 00:00:00 2001 From: Ryzee119 Date: Thu, 30 Jun 2022 13:58:44 +0930 Subject: [PATCH 5/7] input: Add xbox live communicator support --- hw/xbox/meson.build | 1 + hw/xbox/xblc.c | 402 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 403 insertions(+) create mode 100644 hw/xbox/xblc.c diff --git a/hw/xbox/meson.build b/hw/xbox/meson.build index 907ebdc4e8..b8199e55a8 100644 --- a/hw/xbox/meson.build +++ b/hw/xbox/meson.build @@ -15,6 +15,7 @@ specific_ss.add(files( 'xbox.c', 'xbox_pci.c', 'xid.c', + 'xblc.c', )) subdir('nv2a') subdir('mcpx') diff --git a/hw/xbox/xblc.c b/hw/xbox/xblc.c new file mode 100644 index 0000000000..60badb10a7 --- /dev/null +++ b/hw/xbox/xblc.c @@ -0,0 +1,402 @@ +/* + * QEMU USB Xbox Live Communicator (XBLC) Device + * + * Copyright (c) 2022 Ryan Wendland + * + * 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/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" +#include "audio/audio.h" +#include "qemu/fifo8.h" + +//#define DEBUG_XBLC +#ifdef DEBUG_XBLC +#define DPRINTF printf +#else +#define DPRINTF(...) +#endif + +#define TYPE_USB_XBLC "usb-xblc" +#define USB_XBLC(obj) OBJECT_CHECK(USBXBLCState, (obj), TYPE_USB_XBLC) + +#define XBLC_STR "Microsoft Xbox Live Communicator" +#define XBLC_INTERFACE_CLASS 0x78 +#define XBLC_INTERFACE_SUBCLASS 0x00 +#define XBLC_EP_OUT 0x04 +#define XBLC_EP_IN 0x05 + +#define XBLC_SET_SAMPLE_RATE 0x00 +#define XBLC_SET_AGC 0x01 + +#define XBLC_MAX_PACKET 48 +#define XBLC_FIFO_SIZE (XBLC_MAX_PACKET * 100) //~100 ms worth of audio at 16bit 24kHz + +static const uint8_t silence[256] = {0}; + +static const uint16_t xblc_sample_rates[5] = { + 8000, 11025, 16000, 22050, 24000 +}; + +typedef struct USBXBLCState { + USBDevice dev; + uint8_t device_index; + uint8_t auto_gain_control; + uint16_t sample_rate; + + QEMUSoundCard card; + struct audsettings as; + + struct { + SWVoiceOut* voice; + uint8_t packet[XBLC_MAX_PACKET]; + Fifo8 fifo; + } out; + + struct { + SWVoiceIn *voice; + uint8_t packet[XBLC_MAX_PACKET]; + Fifo8 fifo; + } in; +} USBXBLCState; + +enum { + STR_MANUFACTURER = 1, + STR_PRODUCT, + STR_SERIALNUMBER, +}; + +static const USBDescStrings desc_strings = { + [STR_MANUFACTURER] = "xemu", + [STR_PRODUCT] = XBLC_STR, + [STR_SERIALNUMBER] = "1", +}; + +static const USBDescIface desc_iface[]= { + { + .bInterfaceNumber = 0, + .bNumEndpoints = 1, + .bInterfaceClass = XBLC_INTERFACE_CLASS, + .bInterfaceSubClass = XBLC_INTERFACE_SUBCLASS, + .bInterfaceProtocol = 0x00, + .eps = (USBDescEndpoint[]) { + { + .bEndpointAddress = USB_DIR_OUT | XBLC_EP_OUT, + .bmAttributes = USB_ENDPOINT_XFER_ISOC, + .wMaxPacketSize = XBLC_MAX_PACKET, + .is_audio = 1, + .bInterval = 1, + .bRefresh = 0, + .bSynchAddress = 0, + } + }, + }, + { + .bInterfaceNumber = 1, + .bNumEndpoints = 1, + .bInterfaceClass = XBLC_INTERFACE_CLASS, + .bInterfaceSubClass = XBLC_INTERFACE_SUBCLASS, + .bInterfaceProtocol = 0x00, + .eps = (USBDescEndpoint[]) { + { + .bEndpointAddress = USB_DIR_IN | XBLC_EP_IN, + .bmAttributes = USB_ENDPOINT_XFER_ISOC, + .wMaxPacketSize = XBLC_MAX_PACKET, + .is_audio = 1, + .bInterval = 1, + .bRefresh = 0, + .bSynchAddress = 0, + } + }, + } +}; + +static const USBDescDevice desc_device = { + .bcdUSB = 0x0110, + .bMaxPacketSize0 = 8, + .bNumConfigurations = 1, + .confs = (USBDescConfig[]) { + { + .bNumInterfaces = 2, + .bConfigurationValue = 1, + .bmAttributes = USB_CFG_ATT_ONE, + .bMaxPower = 100, + .nif = ARRAY_SIZE(desc_iface), + .ifs = desc_iface, + }, + }, +}; + +static const USBDesc desc_xblc = { + .id = { + .idVendor = 0x045e, + .idProduct = 0x0283, + .bcdDevice = 0x0110, + .iManufacturer = STR_MANUFACTURER, + .iProduct = STR_PRODUCT, + .iSerialNumber = STR_SERIALNUMBER, + }, + .full = &desc_device, + .str = desc_strings, +}; + +static void usb_xblc_handle_reset(USBDevice *dev) +{ + USBXBLCState *s = (USBXBLCState *)dev; + + DPRINTF("[XBLC] Reset\n"); + fifo8_reset(&s->in.fifo); + fifo8_reset(&s->out.fifo); +} + +static void output_callback(void *opaque, int avail) +{ + USBXBLCState *s = (USBXBLCState *)opaque; + const uint8_t *data; + uint32_t processed, max_len; + + // Not enough data to send, wait a bit longer, fill with silence for now + if (fifo8_num_used(&s->out.fifo) < XBLC_MAX_PACKET) { + do { + processed = AUD_write(s->out.voice, (void *)silence, ARRAY_SIZE(silence)); + avail -= processed; + } while (avail > 0 && processed >= XBLC_MAX_PACKET); + return; + } + + // Write speaker data into audio backend + while (avail > 0 && !fifo8_is_empty(&s->out.fifo)) { + max_len = MIN(fifo8_num_used(&s->out.fifo), avail); + data = fifo8_pop_bufptr(&s->out.fifo, max_len, &max_len); + processed = AUD_write(s->out.voice, (void *)data, max_len); + avail -= processed; + if (processed < max_len) return; + } +} + +static void input_callback(void *opaque, int avail) +{ + USBXBLCState *s = (USBXBLCState *)opaque; + uint32_t processed, max_len; + + // Get microphone data from audio backend + while (avail > 0 && !fifo8_is_full(&s->in.fifo)) { + max_len = MIN(sizeof(s->in.packet), fifo8_num_free(&s->in.fifo)); + processed = AUD_read(s->in.voice, s->in.packet, max_len); + avail -= processed; + fifo8_push_all(&s->in.fifo, s->in.packet, processed); + if (processed < max_len) return; + } + + // Flush excess/old data - this can happen if the user program stops the iso transfers after it + // has setup the xblc. + while (avail > 0) + { + processed = AUD_read(s->in.voice, s->in.packet, XBLC_MAX_PACKET); + avail -= processed; + if (processed == 0) break; + } +} + +static void xblc_audio_stream_init(USBDevice *dev, uint16_t sample_rate) +{ + USBXBLCState *s = (USBXBLCState *)dev; + + AUD_set_active_out(s->out.voice, FALSE); + AUD_set_active_in(s->in.voice, FALSE); + + fifo8_reset(&s->in.fifo); + fifo8_reset(&s->out.fifo); + + s->as.freq = sample_rate; + s->as.nchannels = 1; + s->as.fmt = AUDIO_FORMAT_S16; + s->as.endianness = 0; + + s->out.voice = AUD_open_out(&s->card, s->out.voice, TYPE_USB_XBLC "-speaker", + s, output_callback, &s->as); + + s->in.voice = AUD_open_in(&s->card, s->in.voice, TYPE_USB_XBLC "-microphone", + s, input_callback, &s->as); + + AUD_set_active_out(s->out.voice, TRUE); + AUD_set_active_in(s->in.voice, TRUE); + DPRINTF("[XBLC] Init audio streams at %d Hz\n", sample_rate); +} + +static void usb_xblc_handle_control(USBDevice *dev, USBPacket *p, + int request, int value, int index, int length, uint8_t *data) +{ + USBXBLCState *s = (USBXBLCState *)dev; + + if (usb_desc_handle_control(dev, p, request, value, index, length, data) >= 0) { + DPRINTF("[XBLC] USB Control request handled by usb_desc_handle_control\n"); + return; + } + + switch (request) { + case VendorInterfaceOutRequest | USB_REQ_SET_FEATURE: + if (index == XBLC_SET_SAMPLE_RATE) + { + uint8_t rate = value & 0xFF; + assert(rate < ARRAY_SIZE(xblc_sample_rates)); + DPRINTF("[XBLC] Set Sample Rate to %04x\n", rate); + s->sample_rate = xblc_sample_rates[rate]; + xblc_audio_stream_init(dev, s->sample_rate); + break; + } + else if (index == XBLC_SET_AGC) + { + DPRINTF("[XBLC] Set Auto Gain Control to %d\n", value); + s->auto_gain_control = (value) ? 1 : 0; + break; + } + // Fallthrough + default: + DPRINTF("[XBLC] USB stalled on request 0x%x value 0x%x\n", request, value); + p->status = USB_RET_STALL; + assert(false); + return; + } +} + +static void usb_xblc_handle_data(USBDevice *dev, USBPacket *p) +{ + USBXBLCState *s = (USBXBLCState *)dev; + uint32_t to_process, chunk_len; + + switch (p->pid) { + case USB_TOKEN_IN: + // Microphone Data - Get data from fifo and copy into usb packet + assert(p->ep->nr == XBLC_EP_IN); + to_process = MIN(fifo8_num_used(&s->in.fifo), p->iov.size); + chunk_len = 0; + + // fifo may not give us a contiguous packet, so may need multiple calls + while (to_process) { + const uint8_t *packet = fifo8_pop_bufptr(&s->in.fifo, to_process, &chunk_len); + usb_packet_copy(p, (void *)packet, chunk_len); + to_process -= chunk_len; + } + + break; + case USB_TOKEN_OUT: + // Speaker data - get data from usb packet then push to fifo. + assert(p->ep->nr == XBLC_EP_OUT); + to_process = MIN(fifo8_num_free(&s->out.fifo), p->iov.size); + usb_packet_copy(p, s->out.packet, to_process); + fifo8_push_all(&s->out.fifo, s->out.packet, to_process); + + break; + default: + //Iso cannot report STALL/HALT, but we shouldn't be here anyway. + assert(false); + break; + } + + // Ensure we fill the entire packet regardless of if we have audio data so we don't + // cause an underrun error. + if (p->actual_length < p->iov.size) + usb_packet_copy(p, (void *)silence, p->iov.size - p->actual_length); + +} + +static void usb_xbox_communicator_unrealize(USBDevice *dev) +{ + USBXBLCState *s = USB_XBLC(dev); + + AUD_set_active_out(s->out.voice, false); + AUD_set_active_in(s->in.voice, false); + + fifo8_destroy(&s->out.fifo); + fifo8_destroy(&s->in.fifo); + + AUD_close_out(&s->card, s->out.voice); + AUD_close_in(&s->card, s->in.voice); + AUD_remove_card(&s->card); +} + +static void usb_xblc_class_initfn(ObjectClass *klass, void *data) +{ + USBDeviceClass *uc = USB_DEVICE_CLASS(klass); + uc->handle_reset = usb_xblc_handle_reset; + uc->handle_control = usb_xblc_handle_control; + uc->handle_data = usb_xblc_handle_data; + uc->handle_attach = usb_desc_attach; +} + +static void usb_xbox_communicator_realize(USBDevice *dev, Error **errp) +{ + USBXBLCState *s = USB_XBLC(dev); + usb_desc_create_serial(dev); + usb_desc_init(dev); + AUD_register_card(TYPE_USB_XBLC, &s->card, errp); + + fifo8_create(&s->in.fifo, XBLC_FIFO_SIZE); + fifo8_create(&s->out.fifo, XBLC_FIFO_SIZE); +} + +static Property xblc_properties[] = { + DEFINE_PROP_UINT8("index", USBXBLCState, device_index, 0), + DEFINE_PROP_END_OF_LIST(), +}; + +static const VMStateDescription usb_xblc_vmstate = { + .name = TYPE_USB_XBLC, + .version_id = 1, + .minimum_version_id = 1, + .fields = (VMStateField[]) { + VMSTATE_USB_DEVICE(dev, USBXBLCState), + // FIXME + VMSTATE_END_OF_LIST() + }, +}; + +static void usb_xbox_communicator_class_initfn(ObjectClass *klass, void *data) +{ + DeviceClass *dc = DEVICE_CLASS(klass); + USBDeviceClass *uc = USB_DEVICE_CLASS(klass); + + uc->product_desc = XBLC_STR; + uc->usb_desc = &desc_xblc; + uc->realize = usb_xbox_communicator_realize; + uc->unrealize = usb_xbox_communicator_unrealize; + usb_xblc_class_initfn(klass, data); + set_bit(DEVICE_CATEGORY_INPUT, dc->categories); + dc->vmsd = &usb_xblc_vmstate; + device_class_set_props(dc, xblc_properties); + dc->desc = XBLC_STR; +} + +static const TypeInfo info_xblc = { + .name = TYPE_USB_XBLC, + .parent = TYPE_USB_DEVICE, + .instance_size = sizeof(USBXBLCState), + .class_init = usb_xbox_communicator_class_initfn, +}; + +static void usb_xblc_register_types(void) +{ + type_register_static(&info_xblc); +} + +type_init(usb_xblc_register_types) From 5896b9dc91d2b8b94b2b30570e1e329b161c1453 Mon Sep 17 00:00:00 2001 From: Ryan Wendland Date: Wed, 15 Jan 2025 14:12:54 +1030 Subject: [PATCH 6/7] vl: Enable audio backend --- system/vl.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/system/vl.c b/system/vl.c index 9168fcec9a..580d0cfd28 100644 --- a/system/vl.c +++ b/system/vl.c @@ -2996,9 +2996,6 @@ void qemu_init(int argc, char **argv) fake_argv[fake_argc++] = strdup("-device"); fake_argv[fake_argc++] = strdup("usb-hub,port=1,ports=4"); - fake_argv[fake_argc++] = strdup("-audio"); - fake_argv[fake_argc++] = strdup("none"); - for (int i = 1; i < argc; i++) { if (argv[i] != NULL) { fake_argv[fake_argc++] = argv[i]; From 191bc40f7069fb09c3cbb709cb7125019315e9c9 Mon Sep 17 00:00:00 2001 From: Fred Hallock Date: Sat, 25 Jan 2025 22:48:58 -0500 Subject: [PATCH 7/7] 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 //