diff -ruN a/hw/usb/dev-lambbox-pad.c b/hw/usb/dev-lambbox-pad.c --- a/hw/usb/dev-lambbox-pad.c 1970-01-01 09:00:00 +++ b/hw/usb/dev-lambbox-pad.c 2026-10-03 11:14:03 @@ -0,0 +1,336 @@ +/* + * LambBox virtual gamepad + * + * A USB game controller that speaks the Xbox 360 (XInput) protocol, so + * Windows uses its built-in driver. The host app sends the state of a Mac + * game controller through a chardev: + * + * -chardev socket,id=pad0,path=/path/to/pad.sock,server=on,wait=off + * -device usb-lambbox-pad,chardev=pad0,bus=xhci.0 + * + * The device is plugged in only while the chardev is connected, so Windows + * sees a controller only while one is in use on the Mac. + * + * Input from the app: 12-byte packets, little endian + * uint16 buttons (XInput wButtons bits), uint8 left trigger, + * uint8 right trigger, int16 lx, ly, rx, ry + * + * Descriptors follow the wired Xbox 360 controller as documented by the + * GP2040-CE project (MIT). + * + * Copyright (c) 2026 LittleLamb Labs + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "qemu/osdep.h" +#include "qapi/error.h" +#include "qemu/module.h" +#include "hw/core/qdev-properties.h" +#include "hw/core/qdev-properties-system.h" +#include "hw/usb/usb.h" +#include "migration/vmstate.h" +#include "desc.h" +#include "chardev/char-fe.h" +#include "qom/object.h" + +#define TYPE_USB_LAMBBOX_PAD "usb-lambbox-pad" +OBJECT_DECLARE_SIMPLE_TYPE(USBLambBoxPadState, USB_LAMBBOX_PAD) + +#define PAD_PACKET_SIZE 12 +#define PAD_REPORT_SIZE 20 + +struct USBLambBoxPadState { + USBDevice dev; + CharFrontend cs; + USBEndpoint *intr; + + uint8_t rx[PAD_PACKET_SIZE]; + uint32_t rx_len; + uint8_t report[PAD_REPORT_SIZE]; + bool changed; +}; + +enum { + STR_MANUFACTURER = 1, + STR_PRODUCT, + STR_SERIALNUMBER, +}; + +static const USBDescStrings desc_strings = { + [STR_MANUFACTURER] = "LittleLamb Labs", + [STR_PRODUCT] = "LambBox Virtual Gamepad", + [STR_SERIALNUMBER] = "1", +}; + +/* Vendor-specific class descriptors of the Xbox 360 controller interfaces */ +static const uint8_t xinput_gamepad_desc[] = { + 0x11, 0x21, 0x00, 0x01, 0x01, 0x25, 0x81, 0x14, 0x00, + 0x00, 0x00, 0x00, 0x13, 0x02, 0x08, 0x00, 0x00, +}; +static const uint8_t xinput_audio_desc[] = { + 0x1B, 0x21, 0x00, 0x01, 0x01, 0x01, 0x83, 0x40, 0x01, 0x04, 0x20, 0x16, + 0x85, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x06, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, +}; +static const uint8_t xinput_plugin_desc[] = { + 0x09, 0x21, 0x00, 0x01, 0x01, 0x22, 0x86, 0x03, 0x00, +}; +static const uint8_t xinput_security_desc[] = { + 0x06, 0x41, 0x00, 0x01, 0x01, 0x03, +}; + +static USBDescOther gamepad_other[] = { + { .length = sizeof(xinput_gamepad_desc), .data = xinput_gamepad_desc }, +}; +static USBDescOther audio_other[] = { + { .length = sizeof(xinput_audio_desc), .data = xinput_audio_desc }, +}; +static USBDescOther plugin_other[] = { + { .length = sizeof(xinput_plugin_desc), .data = xinput_plugin_desc }, +}; +static USBDescOther security_other[] = { + { .length = sizeof(xinput_security_desc), .data = xinput_security_desc }, +}; + +static USBDescEndpoint gamepad_eps[] = { + { .bEndpointAddress = USB_DIR_IN | 0x01, .bmAttributes = USB_ENDPOINT_XFER_INT, + .wMaxPacketSize = 32, .bInterval = 1 }, + { .bEndpointAddress = USB_DIR_OUT | 0x02, .bmAttributes = USB_ENDPOINT_XFER_INT, + .wMaxPacketSize = 32, .bInterval = 8 }, +}; +static USBDescEndpoint audio_eps[] = { + { .bEndpointAddress = USB_DIR_IN | 0x03, .bmAttributes = USB_ENDPOINT_XFER_INT, + .wMaxPacketSize = 32, .bInterval = 2 }, + { .bEndpointAddress = USB_DIR_OUT | 0x04, .bmAttributes = USB_ENDPOINT_XFER_INT, + .wMaxPacketSize = 32, .bInterval = 4 }, + { .bEndpointAddress = USB_DIR_IN | 0x05, .bmAttributes = USB_ENDPOINT_XFER_INT, + .wMaxPacketSize = 32, .bInterval = 64 }, + { .bEndpointAddress = USB_DIR_OUT | 0x06, .bmAttributes = USB_ENDPOINT_XFER_INT, + .wMaxPacketSize = 32, .bInterval = 16 }, +}; +static USBDescEndpoint plugin_eps[] = { + { .bEndpointAddress = USB_DIR_IN | 0x06, .bmAttributes = USB_ENDPOINT_XFER_INT, + .wMaxPacketSize = 32, .bInterval = 16 }, +}; + +static const USBDescIface desc_ifaces[] = { + { + .bInterfaceNumber = 0, .bNumEndpoints = 2, + .bInterfaceClass = 0xFF, .bInterfaceSubClass = 0x5D, .bInterfaceProtocol = 0x01, + .ndesc = 1, .descs = gamepad_other, .eps = gamepad_eps, + }, { + .bInterfaceNumber = 1, .bNumEndpoints = 4, + .bInterfaceClass = 0xFF, .bInterfaceSubClass = 0x5D, .bInterfaceProtocol = 0x03, + .ndesc = 1, .descs = audio_other, .eps = audio_eps, + }, { + .bInterfaceNumber = 2, .bNumEndpoints = 1, + .bInterfaceClass = 0xFF, .bInterfaceSubClass = 0x5D, .bInterfaceProtocol = 0x02, + .ndesc = 1, .descs = plugin_other, .eps = plugin_eps, + }, { + .bInterfaceNumber = 3, .bNumEndpoints = 0, + .bInterfaceClass = 0xFF, .bInterfaceSubClass = 0xFD, .bInterfaceProtocol = 0x13, + .ndesc = 1, .descs = security_other, + }, +}; + +static const USBDescDevice desc_device = { + .bcdUSB = 0x0200, + .bDeviceClass = 0xFF, + .bDeviceSubClass = 0xFF, + .bDeviceProtocol = 0xFF, + .bMaxPacketSize0 = 64, + .bNumConfigurations = 1, + .confs = (USBDescConfig[]) { + { + .bNumInterfaces = 4, + .bConfigurationValue = 1, + .bmAttributes = USB_CFG_ATT_ONE | USB_CFG_ATT_WAKEUP, + .bMaxPower = 250, + .nif = ARRAY_SIZE(desc_ifaces), + .ifs = desc_ifaces, + }, + }, +}; + +static const USBDesc desc_pad = { + .id = { + /* Wired Xbox 360 controller: Windows binds its built-in XInput driver */ + .idVendor = 0x045E, + .idProduct = 0x028E, + .bcdDevice = 0x0114, + .iManufacturer = STR_MANUFACTURER, + .iProduct = STR_PRODUCT, + .iSerialNumber = STR_SERIALNUMBER, + }, + .full = &desc_device, + .str = desc_strings, +}; + +static void pad_reset_report(USBLambBoxPadState *s) +{ + memset(s->report, 0, sizeof(s->report)); + s->report[1] = PAD_REPORT_SIZE; + s->changed = true; +} + +static void pad_handle_reset(USBDevice *dev) +{ + USBLambBoxPadState *s = USB_LAMBBOX_PAD(dev); + + s->rx_len = 0; + pad_reset_report(s); +} + +static void pad_handle_control(USBDevice *dev, USBPacket *p, int request, + int value, int index, int length, uint8_t *data) +{ + int ret = usb_desc_handle_control(dev, p, request, value, index, length, data); + + if (ret < 0) { + /* Vendor requests (serial number, security) are not needed by Windows */ + p->status = USB_RET_STALL; + } +} + +static void pad_handle_data(USBDevice *dev, USBPacket *p) +{ + USBLambBoxPadState *s = USB_LAMBBOX_PAD(dev); + + if (p->pid == USB_TOKEN_IN && p->ep->nr == 1) { + if (!s->changed) { + p->status = USB_RET_NAK; + return; + } + usb_packet_copy(p, s->report, MIN(p->iov.size, sizeof(s->report))); + s->changed = false; + } else if (p->pid == USB_TOKEN_IN) { + /* headset and plug-in module: nothing attached */ + p->status = USB_RET_NAK; + } + /* OUT (rumble, LEDs): accepted and ignored */ +} + +static int pad_can_read(void *opaque) +{ + USBLambBoxPadState *s = opaque; + + return PAD_PACKET_SIZE - s->rx_len; +} + +static void pad_read(void *opaque, const uint8_t *buf, int size) +{ + USBLambBoxPadState *s = opaque; + + while (size > 0) { + int n = MIN(size, (int)(PAD_PACKET_SIZE - s->rx_len)); + + memcpy(s->rx + s->rx_len, buf, n); + s->rx_len += n; + buf += n; + size -= n; + if (s->rx_len == PAD_PACKET_SIZE) { + /* buttons(2) lt rt lx(2) ly(2) rx(2) ry(2) -> report bytes 2..13 */ + memcpy(s->report + 2, s->rx, PAD_PACKET_SIZE); + s->rx_len = 0; + s->changed = true; + if (s->intr) { + usb_wakeup(s->intr, 0); + } + } + } +} + +static void pad_event(void *opaque, QEMUChrEvent event) +{ + USBLambBoxPadState *s = opaque; + + switch (event) { + case CHR_EVENT_OPENED: + s->rx_len = 0; + pad_reset_report(s); + if (!s->dev.attached) { + usb_device_attach(&s->dev, &error_abort); + } + break; + case CHR_EVENT_CLOSED: + if (s->dev.attached) { + usb_device_detach(&s->dev); + } + break; + default: + break; + } +} + +static void pad_realize(USBDevice *dev, Error **errp) +{ + USBLambBoxPadState *s = USB_LAMBBOX_PAD(dev); + + usb_desc_init(dev); + dev->auto_attach = 0; + + if (!qemu_chr_fe_backend_connected(&s->cs)) { + error_setg(errp, "Property chardev is required"); + return; + } + Error *local_err = NULL; + + usb_check_attach(dev, &local_err); + if (local_err) { + error_propagate(errp, local_err); + return; + } + s->intr = usb_ep_get(dev, USB_TOKEN_IN, 1); + pad_handle_reset(dev); + qemu_chr_fe_set_handlers(&s->cs, pad_can_read, pad_read, pad_event, + NULL, s, NULL, true); + if (qemu_chr_fe_backend_open(&s->cs) && !dev->attached) { + usb_device_attach(dev, &error_abort); + } +} + +static const VMStateDescription vmstate_pad = { + .name = "usb-lambbox-pad", + .version_id = 1, + .minimum_version_id = 1, + .fields = (const VMStateField[]) { + VMSTATE_USB_DEVICE(dev, USBLambBoxPadState), + VMSTATE_BUFFER(report, USBLambBoxPadState), + VMSTATE_BOOL(changed, USBLambBoxPadState), + VMSTATE_END_OF_LIST() + }, +}; + +static const Property pad_properties[] = { + DEFINE_PROP_CHR("chardev", USBLambBoxPadState, cs), +}; + +static void pad_class_init(ObjectClass *klass, const void *data) +{ + DeviceClass *dc = DEVICE_CLASS(klass); + USBDeviceClass *uc = USB_DEVICE_CLASS(klass); + + uc->product_desc = "LambBox Virtual Gamepad"; + uc->usb_desc = &desc_pad; + uc->realize = pad_realize; + uc->handle_reset = pad_handle_reset; + uc->handle_control = pad_handle_control; + uc->handle_data = pad_handle_data; + dc->vmsd = &vmstate_pad; + device_class_set_props(dc, pad_properties); + set_bit(DEVICE_CATEGORY_INPUT, dc->categories); +} + +static const TypeInfo pad_info = { + .name = TYPE_USB_LAMBBOX_PAD, + .parent = TYPE_USB_DEVICE, + .instance_size = sizeof(USBLambBoxPadState), + .class_init = pad_class_init, +}; + +static void pad_register_types(void) +{ + type_register_static(&pad_info); +} + +type_init(pad_register_types) diff -ruN a/hw/usb/meson.build b/hw/usb/meson.build --- a/hw/usb/meson.build 2026-10-03 11:14:03 +++ b/hw/usb/meson.build 2026-10-03 11:14:03 @@ -43,6 +43,7 @@ system_ss.add(when: 'CONFIG_USB_STORAGE_UAS', if_true: files('dev-uas.c')) system_ss.add(when: 'CONFIG_USB_AUDIO', if_true: files('dev-audio.c')) system_ss.add(when: 'CONFIG_USB_SERIAL', if_true: files('dev-serial.c')) +system_ss.add(when: 'CONFIG_USB', if_true: files('dev-lambbox-pad.c')) system_ss.add(when: 'CONFIG_USB_NETWORK', if_true: files('dev-network.c')) if host_os != 'windows' system_ss.add(when: 'CONFIG_USB_STORAGE_MTP', if_true: files('dev-mtp.c'))