diff -ruN a/hw/usb/dev-lambbox-cam.c b/hw/usb/dev-lambbox-cam.c --- a/hw/usb/dev-lambbox-cam.c 1970-01-01 09:00:00 +++ b/hw/usb/dev-lambbox-cam.c 2026-10-03 14:26:51 @@ -0,0 +1,606 @@ +/* + * LambBox virtual webcam + * + * A USB Video Class (UVC 1.0) camera with MJPEG frames on a bulk endpoint, + * so Windows uses its built-in camera driver. The host app captures the Mac + * camera and sends JPEG frames through a chardev: + * + * -chardev socket,id=cam0,path=/path/to/cam.sock,server=on,wait=off + * -device usb-lambbox-cam,chardev=cam0,bus=xhci.0 + * + * App -> device: frames as uint32 little-endian length + JPEG data + * Device -> app: 'S' + uint16 width + uint16 height (start capturing), + * 'P' (stop capturing) + * + * The device is plugged in while the chardev is connected. The Mac camera + * runs only while Windows is streaming. + * + * 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 "qemu/timer.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_CAM "usb-lambbox-cam" +OBJECT_DECLARE_SIMPLE_TYPE(USBLambBoxCamState, USB_LAMBBOX_CAM) + +#define CAM_MAX_FRAME (4 * 1024 * 1024) +#define CAM_FRAME_INTERVAL 333333 /* 30 frames per second, 100 ns units */ +#define CAM_PROBE_SIZE 26 /* UVC 1.0 probe/commit structure */ +#define CAM_EP_STREAM 1 +#define CAM_IDLE_STOP_MS 1500 + +/* UVC requests */ +#define UVC_SET_CUR 0x01 +#define UVC_GET_CUR 0x81 +#define UVC_GET_MIN 0x82 +#define UVC_GET_MAX 0x83 +#define UVC_GET_RES 0x84 +#define UVC_GET_LEN 0x85 +#define UVC_GET_INFO 0x86 +#define UVC_GET_DEF 0x87 +#define UVC_VS_PROBE_CONTROL 0x01 +#define UVC_VS_COMMIT_CONTROL 0x02 + +typedef struct CamFrameSize { + uint16_t width, height; +} CamFrameSize; + +static const CamFrameSize cam_sizes[] = { + { 1280, 720 }, + { 640, 480 }, +}; + +struct USBLambBoxCamState { + USBDevice dev; + CharFrontend cs; + USBEndpoint *stream_ep; + QEMUTimer *idle_timer; + + /* incoming frame from the app */ + uint8_t rx_len_buf[4]; + uint32_t rx_len_got; + uint32_t rx_size; + uint8_t *rx_buf; + uint32_t rx_got; + + /* latest complete frame */ + uint8_t *frame; + uint32_t frame_size; + uint32_t frame_seq; + + /* frame being sent to the guest */ + uint8_t *tx; + uint32_t tx_size; + uint32_t tx_off; + uint32_t sent_seq; + uint8_t fid; + + uint8_t probe[CAM_PROBE_SIZE]; + uint8_t frame_index; + bool streaming; + /* the guest asked for data and is waiting for the next frame (NAK'ed) */ + bool guest_waiting; +}; + +enum { + STR_MANUFACTURER = 1, + STR_PRODUCT, + STR_SERIALNUMBER, +}; + +static const USBDescStrings desc_strings = { + [STR_MANUFACTURER] = "LittleLamb Labs", + [STR_PRODUCT] = "LambBox Camera", + [STR_SERIALNUMBER] = "1", +}; + +/* Video control: header, camera input terminal (ID 1), output terminal (ID 2) */ +static const uint8_t vc_desc[] = { + /* VC interface header, UVC 1.0, total 40, 48 MHz clock, streaming iface 1 */ + 0x0D, 0x24, 0x01, 0x00, 0x01, 0x28, 0x00, 0x00, 0x6C, 0xDC, 0x02, 0x01, 0x01, + /* camera input terminal, no controls */ + 0x12, 0x24, 0x02, 0x01, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + /* output terminal (USB streaming), source = camera */ + 0x09, 0x24, 0x03, 0x02, 0x01, 0x01, 0x00, 0x01, 0x00, +}; + +#define LE16(v) ((v) & 0xff), (((v) >> 8) & 0xff) +#define LE32(v) ((v) & 0xff), (((v) >> 8) & 0xff), (((v) >> 16) & 0xff), (((v) >> 24) & 0xff) +#define MJPEG_FRAME(index, w, h) \ + 0x1E, 0x24, 0x07, (index), 0x00, LE16(w), LE16(h), \ + LE32((w) * (h) * 16 * 30), LE32((w) * (h) * 16 * 30), LE32((w) * (h) * 2), \ + LE32(CAM_FRAME_INTERVAL), 0x01, LE32(CAM_FRAME_INTERVAL) + +/* Video streaming: input header, MJPEG format, two frame sizes, color matching */ +static const uint8_t vs_desc[] = { + /* input header: 1 format, total 91, endpoint 0x81, output terminal 2 */ + 0x0E, 0x24, 0x01, 0x01, LE16(91), 0x81, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, + /* MJPEG format: index 1, 2 frame sizes, fixed size samples, default frame 1 */ + 0x0B, 0x24, 0x06, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, + MJPEG_FRAME(1, 1280, 720), + MJPEG_FRAME(2, 640, 480), + /* color matching: BT.709, BT.709, SMPTE 170M */ + 0x06, 0x24, 0x0D, 0x01, 0x01, 0x04, +}; + +static USBDescOther vc_other[] = { + { .length = sizeof(vc_desc), .data = vc_desc }, +}; +static USBDescOther vs_other[] = { + { .length = sizeof(vs_desc), .data = vs_desc }, +}; + +static USBDescEndpoint vs_eps[] = { + { .bEndpointAddress = USB_DIR_IN | CAM_EP_STREAM, .bmAttributes = USB_ENDPOINT_XFER_BULK, + .wMaxPacketSize = 512 }, +}; + +static const USBDescIface cam_ifaces[] = { + { + .bInterfaceNumber = 0, .bNumEndpoints = 0, + .bInterfaceClass = 0x0E, .bInterfaceSubClass = 0x01, .bInterfaceProtocol = 0x00, + .ndesc = 1, .descs = vc_other, + }, { + .bInterfaceNumber = 1, .bNumEndpoints = 1, + .bInterfaceClass = 0x0E, .bInterfaceSubClass = 0x02, .bInterfaceProtocol = 0x00, + .ndesc = 1, .descs = vs_other, .eps = vs_eps, + }, +}; + +static const USBDescIfaceAssoc cam_iad = { + .bFirstInterface = 0, + .bInterfaceCount = 2, + .bFunctionClass = 0x0E, + .bFunctionSubClass = 0x03, + .bFunctionProtocol = 0x00, + .nif = ARRAY_SIZE(cam_ifaces), + .ifs = cam_ifaces, +}; + +static const USBDescDevice desc_device_high = { + .bcdUSB = 0x0200, + .bDeviceClass = 0xEF, /* miscellaneous: interface association */ + .bDeviceSubClass = 0x02, + .bDeviceProtocol = 0x01, + .bMaxPacketSize0 = 64, + .bNumConfigurations = 1, + .confs = (USBDescConfig[]) { + { + .bNumInterfaces = 2, + .bConfigurationValue = 1, + .bmAttributes = USB_CFG_ATT_ONE, + .bMaxPower = 250, + .nif_groups = 1, + .if_groups = &cam_iad, + }, + }, +}; + +static const USBDesc desc_cam = { + .id = { + /* QEMU's vendor ID with a product number of our own */ + .idVendor = 0x46f4, + .idProduct = 0x0c4a, + .bcdDevice = 0x0100, + .iManufacturer = STR_MANUFACTURER, + .iProduct = STR_PRODUCT, + .iSerialNumber = STR_SERIALNUMBER, + }, + .high = &desc_device_high, + .str = desc_strings, +}; + +/* ---- probe / commit ---- */ + +static uint32_t cam_frame_bytes(uint8_t index) +{ + const CamFrameSize *f = &cam_sizes[index - 1]; + + return f->width * f->height * 2; +} + +static void cam_fill_probe(uint8_t *p, uint8_t frame_index) +{ + memset(p, 0, CAM_PROBE_SIZE); + p[2] = 1; /* bFormatIndex */ + p[3] = frame_index; /* bFrameIndex */ + stl_le_p(p + 4, CAM_FRAME_INTERVAL); /* dwFrameInterval */ + stl_le_p(p + 18, cam_frame_bytes(frame_index)); /* dwMaxVideoFrameSize */ + stl_le_p(p + 22, cam_frame_bytes(frame_index) + 2); /* dwMaxPayloadTransferSize */ +} + +static void cam_send(USBLambBoxCamState *s, const uint8_t *buf, int len) +{ + if (qemu_chr_fe_backend_open(&s->cs)) { + qemu_chr_fe_write_all(&s->cs, buf, len); + } +} + +static void cam_start(USBLambBoxCamState *s) +{ + const CamFrameSize *f = &cam_sizes[s->frame_index - 1]; + uint8_t msg[5] = { 'S' }; + + stw_le_p(msg + 1, f->width); + stw_le_p(msg + 3, f->height); + s->streaming = true; + s->guest_waiting = false; + s->tx = NULL; + s->sent_seq = s->frame_seq; + cam_send(s, msg, sizeof(msg)); + timer_mod(s->idle_timer, qemu_clock_get_ms(QEMU_CLOCK_REALTIME) + CAM_IDLE_STOP_MS); +} + +static void cam_stop(USBLambBoxCamState *s) +{ + uint8_t msg = 'P'; + + if (!s->streaming) { + return; + } + s->streaming = false; + s->guest_waiting = false; + s->tx = NULL; + timer_del(s->idle_timer); + cam_send(s, &msg, 1); +} + +/* + * The guest stopped reading frames (bulk streams have no explicit stop). + * A request that is still waiting for a frame (first frame after the Mac + * camera starts, which can take seconds) is not "stopped". + */ +static void cam_idle(void *opaque) +{ + USBLambBoxCamState *s = opaque; + + if (s->guest_waiting) { + timer_mod(s->idle_timer, qemu_clock_get_ms(QEMU_CLOCK_REALTIME) + CAM_IDLE_STOP_MS); + return; + } + cam_stop(s); +} + +/* The guest gave up on a waiting request (stream stopped or device reset) */ +static void cam_cancel_packet(USBDevice *dev, USBPacket *p) +{ + USBLambBoxCamState *s = USB_LAMBBOX_CAM(dev); + + s->guest_waiting = false; + if (s->streaming) { + timer_mod(s->idle_timer, qemu_clock_get_ms(QEMU_CLOCK_REALTIME) + CAM_IDLE_STOP_MS); + } +} + +static void cam_handle_reset(USBDevice *dev) +{ + USBLambBoxCamState *s = USB_LAMBBOX_CAM(dev); + + cam_stop(s); + s->frame_index = 1; + cam_fill_probe(s->probe, 1); +} + +static void cam_handle_control(USBDevice *dev, USBPacket *p, int request, + int value, int index, int length, uint8_t *data) +{ + USBLambBoxCamState *s = USB_LAMBBOX_CAM(dev); + int selector = value >> 8; + int iface = index & 0xff; + int ret; + + ret = usb_desc_handle_control(dev, p, request, value, index, length, data); + if (ret >= 0) { + return; + } + + switch (request) { + case EndpointOutRequest | USB_REQ_CLEAR_FEATURE: + /* clearing a halt on the streaming endpoint: the guest stopped streaming */ + cam_stop(s); + p->actual_length = 0; + return; + case ClassInterfaceRequest | UVC_GET_CUR: + case ClassInterfaceRequest | UVC_GET_MIN: + case ClassInterfaceRequest | UVC_GET_MAX: + case ClassInterfaceRequest | UVC_GET_DEF: + if (iface == 1 && (selector == UVC_VS_PROBE_CONTROL || + selector == UVC_VS_COMMIT_CONTROL)) { + uint8_t probe[CAM_PROBE_SIZE]; + + if ((request & 0xff) == UVC_GET_CUR) { + memcpy(probe, s->probe, sizeof(probe)); + } else if ((request & 0xff) == UVC_GET_MAX) { + cam_fill_probe(probe, ARRAY_SIZE(cam_sizes)); + } else { + cam_fill_probe(probe, 1); + } + memcpy(data, probe, MIN(length, CAM_PROBE_SIZE)); + p->actual_length = MIN(length, CAM_PROBE_SIZE); + return; + } + break; + case ClassInterfaceRequest | UVC_GET_LEN: + if (iface == 1 && length >= 2) { + stw_le_p(data, CAM_PROBE_SIZE); + p->actual_length = 2; + return; + } + break; + case ClassInterfaceRequest | UVC_GET_INFO: + if (iface == 1 && length >= 1) { + data[0] = 0x03; /* supports GET and SET */ + p->actual_length = 1; + return; + } + break; + case ClassInterfaceOutRequest | UVC_SET_CUR: + if (iface == 1 && (selector == UVC_VS_PROBE_CONTROL || + selector == UVC_VS_COMMIT_CONTROL)) { + uint8_t frame_index = length >= 4 ? data[3] : 1; + + if (frame_index < 1 || frame_index > ARRAY_SIZE(cam_sizes)) { + frame_index = 1; + } + /* keep our own limits; the guest reads them back with GET_CUR */ + cam_fill_probe(s->probe, frame_index); + if (selector == UVC_VS_COMMIT_CONTROL) { + s->frame_index = frame_index; + cam_start(s); + } + p->actual_length = 0; + return; + } + break; + } + p->status = USB_RET_STALL; +} + +static void cam_set_interface(USBDevice *dev, int interface, + int alt_old, int alt_new) +{ + if (interface == 1 && alt_new == 0) { + cam_stop(USB_LAMBBOX_CAM(dev)); + } +} + +static void cam_handle_data(USBDevice *dev, USBPacket *p) +{ + USBLambBoxCamState *s = USB_LAMBBOX_CAM(dev); + uint8_t header[2]; + size_t room, chunk; + + if (p->pid != USB_TOKEN_IN || p->ep->nr != CAM_EP_STREAM) { + p->status = USB_RET_STALL; + return; + } + if (!s->streaming) { + p->status = USB_RET_NAK; + return; + } + /* the guest is still reading: keep the Mac camera on */ + timer_mod(s->idle_timer, qemu_clock_get_ms(QEMU_CLOCK_REALTIME) + CAM_IDLE_STOP_MS); + + if (!s->tx) { + if (!s->frame || s->frame_seq == s->sent_seq) { + s->guest_waiting = true; + p->status = USB_RET_NAK; + return; + } + s->guest_waiting = false; + /* start sending the newest frame */ + g_free(s->tx); + s->tx = g_memdup2(s->frame, s->frame_size); + s->tx_size = s->frame_size; + s->tx_off = 0; + s->sent_seq = s->frame_seq; + s->fid ^= 1; + } + + room = p->iov.size > sizeof(header) ? p->iov.size - sizeof(header) : 0; + chunk = MIN(room, s->tx_size - s->tx_off); + header[0] = sizeof(header); + header[1] = 0x80 | s->fid; /* end of header, frame ID */ + if (s->tx_off + chunk == s->tx_size) { + header[1] |= 0x02; /* end of frame */ + } + usb_packet_copy(p, header, sizeof(header)); + usb_packet_copy(p, s->tx + s->tx_off, chunk); + s->tx_off += chunk; + if (s->tx_off == s->tx_size) { + g_free(s->tx); + s->tx = NULL; + } +} + +/* ---- frames from the app ---- */ + +static int cam_can_read(void *opaque) +{ + return 64 * 1024; +} + +static void cam_frame_done(USBLambBoxCamState *s) +{ + g_free(s->frame); + s->frame = s->rx_buf; + s->frame_size = s->rx_size; + s->frame_seq++; + s->rx_buf = NULL; + if (s->streaming && s->stream_ep) { + usb_wakeup(s->stream_ep, 0); + } +} + +static void cam_read(void *opaque, const uint8_t *buf, int size) +{ + USBLambBoxCamState *s = opaque; + + while (size > 0) { + if (s->rx_len_got < 4) { + s->rx_len_buf[s->rx_len_got++] = *buf++; + size--; + if (s->rx_len_got == 4) { + s->rx_size = ldl_le_p(s->rx_len_buf); + if (s->rx_size == 0 || s->rx_size > CAM_MAX_FRAME) { + /* broken stream: wait for the app to reconnect */ + s->rx_len_got = 0; + return; + } + s->rx_buf = g_malloc(s->rx_size); + s->rx_got = 0; + } + continue; + } + uint32_t n = MIN((uint32_t)size, s->rx_size - s->rx_got); + + memcpy(s->rx_buf + s->rx_got, buf, n); + s->rx_got += n; + buf += n; + size -= n; + if (s->rx_got == s->rx_size) { + cam_frame_done(s); + s->rx_len_got = 0; + } + } +} + +static void cam_reset_rx(USBLambBoxCamState *s) +{ + g_free(s->rx_buf); + s->rx_buf = NULL; + s->rx_len_got = 0; +} + +static void cam_event(void *opaque, QEMUChrEvent event) +{ + USBLambBoxCamState *s = opaque; + + switch (event) { + case CHR_EVENT_OPENED: + cam_reset_rx(s); + if (!s->dev.attached) { + usb_device_attach(&s->dev, &error_abort); + } + break; + case CHR_EVENT_CLOSED: + cam_reset_rx(s); + s->streaming = false; + if (s->dev.attached) { + usb_device_detach(&s->dev); + } + break; + default: + break; + } +} + +static void cam_realize(USBDevice *dev, Error **errp) +{ + USBLambBoxCamState *s = USB_LAMBBOX_CAM(dev); + Error *local_err = NULL; + + usb_desc_init(dev); + dev->auto_attach = 0; + + if (!qemu_chr_fe_backend_connected(&s->cs)) { + error_setg(errp, "Property chardev is required"); + return; + } + usb_check_attach(dev, &local_err); + if (local_err) { + error_propagate(errp, local_err); + return; + } + s->stream_ep = usb_ep_get(dev, USB_TOKEN_IN, CAM_EP_STREAM); + s->idle_timer = timer_new_ms(QEMU_CLOCK_REALTIME, cam_idle, s); + cam_handle_reset(dev); + qemu_chr_fe_set_handlers(&s->cs, cam_can_read, cam_read, cam_event, + NULL, s, NULL, true); + if (qemu_chr_fe_backend_open(&s->cs) && !dev->attached) { + usb_device_attach(dev, &error_abort); + } +} + +static void cam_unrealize(USBDevice *dev) +{ + USBLambBoxCamState *s = USB_LAMBBOX_CAM(dev); + + timer_free(s->idle_timer); + g_free(s->rx_buf); + g_free(s->frame); + g_free(s->tx); +} + +static int cam_post_load(void *opaque, int version_id) +{ + USBLambBoxCamState *s = opaque; + + /* the app starts the Mac camera again when Windows asks for frames */ + s->streaming = false; + s->tx = NULL; + return 0; +} + +static const VMStateDescription vmstate_cam = { + .name = "usb-lambbox-cam", + .version_id = 1, + .minimum_version_id = 1, + .post_load = cam_post_load, + .fields = (const VMStateField[]) { + VMSTATE_USB_DEVICE(dev, USBLambBoxCamState), + VMSTATE_BUFFER(probe, USBLambBoxCamState), + VMSTATE_UINT8(frame_index, USBLambBoxCamState), + VMSTATE_END_OF_LIST() + }, +}; + +static const Property cam_properties[] = { + DEFINE_PROP_CHR("chardev", USBLambBoxCamState, cs), +}; + +static void cam_class_init(ObjectClass *klass, const void *data) +{ + DeviceClass *dc = DEVICE_CLASS(klass); + USBDeviceClass *uc = USB_DEVICE_CLASS(klass); + + uc->product_desc = "LambBox Camera"; + uc->usb_desc = &desc_cam; + uc->realize = cam_realize; + uc->unrealize = cam_unrealize; + uc->handle_attach = usb_desc_attach; + uc->handle_reset = cam_handle_reset; + uc->handle_control = cam_handle_control; + uc->handle_data = cam_handle_data; + uc->cancel_packet = cam_cancel_packet; + uc->set_interface = cam_set_interface; + dc->vmsd = &vmstate_cam; + device_class_set_props(dc, cam_properties); + set_bit(DEVICE_CATEGORY_USB, dc->categories); +} + +static const TypeInfo cam_info = { + .name = TYPE_USB_LAMBBOX_CAM, + .parent = TYPE_USB_DEVICE, + .instance_size = sizeof(USBLambBoxCamState), + .class_init = cam_class_init, +}; + +static void cam_register_types(void) +{ + type_register_static(&cam_info); +} + +type_init(cam_register_types) diff -ruN a/hw/usb/meson.build b/hw/usb/meson.build --- a/hw/usb/meson.build 2026-10-03 14:26:51 +++ b/hw/usb/meson.build 2026-10-03 14:26:51 @@ -44,6 +44,7 @@ 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', if_true: files('dev-lambbox-cam.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'))