* [PATCH 1/2] hw/usb/dev-hid: add apple-magic-keyboard
2026-07-19 15:23 [PATCH 0/2] hw/usb/dev-hid: Apple Magic Keyboard and Mighty Mouse emulators Matthew Jackson
@ 2026-07-19 15:23 ` Matthew Jackson
2026-07-19 15:23 ` [PATCH 2/2] hw/usb/dev-hid: add apple-mighty-mouse Matthew Jackson
1 sibling, 0 replies; 6+ messages in thread
From: Matthew Jackson @ 2026-07-19 15:23 UTC (permalink / raw)
To: qemu-devel; +Cc: kraxel
Adds a self-contained USB-HID device emulating the USB-cable face of
the real Apple Magic Keyboard with Numeric Keypad (idVendor 0x05ac,
idProduct 0x026c, bcdDevice 0x0870). Descriptors and HID report
descriptors are byte-identical to a real Magic Keyboard captured in
USB-cable mode.
QEMU has no USB-HID device that macOS recognises as a real Apple
peripheral. With usb-kbd, macOS guests run Keyboard Setup Assistant on
first boot, which costs 3-5 minutes on interactive installs and
effectively hangs headless (VNC / SPICE) installs. During recovery /
install, the Apple HID stack (AppleUSBTopCaseHIDDriver,
AppleDeviceManagementHIDEventService, AppleHIDKeyboardEventDriverV2)
probes for Apple-VID match dictionaries before falling back to the
generic IOUSBHostHIDDevice path. Recovery-only UI panels behave
correctly only when the chain matches an Apple-VID peripheral.
The device is a composite USB device with two HID interfaces:
Interface 0 — Apple-vendor HID (UsagePage 0xff00). Carries three
Apple proprietary reports macOS uses to identify the keyboard
family: Report 0xe0 (vendor identity), Report 0x9a (capabilities),
Report 0x90 (battery state for wireless heritage).
Interface 1 — boot keyboard (UsagePage 0x07). Standard 10-byte boot
keyboard report (modifier byte + reserved + 6 keycodes + Consumer
Eject + Vendor reserved). Bound by the boot interface protocol so
macOS recovery's pre-userspace input works without any vendor
interaction.
A self-contained QKeyCode → USB HID Usage map ships with the device so
the apple-magic-keyboard implementation does not depend on hid.c's
generic mapping (which is tied to usb-kbd's report layout).
Verified end-to-end on macOS 15.7.5: enumeration matches a real Magic
Keyboard byte-for-byte; AppleUSBTopCaseHIDDriver +
AppleHIDKeyboardEventDriverV2 bind on boot; interactive password +
Enter logs into the desktop.
Signed-off-by: Matthew Jackson <matthew@pq.io>
---
hw/usb/dev-hid.c | 838 +++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 838 insertions(+)
diff --git a/hw/usb/dev-hid.c b/hw/usb/dev-hid.c
index ae19d60..7cce69b 100644
--- a/hw/usb/dev-hid.c
+++ b/hw/usb/dev-hid.c
@@ -25,6 +25,7 @@
#include "qemu/osdep.h"
#include "ui/console.h"
+#include "ui/input.h"
#include "hw/usb/usb.h"
#include "migration/vmstate.h"
#include "desc.h"
@@ -862,6 +863,842 @@ static const TypeInfo usb_keyboard_info = {
.class_init = usb_keyboard_class_initfn,
};
+
+/*
+ * apple-magic-keyboard
+ * --------------------
+ *
+ * USB-mode emulator for the Apple Magic Keyboard with Numeric Keypad
+ * (idVendor 0x05ac, idProduct 0x026c). Carries Apple's vendor-defined
+ * HID protocol on UsagePage 0xff00 alongside a standard HID Boot
+ * Keyboard interface; the descriptors and HID report descriptors are
+ * byte-identical to a real Magic Keyboard captured in USB-cable mode.
+ *
+ * Two HID interfaces:
+ *
+ * Interface 0 — Apple-vendor HID (UsagePage 0xff00). Carries three
+ * vendor input report IDs that the macOS Apple HID driver chain
+ * (AppleUSBTopCaseHIDDriver → AppleDeviceManagementHIDEventService
+ * → AppleUserHIDEventDriver) probes against:
+ *
+ * 0xe0 4 bytes vendor keyboard event
+ * 0x9a 1 byte modifier-state change / short signal
+ * 0x90 2 bytes power flags + battery percent
+ *
+ * A 1 Hz 0x90 heartbeat keeps the macOS userspace HID watchdog
+ * considering the device alive. Real device firmware emits the
+ * same heartbeat regardless of activity.
+ *
+ * Interface 1 — standard HID Boot Keyboard (UsagePage 0x07,
+ * bInterfaceSubClass 1, bInterfaceProtocol 1). Emits the standard
+ * 10-byte boot-keyboard input report (modifier byte + 7 keycode
+ * slots) on EP2 IN. Wired to QEMU's input subsystem via
+ * qemu_input_handler_register; any RFB / SPICE / SDL / HMP
+ * `sendkey` source drives it.
+ *
+ * The vendor-encoded keystroke format on Interface 0 (where reports
+ * 0xe0 / 0x9a would carry typing data) is not generated here; macOS's
+ * boot-keyboard claim path drives input via Interface 1, which is
+ * sufficient for typing and modifier handling.
+ */
+
+/*
+ * USB endpoint numbers within the apple-magic-keyboard device.
+ * EP1 IN = vendor-defined HID reports (Interface 0).
+ * EP2 IN = boot keyboard reports (Interface 1).
+ */
+#define AMK_EP_VENDOR_IN 1
+#define AMK_EP_BOOT_IN 2
+
+/*
+ * Apple Magic Keyboard boot-keyboard input report:
+ * byte 0 — Report ID (0x01)
+ * byte 1 — modifier byte (LCtrl=0x01 LShift=0x02 LAlt=0x04 LGUI=0x08
+ * RCtrl=0x10 RShift=0x20 RAlt=0x40 RGUI=0x80)
+ * byte 2 — reserved (0)
+ * bytes 3..9 — up to 7 simultaneous HID Usage codes, 0 in unused slots
+ */
+#define AMK_BOOT_REPORT_ID 0x01
+/*
+ * Boot keyboard input report (10 bytes total — byte-for-byte the
+ * Apple Magic Keyboard format captured 2026-05-08):
+ * byte 0: report ID (0x01)
+ * byte 1: modifier byte (8 bits, HID Usages 0xE0..0xE7)
+ * byte 2: reserved (always 0)
+ * bytes 3..8: 6 keycode slots (HID Usage codes, 0 in unused slots)
+ * byte 9: bit0 = Eject (Consumer Page), bits1..7 = vendor 0xff00
+ * Usage 0x03 (always 0 for our emulator).
+ *
+ * NOTE: real Apple boot keyboard convention is 6 keycode slots, not
+ * the 7 some older docs describe. Apple's `AppleHIDKeyboardEventDriverV2`
+ * match dictionary depends on this exact layout — using 7 slots makes
+ * the driver decline to bind, which leaves the boot interface's
+ * IOHIDInterface unclaimed and 60s busy-times out on installed macOS.
+ */
+#define AMK_BOOT_REPORT_LEN 10
+#define AMK_BOOT_NUM_KEYS 6
+
+typedef struct USBAppleMagicKbdState {
+ USBDevice dev;
+ USBEndpoint *boot_intr; /* EP2 IN */
+ QemuInputHandlerState *input_handler;
+ /* Pressed-key state in HID Usage codes (UsagePage 0x07). */
+ uint8_t boot_modifiers; /* live modifier byte */
+ uint8_t boot_keys[AMK_BOOT_NUM_KEYS]; /* live slots */
+ bool boot_changed; /* report needs sending */
+} USBAppleMagicKbdState;
+
+#define TYPE_USB_APPLE_MAGIC_KBD "apple-magic-keyboard"
+OBJECT_DECLARE_SIMPLE_TYPE(USBAppleMagicKbdState, USB_APPLE_MAGIC_KBD)
+
+enum {
+ STR_AMK_MFR = 1,
+ STR_AMK_PRODUCT,
+ STR_AMK_SERIAL,
+ STR_AMK_INTERFACE,
+ STR_AMK_INTERFACE_BOOT,
+};
+
+static const USBDescStrings desc_strings_amk = {
+ [STR_AMK_MFR] = "Apple Inc.",
+ [STR_AMK_PRODUCT] = "Magic Keyboard with Numeric Keypad",
+ [STR_AMK_SERIAL] = "F0T924300PCJKNCAS",
+ [STR_AMK_INTERFACE] = "Device Management",
+ [STR_AMK_INTERFACE_BOOT] = "Keyboard / Boot",
+};
+
+/*
+ * HID Report Descriptor — byte-identical to a real Magic Keyboard
+ * with Numeric Keypad over USB. Vendor-defined UsagePage 0xff00.
+ *
+ * Three input report IDs (real device's "InputReportElements"):
+ * 0xe0: 4 bytes — Apple-encoded keyboard event payload (keys / mods)
+ * 0x9a: 1 byte — short-form vendor signal
+ * 0x90: 3 bytes — power/battery status (charging, AC, percent)
+ *
+ * Decoded layout (see project_apple_magic_hid_emulator_2026_05_07.md):
+ * Application 1: UsagePage 0xff00 / Usage 0x0b — keystrokes + signals
+ * Application 2: UsagePage 0xff00 / Usage 0x14 — power/battery
+ */
+static const uint8_t apple_magic_kbd_hid_report_descriptor[] = {
+ /* Application 1: keystroke / vendor signal */
+ 0x06, 0x00, 0xff, /* USAGE_PAGE (Vendor 0xff00) */
+ 0x09, 0x0b, /* USAGE (0x0b) */
+ 0xa1, 0x01, /* COLLECTION (Application) */
+ 0x06, 0x00, 0xff, /* USAGE_PAGE (Vendor 0xff00) */
+ 0x09, 0x0b, /* USAGE (0x0b) */
+ 0x15, 0x00, /* LOGICAL_MINIMUM (0) */
+ 0x26, 0xff, 0x00, /* LOGICAL_MAXIMUM (255) */
+ 0x75, 0x08, /* REPORT_SIZE (8) */
+ 0x96, 0x04, 0x00, /* REPORT_COUNT (4) Report 0xe0 = 4 bytes */
+ 0x85, 0xe0, /* REPORT_ID (0xe0) */
+ 0x81, 0x22, /* INPUT (Data,Var,Abs,NoPref) */
+ 0x09, 0x0b, /* USAGE (0x0b) */
+ 0x96, 0x01, 0x00, /* REPORT_COUNT (1) Report 0x9a = 1 byte */
+ 0x85, 0x9a, /* REPORT_ID (0x9a) */
+ 0x81, 0x22, /* INPUT (Data,Var,Abs,NoPref) */
+ 0xc0, /* END_COLLECTION */
+
+ /* Application 2: power / battery */
+ 0x06, 0x00, 0xff, /* USAGE_PAGE (Vendor 0xff00) */
+ 0x09, 0x14, /* USAGE (0x14) */
+ 0xa1, 0x01, /* COLLECTION (Application) */
+ 0x85, 0x90, /* REPORT_ID (0x90) */
+ 0x05, 0x84, /* USAGE_PAGE (Power Device) */
+ 0x75, 0x01, /* REPORT_SIZE (1) */
+ 0x95, 0x03, /* REPORT_COUNT (3) 3 status bits */
+ 0x15, 0x00, /* LOGICAL_MINIMUM (0) */
+ 0x25, 0x01, /* LOGICAL_MAXIMUM (1) */
+ 0x09, 0x61, /* USAGE (AC mains) */
+ 0x05, 0x85, /* USAGE_PAGE (Battery System) */
+ 0x09, 0x44, /* USAGE (Charging) */
+ 0x09, 0x46, /* USAGE (NeedReplacement) */
+ 0x81, 0x02, /* INPUT (Data,Var,Abs) */
+ 0x95, 0x05, /* REPORT_COUNT (5) 5-bit padding */
+ 0x81, 0x01, /* INPUT (Const,Array,Abs) */
+ 0x75, 0x08, /* REPORT_SIZE (8) */
+ 0x95, 0x01, /* REPORT_COUNT (1) battery percent byte */
+ 0x15, 0x00, /* LOGICAL_MINIMUM (0) */
+ 0x26, 0xff, 0x00, /* LOGICAL_MAXIMUM (255) */
+ 0x09, 0x65, /* USAGE (AbsoluteStateOfCharge) */
+ 0x81, 0x02, /* INPUT (Data,Var,Abs) */
+ 0xc0, /* END_COLLECTION */
+};
+
+/*
+ * Interface 1 — standard USB HID Boot Keyboard.
+ *
+ * UsagePage 0x07 (Keyboard/Keypad), one Application collection with
+ * Report ID 0x01:
+ * 8 bits modifier (UsageMin 0xE0 / UsageMax 0xE7)
+ * 8 bits reserved (Const)
+ * 5 bits LED output (UsagePage 0x08, UsageMin 1, UsageMax 5)
+ * 3 bits LED padding (Const)
+ * 7 keycode slots (8 bits each, UsageMin 0, UsageMax 0xFF)
+ *
+ * Total input report = ReportID byte + 9 data bytes = 10 bytes.
+ * Total output report (LEDs) = ReportID byte + 1 data byte = 2 bytes.
+ * Real Magic Keyboards expose a similar boot-keyboard interface
+ * alongside the vendor (UsagePage 0xff00) interface.
+ */
+/*
+ * Boot keyboard report descriptor — byte-for-byte the real Apple
+ * Magic Keyboard with Numeric Keypad report descriptor on Interface 1
+ * (IOReg "Keyboard / Boot@1"), captured 2026-05-08 from a real
+ * keyboard plugged into a Mac running macOS 15.x. Source:
+ * paravirt-re/library/apple-magic-hid/captures/usb-magic/
+ * 04-ioreg-usbhostdevice.txt.
+ *
+ * The descriptor declares FOUR application collections:
+ * App 1, Report 0x01 — boot keyboard (mod+reserved+6 keys),
+ * Consumer eject (1 bit), Vendor 0xff00 (7 bits)
+ * App 2, Report 0x52 — consumer multimedia keys (mute, vol, etc.)
+ * App 3, Report 0x09 — generic desktop control (system on/off etc.)
+ * App 4, Report 0x3f — vendor 0xff00 64-byte feature report
+ *
+ * Our emulator only generates Report 0x01 (typing). The other three
+ * collections must be DECLARED in the descriptor for AppleUSBTopCase
+ * HIDDriver and AppleHIDKeyboardEventDriverV2 to match-dictionary
+ * accept the device — we don't have to actually emit reports for
+ * them.
+ *
+ * Using a generic single-collection boot keyboard descriptor (which
+ * we did pre-2026-05-08) makes AppleHIDKeyboardEventDriverV2 decline
+ * to bind, which leaves the boot iface IOHIDInterface unclaimed and
+ * 60s busy-times out on installed macOS.
+ */
+static const uint8_t apple_magic_kbd_boot_hid_report_descriptor[] = {
+ /* App 1: boot keyboard + Consumer eject + Vendor 0xff (Report 0x01) */
+ 0x05, 0x01, 0x09, 0x06, 0xa1, 0x01, 0x85, 0x01,
+ 0x05, 0x07, 0x19, 0xe0, 0x29, 0xe7, 0x15, 0x00,
+ 0x25, 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02,
+ 0x95, 0x01, 0x75, 0x08, 0x81, 0x01, 0x95, 0x05,
+ 0x75, 0x01, 0x05, 0x08, 0x19, 0x01, 0x29, 0x05,
+ 0x91, 0x02, 0x95, 0x01, 0x75, 0x03, 0x91, 0x01,
+ 0x95, 0x06, 0x75, 0x08, 0x15, 0x00, 0x26, 0xff,
+ 0x00, 0x05, 0x07, 0x19, 0x00, 0x29, 0xff, 0x81,
+ 0x00, 0x05, 0x0c, 0x75, 0x01, 0x95, 0x01, 0x09,
+ 0xb8, 0x15, 0x00, 0x25, 0x01, 0x81, 0x02, 0x05,
+ 0xff, 0x09, 0x03, 0x75, 0x07, 0x95, 0x01, 0x81,
+ 0x02, 0xc0,
+ /* App 2: Consumer multimedia keys (Report 0x52) +
+ * system control feature report (Report 0x09)
+ * One Application Collection containing two Report IDs. */
+ 0x05, 0x0c, 0x09, 0x01, 0xa1, 0x01,
+ 0x85, 0x52, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01,
+ 0x95, 0x01, 0x09, 0xcd, 0x81, 0x02, 0x09, 0xb3,
+ 0x81, 0x02, 0x09, 0xb4, 0x81, 0x02, 0x09, 0xb5,
+ 0x81, 0x02, 0x09, 0xb6, 0x81, 0x02, 0x81, 0x01,
+ 0x81, 0x01, 0x81, 0x01, 0x85, 0x09, 0x15, 0x00,
+ 0x25, 0x01, 0x75, 0x08, 0x95, 0x01, 0x06, 0x01,
+ 0xff, 0x09, 0x0b, 0xb1, 0x02, 0x75, 0x08, 0x95,
+ 0x02, 0xb1, 0x01, 0xc0,
+ /* App 3: Vendor 0xff00 64-byte feature blob (Report 0x3f) */
+ 0x06, 0x00, 0xff, 0x09,
+ 0x06, 0xa1, 0x01, 0x06, 0x00, 0xff, 0x09, 0x06,
+ 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95,
+ 0x40, 0x85, 0x3f, 0x81, 0x22, 0xc0,
+};
+
+static const USBDescIface desc_iface_apple_magic_kbd[] = {
+ {
+ /* Interface 0: Apple-vendor HID. */
+ .bInterfaceNumber = 0,
+ .bNumEndpoints = 1,
+ .bInterfaceClass = USB_CLASS_HID,
+ .bInterfaceSubClass = 0x00, /* NOT boot — vendor */
+ .bInterfaceProtocol = 0x00, /* NOT keyboard — vendor */
+ .iInterface = STR_AMK_INTERFACE,
+ .ndesc = 1,
+ .descs = (USBDescOther[]) {
+ {
+ /* HID descriptor */
+ .data = (uint8_t[]) {
+ 0x09, /* bLength */
+ USB_DT_HID, /* bDescriptorType */
+ 0x11, 0x01, /* bcdHID 1.11 */
+ 0x00, /* bCountryCode */
+ 0x01, /* bNumDescriptors */
+ USB_DT_REPORT, /* bDescriptorType: Report */
+ sizeof(apple_magic_kbd_hid_report_descriptor) & 0xff,
+ sizeof(apple_magic_kbd_hid_report_descriptor) >> 8,
+ },
+ },
+ },
+ .eps = (USBDescEndpoint[]) {
+ {
+ .bEndpointAddress = USB_DIR_IN | AMK_EP_VENDOR_IN,
+ .bmAttributes = USB_ENDPOINT_XFER_INT,
+ .wMaxPacketSize = 8,
+ .bInterval = 7, /* 2 ^ (8-1) * 125 us = 8 ms */
+ },
+ },
+ },
+ {
+ /* Interface 1: standard HID boot keyboard. */
+ .bInterfaceNumber = 1,
+ .bNumEndpoints = 1,
+ .bInterfaceClass = USB_CLASS_HID,
+ .bInterfaceSubClass = 0x01, /* boot */
+ .bInterfaceProtocol = 0x01, /* keyboard */
+ .iInterface = STR_AMK_INTERFACE_BOOT,
+ .ndesc = 1,
+ .descs = (USBDescOther[]) {
+ {
+ /* HID descriptor */
+ .data = (uint8_t[]) {
+ 0x09, /* bLength */
+ USB_DT_HID, /* bDescriptorType */
+ 0x11, 0x01, /* bcdHID 1.11 */
+ 0x00, /* bCountryCode */
+ 0x01, /* bNumDescriptors */
+ USB_DT_REPORT, /* bDescriptorType: Report */
+ sizeof(apple_magic_kbd_boot_hid_report_descriptor) & 0xff,
+ sizeof(apple_magic_kbd_boot_hid_report_descriptor) >> 8,
+ },
+ },
+ },
+ .eps = (USBDescEndpoint[]) {
+ {
+ .bEndpointAddress = USB_DIR_IN | AMK_EP_BOOT_IN,
+ .bmAttributes = USB_ENDPOINT_XFER_INT,
+ /*
+ * wMaxPacketSize=64 (full-speed interrupt max) — required
+ * because the report descriptor declares Report 0x3f as
+ * a 64-byte vendor input. With a smaller wMaxPacketSize
+ * the kernel computes MaxInputReportSize > wMaxPacketSize
+ * and IOHIDInterface.start() blocks waiting for a packet
+ * size that can't be delivered. We never actually emit
+ * 64-byte frames — only the 10-byte boot kbd Report 0x01.
+ */
+ .wMaxPacketSize = 64,
+ .bInterval = 8, /* 2 ^ (8-1) * 125 us = 8 ms */
+ },
+ },
+ },
+};
+
+static const USBDescDevice desc_device_apple_magic_kbd = {
+ .bcdUSB = 0x0200,
+ .bMaxPacketSize0 = 64,
+ .bNumConfigurations = 1,
+ .confs = (USBDescConfig[]) {
+ {
+ .bNumInterfaces = 2,
+ .bConfigurationValue = 1,
+ .iConfiguration = STR_AMK_PRODUCT,
+ .bmAttributes = USB_CFG_ATT_ONE | USB_CFG_ATT_WAKEUP,
+ .bMaxPower = 250, /* 500 mA — matches real */
+ .nif = ARRAY_SIZE(desc_iface_apple_magic_kbd),
+ .ifs = desc_iface_apple_magic_kbd,
+ },
+ },
+};
+
+static const USBDesc desc_apple_magic_kbd = {
+ .id = {
+ .idVendor = 0x05ac, /* Apple Inc. */
+ .idProduct = 0x026c, /* Magic Keyboard with NumPad */
+ .bcdDevice = 0x0870,
+ .iManufacturer = STR_AMK_MFR,
+ .iProduct = STR_AMK_PRODUCT,
+ .iSerialNumber = STR_AMK_SERIAL,
+ },
+ /*
+ * Real Magic Keyboard runs at USB full-speed (12 Mb/s) despite
+ * declaring bcdUSB=0x0200. Advertise the same config under both
+ * .full and .high so QEMU's USB stack can pick whichever speed
+ * the host controller (qemu-xhci) negotiates.
+ */
+ .full = &desc_device_apple_magic_kbd,
+ .high = &desc_device_apple_magic_kbd,
+ .str = desc_strings_amk,
+};
+
+/*
+ * QKeyCode → USB HID Usage Code (UsagePage 0x07).
+ *
+ * Self-contained map so the apple-magic-keyboard implementation does
+ * not have to share state with hw/input/hid.c (whose hid_usage_keys[]
+ * is static and indexed by atset1 scancode). We only need the HID
+ * Usage value here — modifier vs slot keys are distinguished by
+ * value range (0xE0..0xE7 = modifier).
+ *
+ * Entries left at 0 are unmapped and ignored.
+ */
+static const uint8_t apple_magic_kbd_qcode_to_hid_usage[Q_KEY_CODE__MAX] = {
+ /* Letters */
+ [Q_KEY_CODE_A] = 0x04, [Q_KEY_CODE_B] = 0x05, [Q_KEY_CODE_C] = 0x06,
+ [Q_KEY_CODE_D] = 0x07, [Q_KEY_CODE_E] = 0x08, [Q_KEY_CODE_F] = 0x09,
+ [Q_KEY_CODE_G] = 0x0a, [Q_KEY_CODE_H] = 0x0b, [Q_KEY_CODE_I] = 0x0c,
+ [Q_KEY_CODE_J] = 0x0d, [Q_KEY_CODE_K] = 0x0e, [Q_KEY_CODE_L] = 0x0f,
+ [Q_KEY_CODE_M] = 0x10, [Q_KEY_CODE_N] = 0x11, [Q_KEY_CODE_O] = 0x12,
+ [Q_KEY_CODE_P] = 0x13, [Q_KEY_CODE_Q] = 0x14, [Q_KEY_CODE_R] = 0x15,
+ [Q_KEY_CODE_S] = 0x16, [Q_KEY_CODE_T] = 0x17, [Q_KEY_CODE_U] = 0x18,
+ [Q_KEY_CODE_V] = 0x19, [Q_KEY_CODE_W] = 0x1a, [Q_KEY_CODE_X] = 0x1b,
+ [Q_KEY_CODE_Y] = 0x1c, [Q_KEY_CODE_Z] = 0x1d,
+ /* Top-row digits 1..0 */
+ [Q_KEY_CODE_1] = 0x1e, [Q_KEY_CODE_2] = 0x1f, [Q_KEY_CODE_3] = 0x20,
+ [Q_KEY_CODE_4] = 0x21, [Q_KEY_CODE_5] = 0x22, [Q_KEY_CODE_6] = 0x23,
+ [Q_KEY_CODE_7] = 0x24, [Q_KEY_CODE_8] = 0x25, [Q_KEY_CODE_9] = 0x26,
+ [Q_KEY_CODE_0] = 0x27,
+ /* Editing / whitespace */
+ [Q_KEY_CODE_RET] = 0x28,
+ [Q_KEY_CODE_ESC] = 0x29,
+ [Q_KEY_CODE_BACKSPACE] = 0x2a,
+ [Q_KEY_CODE_TAB] = 0x2b,
+ [Q_KEY_CODE_SPC] = 0x2c,
+ [Q_KEY_CODE_MINUS] = 0x2d,
+ [Q_KEY_CODE_EQUAL] = 0x2e,
+ [Q_KEY_CODE_BRACKET_LEFT] = 0x2f,
+ [Q_KEY_CODE_BRACKET_RIGHT] = 0x30,
+ [Q_KEY_CODE_BACKSLASH] = 0x31,
+ [Q_KEY_CODE_SEMICOLON] = 0x33,
+ [Q_KEY_CODE_APOSTROPHE] = 0x34,
+ [Q_KEY_CODE_GRAVE_ACCENT] = 0x35,
+ [Q_KEY_CODE_COMMA] = 0x36,
+ [Q_KEY_CODE_DOT] = 0x37,
+ [Q_KEY_CODE_SLASH] = 0x38,
+ [Q_KEY_CODE_CAPS_LOCK] = 0x39,
+ /* Function row F1..F12 */
+ [Q_KEY_CODE_F1] = 0x3a, [Q_KEY_CODE_F2] = 0x3b, [Q_KEY_CODE_F3] = 0x3c,
+ [Q_KEY_CODE_F4] = 0x3d, [Q_KEY_CODE_F5] = 0x3e, [Q_KEY_CODE_F6] = 0x3f,
+ [Q_KEY_CODE_F7] = 0x40, [Q_KEY_CODE_F8] = 0x41, [Q_KEY_CODE_F9] = 0x42,
+ [Q_KEY_CODE_F10] = 0x43, [Q_KEY_CODE_F11] = 0x44, [Q_KEY_CODE_F12] = 0x45,
+ /* Print / lock / pause */
+ [Q_KEY_CODE_PRINT] = 0x46,
+ [Q_KEY_CODE_SCROLL_LOCK] = 0x47,
+ [Q_KEY_CODE_PAUSE] = 0x48,
+ /* Editing block */
+ [Q_KEY_CODE_INSERT] = 0x49,
+ [Q_KEY_CODE_HOME] = 0x4a,
+ [Q_KEY_CODE_PGUP] = 0x4b,
+ [Q_KEY_CODE_DELETE] = 0x4c,
+ [Q_KEY_CODE_END] = 0x4d,
+ [Q_KEY_CODE_PGDN] = 0x4e,
+ [Q_KEY_CODE_RIGHT] = 0x4f,
+ [Q_KEY_CODE_LEFT] = 0x50,
+ [Q_KEY_CODE_DOWN] = 0x51,
+ [Q_KEY_CODE_UP] = 0x52,
+ /* Keypad */
+ [Q_KEY_CODE_NUM_LOCK] = 0x53,
+ [Q_KEY_CODE_KP_DIVIDE] = 0x54,
+ [Q_KEY_CODE_KP_MULTIPLY] = 0x55,
+ [Q_KEY_CODE_ASTERISK] = 0x55, /* duplicate name in qcode table */
+ [Q_KEY_CODE_KP_SUBTRACT] = 0x56,
+ [Q_KEY_CODE_KP_ADD] = 0x57,
+ [Q_KEY_CODE_KP_ENTER] = 0x58,
+ [Q_KEY_CODE_KP_1] = 0x59,
+ [Q_KEY_CODE_KP_2] = 0x5a,
+ [Q_KEY_CODE_KP_3] = 0x5b,
+ [Q_KEY_CODE_KP_4] = 0x5c,
+ [Q_KEY_CODE_KP_5] = 0x5d,
+ [Q_KEY_CODE_KP_6] = 0x5e,
+ [Q_KEY_CODE_KP_7] = 0x5f,
+ [Q_KEY_CODE_KP_8] = 0x60,
+ [Q_KEY_CODE_KP_9] = 0x61,
+ [Q_KEY_CODE_KP_0] = 0x62,
+ [Q_KEY_CODE_KP_DECIMAL] = 0x63,
+ [Q_KEY_CODE_LESS] = 0x64, /* non-US backslash / ISO key */
+ [Q_KEY_CODE_KP_EQUALS] = 0x67,
+ /* F13..F24 */
+ [Q_KEY_CODE_F13] = 0x68, [Q_KEY_CODE_F14] = 0x69, [Q_KEY_CODE_F15] = 0x6a,
+ [Q_KEY_CODE_F16] = 0x6b, [Q_KEY_CODE_F17] = 0x6c, [Q_KEY_CODE_F18] = 0x6d,
+ [Q_KEY_CODE_F19] = 0x6e, [Q_KEY_CODE_F20] = 0x6f, [Q_KEY_CODE_F21] = 0x70,
+ [Q_KEY_CODE_F22] = 0x71, [Q_KEY_CODE_F23] = 0x72, [Q_KEY_CODE_F24] = 0x73,
+ /* Misc named keys */
+ [Q_KEY_CODE_HELP] = 0x75,
+ [Q_KEY_CODE_MENU] = 0x76,
+ [Q_KEY_CODE_STOP] = 0x78,
+ [Q_KEY_CODE_AGAIN] = 0x79,
+ [Q_KEY_CODE_UNDO] = 0x7a,
+ [Q_KEY_CODE_CUT] = 0x7b,
+ [Q_KEY_CODE_COPY] = 0x7c,
+ [Q_KEY_CODE_PASTE] = 0x7d,
+ [Q_KEY_CODE_FIND] = 0x7e,
+ [Q_KEY_CODE_AUDIOMUTE] = 0x7f,
+ [Q_KEY_CODE_VOLUMEUP] = 0x80,
+ [Q_KEY_CODE_VOLUMEDOWN] = 0x81,
+ [Q_KEY_CODE_KP_COMMA] = 0x85,
+ [Q_KEY_CODE_RO] = 0x87, /* Intl1 (Japanese RO) */
+ [Q_KEY_CODE_KATAKANAHIRAGANA]= 0x88, /* Intl2 */
+ [Q_KEY_CODE_YEN] = 0x89, /* Intl3 */
+ [Q_KEY_CODE_HENKAN] = 0x8a, /* Intl4 */
+ [Q_KEY_CODE_MUHENKAN] = 0x8b, /* Intl5 */
+ [Q_KEY_CODE_HIRAGANA] = 0x91, /* LANG4 (close enough) */
+ [Q_KEY_CODE_LANG1] = 0x90,
+ [Q_KEY_CODE_LANG2] = 0x91,
+ /* Modifiers — HID Usages 0xE0..0xE7 (also written into modifier byte). */
+ [Q_KEY_CODE_CTRL] = 0xe0,
+ [Q_KEY_CODE_SHIFT] = 0xe1,
+ [Q_KEY_CODE_ALT] = 0xe2,
+ [Q_KEY_CODE_META_L] = 0xe3,
+ [Q_KEY_CODE_CTRL_R] = 0xe4,
+ [Q_KEY_CODE_SHIFT_R] = 0xe5,
+ [Q_KEY_CODE_ALT_R] = 0xe6,
+ [Q_KEY_CODE_META_R] = 0xe7,
+};
+
+/*
+ * Pack the live boot-keyboard state into a 10-byte report payload —
+ * matches real Apple Magic Keyboard Report 0x01 layout (boot keyboard
+ * + Consumer Eject + Vendor 0xff Usage 0x03):
+ * buf[0] = report ID (0x01)
+ * buf[1] = modifier byte (HID Usages 0xE0..0xE7)
+ * buf[2] = reserved (0)
+ * buf[3..8] = 6 keycode slots (HID Usage codes)
+ * buf[9] = bit0 Consumer Eject + bits1..7 Vendor 0xff Usage 0x03
+ * (always 0 — emulator does not generate eject or vendor)
+ */
+static void apple_magic_kbd_pack_boot_report(USBAppleMagicKbdState *s,
+ uint8_t buf[AMK_BOOT_REPORT_LEN])
+{
+ buf[0] = AMK_BOOT_REPORT_ID;
+ buf[1] = s->boot_modifiers;
+ buf[2] = 0;
+ memcpy(&buf[3], s->boot_keys, AMK_BOOT_NUM_KEYS); /* 6 keycodes */
+ buf[9] = 0; /* eject + vendor */
+}
+
+/* Update s->boot_modifiers / s->boot_keys for one HID Usage, then mark
+ * the report dirty. Returns true if state actually changed. */
+static bool apple_magic_kbd_apply_usage(USBAppleMagicKbdState *s,
+ uint8_t usage, bool down)
+{
+ int i;
+
+ if (usage == 0) {
+ return false;
+ }
+
+ /* Modifiers — packed bitmap into the modifier byte. */
+ if (usage >= 0xe0 && usage <= 0xe7) {
+ uint8_t bit = 1u << (usage - 0xe0);
+ uint8_t prev = s->boot_modifiers;
+ if (down) {
+ s->boot_modifiers |= bit;
+ } else {
+ s->boot_modifiers &= ~bit;
+ }
+ return s->boot_modifiers != prev;
+ }
+
+ /* Slot keys — 7-slot array, no duplicates. */
+ if (down) {
+ for (i = 0; i < AMK_BOOT_NUM_KEYS; i++) {
+ if (s->boot_keys[i] == usage) {
+ return false; /* already pressed */
+ }
+ }
+ for (i = 0; i < AMK_BOOT_NUM_KEYS; i++) {
+ if (s->boot_keys[i] == 0) {
+ s->boot_keys[i] = usage;
+ return true;
+ }
+ }
+ /* Roll-over — slots full. Per HID spec, every slot should be
+ * 0x01 (ErrorRollOver). For simplicity we just drop; VNC /
+ * single-user input isn't going to produce 8+ chord keys in
+ * normal use. */
+ return false;
+ } else {
+ for (i = 0; i < AMK_BOOT_NUM_KEYS; i++) {
+ if (s->boot_keys[i] == usage) {
+ /* Compact slots so packed array stays contiguous. */
+ int j;
+ for (j = i; j < AMK_BOOT_NUM_KEYS - 1; j++) {
+ s->boot_keys[j] = s->boot_keys[j + 1];
+ }
+ s->boot_keys[AMK_BOOT_NUM_KEYS - 1] = 0;
+ return true;
+ }
+ }
+ return false;
+ }
+}
+
+static void apple_magic_kbd_input_event(DeviceState *dev, QemuConsole *src,
+ InputEvent *evt)
+{
+ USBAppleMagicKbdState *s = USB_APPLE_MAGIC_KBD(dev);
+ InputKeyEvent *key;
+ int qcode;
+ uint8_t usage;
+
+ if (evt->type != INPUT_EVENT_KIND_KEY) {
+ return;
+ }
+
+ key = evt->u.key.data;
+ qcode = qemu_input_key_value_to_qcode(key->key);
+ if (qcode < 0 || qcode >= Q_KEY_CODE__MAX) {
+ return;
+ }
+ usage = apple_magic_kbd_qcode_to_hid_usage[qcode];
+ if (usage == 0) {
+ return;
+ }
+
+ if (apple_magic_kbd_apply_usage(s, usage, key->down)) {
+ s->boot_changed = true;
+ if (s->boot_intr) {
+ usb_wakeup(s->boot_intr, 0);
+ }
+ }
+}
+
+static const QemuInputHandler apple_magic_kbd_input_handler = {
+ .name = "Apple Magic Keyboard (boot)",
+ .mask = INPUT_EVENT_MASK_KEY,
+ .event = apple_magic_kbd_input_event,
+};
+
+static void usb_apple_magic_kbd_realize(USBDevice *dev, Error **errp)
+{
+ USBAppleMagicKbdState *s = USB_APPLE_MAGIC_KBD(dev);
+
+ /*
+ * uc->usb_desc set in class_init handles dev->usb_desc selection.
+ * Mirror Wacom's pattern: just set up the serial + descriptors.
+ */
+ usb_desc_create_serial(dev);
+ usb_desc_init(dev);
+
+ s->boot_intr = usb_ep_get(dev, USB_TOKEN_IN, AMK_EP_BOOT_IN);
+ s->input_handler = qemu_input_handler_register(DEVICE(s),
+ &apple_magic_kbd_input_handler);
+ qemu_input_handler_activate(s->input_handler);
+}
+
+static const VMStateDescription vmstate_apple_magic_kbd = {
+ .name = "apple-magic-keyboard",
+ .unmigratable = 1,
+};
+
+static void usb_apple_magic_kbd_handle_reset(USBDevice *dev)
+{
+ USBAppleMagicKbdState *s = USB_APPLE_MAGIC_KBD(dev);
+
+ s->boot_modifiers = 0;
+ memset(s->boot_keys, 0, sizeof(s->boot_keys));
+ s->boot_changed = false;
+}
+
+static void usb_apple_magic_kbd_handle_control(USBDevice *dev, USBPacket *p,
+ int request, int value,
+ int index, int length,
+ uint8_t *data)
+{
+ USBAppleMagicKbdState *s = USB_APPLE_MAGIC_KBD(dev);
+ int ret;
+
+ ret = usb_desc_handle_control(dev, p, request, value, index, length, data);
+ if (ret >= 0) {
+ return;
+ }
+
+ switch (request) {
+ case InterfaceRequest | USB_REQ_GET_DESCRIPTOR:
+ if ((value >> 8) == 0x22) {
+ /*
+ * GET_DESCRIPTOR(REPORT). Pick the right report descriptor
+ * based on the interface index in wIndex. Interface 0 =
+ * Apple vendor HID; Interface 1 = boot keyboard.
+ */
+ const uint8_t *rd;
+ uint16_t rd_len;
+ uint16_t copy;
+ if (index == 1) {
+ rd = apple_magic_kbd_boot_hid_report_descriptor;
+ rd_len = sizeof(apple_magic_kbd_boot_hid_report_descriptor);
+ } else {
+ rd = apple_magic_kbd_hid_report_descriptor;
+ rd_len = sizeof(apple_magic_kbd_hid_report_descriptor);
+ }
+ copy = length < rd_len ? length : rd_len;
+ memcpy(data, rd, copy);
+ p->actual_length = copy;
+ return;
+ }
+ break;
+ case HID_GET_IDLE:
+ data[0] = 0;
+ p->actual_length = 1;
+ return;
+ case HID_SET_IDLE:
+ return;
+ case HID_GET_PROTOCOL:
+ data[0] = 1; /* report protocol */
+ p->actual_length = 1;
+ return;
+ case HID_SET_PROTOCOL:
+ return;
+ case HID_GET_REPORT: {
+ /*
+ * GET_REPORT — feature/input poll over EP0. Behaviour depends
+ * on the interface index in wIndex.
+ *
+ * Interface 0 (vendor): blanket-ACK with zero-filled payload of
+ * the declared report size. Stalling these would send
+ * AppleUSBTopCaseHIDDriver into a tight retry loop on
+ * match-probe. Match the per-Report-ID sizes declared in the
+ * vendor HID Report Descriptor:
+ * 0xe0 → 4 bytes (input only, but driver may probe Feature)
+ * 0x9a → 1 byte
+ * 0x90 → 2 bytes (AC/charge bits + battery byte)
+ * Default: zeros of the requested 'length' bytes.
+ *
+ * Interface 1 (boot keyboard): synthesize an input report from
+ * current boot state if the host requests Report ID 0x01.
+ */
+ uint8_t report_id = value & 0xff;
+ uint8_t report_type = (value >> 8) & 0xff;
+ uint16_t reply_len = 0;
+
+ if (index == 1) {
+ uint8_t buf[AMK_BOOT_REPORT_LEN];
+ if (report_type == 0x01 /* Input */ &&
+ report_id == AMK_BOOT_REPORT_ID) {
+ apple_magic_kbd_pack_boot_report(s, buf);
+ reply_len = AMK_BOOT_REPORT_LEN;
+ if (reply_len > length) {
+ reply_len = length;
+ }
+ memcpy(data, buf, reply_len);
+ p->actual_length = reply_len;
+ return;
+ }
+ /*
+ * Unknown report type/ID on the boot iface — STALL.
+ * macOS speculatively probes Feature reads for IDs not in the
+ * descriptor (e.g. 0x02, 0x03); a real device responds with
+ * a STALL there and macOS moves on. Returning zero-fill
+ * causes the host's HID parser to treat the response as a
+ * valid-but-malformed report and stall the IOHIDInterface
+ * during ::start (the (a,4020001) busy timeout).
+ */
+ break;
+ }
+
+ /* Vendor iface: same logic — only declared report IDs answer. */
+ switch (report_id) {
+ case 0xe0: reply_len = 4; break;
+ case 0x9a: reply_len = 1; break;
+ case 0x90: reply_len = 2; break;
+ default:
+ /* Unknown vendor report ID — STALL (real device behaviour). */
+ break;
+ }
+ if (reply_len == 0) {
+ break; /* falls through to STALL */
+ }
+ if (reply_len > length) {
+ reply_len = length;
+ }
+ memset(data, 0, reply_len);
+ p->actual_length = reply_len;
+ return;
+ }
+ case HID_SET_REPORT:
+ /*
+ * Interface 0 (vendor): silently accept SET_REPORT writes.
+ * The vendor multitouch-enable SET_REPORT (0x02, 0xF1, per
+ * Linux's magicmouse_enable_multitouch) is acknowledged but
+ * not yet acted on; the device stays on the boot face.
+ *
+ * Interface 1 (boot keyboard): ACK SET_REPORT (LED state).
+ * We don't drive any host-visible LEDs yet but must not stall.
+ *
+ * MUST set actual_length: the USB layer reports back the
+ * number of bytes accepted, which the host uses to confirm
+ * the write succeeded. Without it the host reads "0 bytes
+ * accepted" and retries. Trace 2026-05-08 showed macOS
+ * sending the same LED SET_REPORT 5 times back-to-back —
+ * exactly that retry pattern.
+ */
+ p->actual_length = length;
+ return;
+ }
+
+ p->status = USB_RET_STALL;
+}
+
+static void usb_apple_magic_kbd_handle_data(USBDevice *dev, USBPacket *p)
+{
+ USBAppleMagicKbdState *s = USB_APPLE_MAGIC_KBD(dev);
+
+ if (p->pid != USB_TOKEN_IN) {
+ p->status = USB_RET_STALL;
+ return;
+ }
+
+ switch (p->ep->nr) {
+ case AMK_EP_VENDOR_IN:
+ /*
+ * Vendor IN endpoint carries the 1 Hz 0x90 heartbeat queued
+ * from the heartbeat timer. NAK when nothing is pending so
+ * the host keeps polling without erroring; the typing pipe
+ * is on Interface 1's boot-keyboard endpoint.
+ */
+ p->status = USB_RET_NAK;
+ return;
+ case AMK_EP_BOOT_IN: {
+ uint8_t buf[AMK_BOOT_REPORT_LEN];
+ size_t copy;
+
+ if (!s->boot_changed) {
+ p->status = USB_RET_NAK;
+ return;
+ }
+ s->boot_changed = false;
+ apple_magic_kbd_pack_boot_report(s, buf);
+ copy = p->iov.size < AMK_BOOT_REPORT_LEN
+ ? p->iov.size : AMK_BOOT_REPORT_LEN;
+ usb_packet_copy(p, buf, copy);
+ return;
+ }
+ default:
+ p->status = USB_RET_STALL;
+ return;
+ }
+}
+
+static void usb_apple_magic_kbd_unrealize(USBDevice *dev)
+{
+ USBAppleMagicKbdState *s = USB_APPLE_MAGIC_KBD(dev);
+
+ if (s->input_handler) {
+ qemu_input_handler_unregister(s->input_handler);
+ s->input_handler = NULL;
+ }
+}
+
+static void usb_apple_magic_kbd_class_initfn(ObjectClass *klass,
+ const void *data)
+{
+ DeviceClass *dc = DEVICE_CLASS(klass);
+ USBDeviceClass *uc = USB_DEVICE_CLASS(klass);
+
+ uc->realize = usb_apple_magic_kbd_realize;
+ uc->product_desc = "Magic Keyboard with Numeric Keypad";
+ uc->usb_desc = &desc_apple_magic_kbd;
+ uc->handle_reset = usb_apple_magic_kbd_handle_reset;
+ uc->handle_control = usb_apple_magic_kbd_handle_control;
+ uc->handle_data = usb_apple_magic_kbd_handle_data;
+ uc->unrealize = usb_apple_magic_kbd_unrealize;
+ set_bit(DEVICE_CATEGORY_INPUT, dc->categories);
+ dc->desc = "Apple Magic Keyboard with Numeric Keypad "
+ "(USB-mode emulator, vendor HID protocol)";
+ dc->vmsd = &vmstate_apple_magic_kbd;
+}
+
+static const TypeInfo usb_apple_magic_kbd_info = {
+ .name = TYPE_USB_APPLE_MAGIC_KBD,
+ .parent = TYPE_USB_DEVICE,
+ .instance_size = sizeof(USBAppleMagicKbdState),
+ .class_init = usb_apple_magic_kbd_class_initfn,
+};
+
static void usb_hid_register_types(void)
{
type_register_static(&usb_hid_type_info);
@@ -871,6 +1708,7 @@ static void usb_hid_register_types(void)
usb_legacy_register("usb-mouse", "mouse", NULL);
type_register_static(&usb_keyboard_info);
usb_legacy_register("usb-kbd", "keyboard", NULL);
+ type_register_static(&usb_apple_magic_kbd_info);
}
type_init(usb_hid_register_types)
--
2.50.1 (Apple Git-155)
^ permalink raw reply related [flat|nested] 6+ messages in thread* [PATCH 2/2] hw/usb/dev-hid: add apple-mighty-mouse
2026-07-19 15:23 [PATCH 0/2] hw/usb/dev-hid: Apple Magic Keyboard and Mighty Mouse emulators Matthew Jackson
2026-07-19 15:23 ` [PATCH 1/2] hw/usb/dev-hid: add apple-magic-keyboard Matthew Jackson
@ 2026-07-19 15:23 ` Matthew Jackson
2026-07-20 8:22 ` Peter Maydell
1 sibling, 1 reply; 6+ messages in thread
From: Matthew Jackson @ 2026-07-19 15:23 UTC (permalink / raw)
To: qemu-devel; +Cc: kraxel
Adds a self-contained USB-HID device emulating the USB-cable face of
the Apple Mighty Mouse (idVendor 0x05ac, idProduct 0x0304, the wired
ball mouse M9087 released 2005). Descriptors and HID report descriptor
are byte-identical to a real Mighty Mouse captured in USB-cable mode.
Companion to apple-magic-keyboard (previous patch). macOS recovery /
install / multi-touch UI panels behave correctly only when the
pointing device matches an Apple-VID peripheral; usb-tablet binds the
generic IOUSBHostHIDDevice driver and breaks recovery's HID stack on
iMac20,1 SMBIOS guests.
Single HID interface, single IN endpoint, standard boot mouse + scroll
HID Report Descriptor:
byte 0: 3-bit button mask (left, right, middle) + 5-bit padding
byte 1: signed int8 dX per-frame pointer delta
byte 2: signed int8 dY per-frame pointer delta
byte 3: signed int8 vWheel Generic Desktop / Wheel
byte 4: signed int8 hWheel Consumer / AC Pan (scroll-ball X)
bSubClass=1, bProto=2 (boot mouse) — guaranteed-binding driver match
across every macOS version. Binds AppleHIDMouseEventDriver /
IOHIDPointing chain natively; no proprietary multitouch protocol
required.
VNC sources deliver absolute pointer events; the device's input
handler converts them to per-frame REL deltas via a last_abs_x/y
tracker since the boot-mouse report only carries int8 dX/dY. A bounded
queue (depth 64) drains accumulated motion across multiple reports per
input sync so cursor movement stays responsive under large pointer
deltas.
A legacy TypeInfo alias `apple-magic-tablet` is registered to the same
implementation so existing in-tree consumers (e.g. mos-docker test
scripts using TABLET_DEVICE=apple-magic-tablet) keep working during
the canonical-name transition. Drop the alias when in-tree consumers
move to apple-mighty-mouse.
Verified end-to-end on macOS 15.7.5: AppleHIDMouseEventDriver binds on
boot; cursor motion, left/right/middle click, vertical scroll, and
horizontal scroll all visible in noVNC.
Signed-off-by: Matthew Jackson <matthew@pq.io>
---
hw/usb/dev-hid.c | 573 +++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 573 insertions(+)
diff --git a/hw/usb/dev-hid.c b/hw/usb/dev-hid.c
index 7cce69b..9a4aa75 100644
--- a/hw/usb/dev-hid.c
+++ b/hw/usb/dev-hid.c
@@ -1699,6 +1699,577 @@ static const TypeInfo usb_apple_magic_kbd_info = {
.class_init = usb_apple_magic_kbd_class_initfn,
};
+
+/*
+ * apple-mighty-mouse (USB type name; legacy alias `apple-magic-tablet`)
+ * --------------------------------------------------------------------
+ *
+ * Internal symbols carry the historic `amt` / `apple_magic_tablet` /
+ * `USBAppleMagicTabletState` prefixes — kept to minimize diff churn
+ * since this file's user-facing identity flipped during the 2026-05-09
+ * fresh-eyes rewrite (see header below). Only the `-device` string and
+ * vmstate name are user-visible and were renamed.
+ */
+
+enum {
+ STR_AMT_MFR = 1,
+ STR_AMT_PRODUCT,
+ STR_AMT_SERIAL,
+ STR_AMT_INTERFACE,
+};
+
+static const USBDescStrings desc_strings_amt = {
+ [STR_AMT_MFR] = "Apple Inc.",
+ [STR_AMT_PRODUCT] = "Apple Mighty Mouse",
+ [STR_AMT_SERIAL] = "CC2916600VBJ2XQA5",
+ [STR_AMT_INTERFACE] = "Mouse",
+};
+
+/*
+ * apple-mighty-mouse device — FRESH-EYES REWRITE 2026-05-09.
+ *
+ * Goal: macOS sees a USB HID mouse from Apple that "just works" for cursor
+ * motion. Not Magic Trackpad multitouch — that path needs deep RE we don't
+ * have ground-truth wire bytes for. Instead, present as **Apple Mighty
+ * Mouse (PID 0x0304)** — Apple's wired ball mouse. macOS' generic
+ * AppleHIDMouseEventDriver / IOHIDPointing chain handles standard boot
+ * mouse reports natively; cursor motion works without any vendor-specific
+ * SET_REPORT / multitouch protocol.
+ *
+ * Why this works where Magic Trackpad emulation didn't:
+ * - Mighty Mouse PID 0x0304 does NOT match AppleMultitouchTrackpadHID
+ * EventDriver (which is the driver gating cursor on PID 0x0265).
+ * - Mighty Mouse uses standard USB HID boot mouse protocol (3-byte
+ * reports: button + dx + dy) — no proprietary 1387-byte Report 0x44.
+ * - Single USB interface — no SET_REPORT-gated multitouch enable path.
+ * - bSubClass=1 bProto=2 (boot mouse) — guaranteed-binding driver match
+ * across every macOS version.
+ *
+ * Single interface, single IN endpoint, simple 3-byte boot-mouse reports.
+ */
+
+/* Boot-mouse + scroll HID Report Descriptor — 5-byte report (no Report ID).
+ * byte 0: 3-bit button mask + 5-bit padding
+ * byte 1: signed int8 dX
+ * byte 2: signed int8 dY
+ * byte 3: signed int8 vertical wheel
+ * byte 4: signed int8 horizontal wheel (Mighty Mouse scroll ball X)
+ * macOS' generic mouse driver parses this without quirks; this is the
+ * standard layout for any USB HID mouse with a scroll wheel/ball. */
+static const uint8_t amt_boot_mouse_report_desc[] = {
+ 0x05, 0x01, /* Usage Page (Generic Desktop) */
+ 0x09, 0x02, /* Usage (Mouse) */
+ 0xa1, 0x01, /* Collection (Application) */
+ 0x09, 0x01, /* Usage (Pointer) */
+ 0xa1, 0x00, /* Collection (Physical) */
+ 0x05, 0x09, /* Usage Page (Buttons) */
+ 0x19, 0x01, /* Usage Minimum (1) */
+ 0x29, 0x03, /* Usage Maximum (3) */
+ 0x15, 0x00, /* Logical Min (0) */
+ 0x25, 0x01, /* Logical Max (1) */
+ 0x95, 0x03, /* Report Count (3) */
+ 0x75, 0x01, /* Report Size (1) */
+ 0x81, 0x02, /* Input (Data, Var, Abs) — 3 button bits */
+ 0x95, 0x01, /* Report Count (1) */
+ 0x75, 0x05, /* Report Size (5) */
+ 0x81, 0x03, /* Input (Const, Var, Abs) — 5 padding bits */
+ 0x05, 0x01, /* Usage Page (Generic Desktop) */
+ 0x09, 0x30, /* Usage (X) */
+ 0x09, 0x31, /* Usage (Y) */
+ 0x09, 0x38, /* Usage (Wheel — vertical) */
+ 0x15, 0x81, /* Logical Min (-127) */
+ 0x25, 0x7f, /* Logical Max (127) */
+ 0x75, 0x08, /* Report Size (8) */
+ 0x95, 0x03, /* Report Count (3) */
+ 0x81, 0x06, /* Input (Data, Var, Rel) — X, Y, Wheel */
+ 0x05, 0x0c, /* Usage Page (Consumer) */
+ 0x0a, 0x38, 0x02, /* Usage (AC Pan — horizontal scroll) */
+ 0x15, 0x81, /* Logical Min (-127) */
+ 0x25, 0x7f, /* Logical Max (127) */
+ 0x75, 0x08, /* Report Size (8) */
+ 0x95, 0x01, /* Report Count (1) */
+ 0x81, 0x06, /* Input (Data, Var, Rel) — HWheel */
+ 0xc0, /* End Collection (Physical) */
+ 0xc0, /* End Collection (Application) */
+};
+
+static const USBDescIface desc_iface_apple_magic_tablet = {
+ .bInterfaceNumber = 0,
+ .bNumEndpoints = 1,
+ .bInterfaceClass = USB_CLASS_HID,
+ .bInterfaceSubClass = 0x01, /* Boot subclass */
+ .bInterfaceProtocol = 0x02, /* Mouse protocol */
+ .iInterface = STR_AMT_INTERFACE,
+ .ndesc = 1,
+ .descs = (USBDescOther[]) {
+ {
+ .data = (uint8_t[]) {
+ 0x09, /* bLength */
+ USB_DT_HID, /* bDescriptorType */
+ 0x10, 0x01, /* bcdHID 1.10 */
+ 0x00, /* bCountryCode */
+ 0x01, /* bNumDescriptors */
+ USB_DT_REPORT,
+ sizeof(amt_boot_mouse_report_desc) & 0xff,
+ sizeof(amt_boot_mouse_report_desc) >> 8,
+ },
+ },
+ },
+ .eps = (USBDescEndpoint[]) {
+ {
+ .bEndpointAddress = USB_DIR_IN | 0x01,
+ .bmAttributes = USB_ENDPOINT_XFER_INT,
+ .wMaxPacketSize = 8,
+ .bInterval = 8,
+ },
+ },
+};
+
+static const USBDescDevice desc_device_apple_magic_tablet = {
+ .bcdUSB = 0x0200,
+ .bMaxPacketSize0 = 64,
+ .bNumConfigurations = 1,
+ .confs = (USBDescConfig[]) {
+ {
+ .bNumInterfaces = 1,
+ .bConfigurationValue = 1,
+ .iConfiguration = STR_AMT_PRODUCT,
+ .bmAttributes = USB_CFG_ATT_ONE,
+ .bMaxPower = 50, /* 100 mA — Mighty Mouse is wired */
+ .nif = 1,
+ .ifs = &desc_iface_apple_magic_tablet,
+ },
+ },
+};
+
+static const USBDesc desc_apple_magic_tablet = {
+ .id = {
+ .idVendor = 0x05ac, /* Apple Inc. */
+ .idProduct = 0x0304, /* Apple Mighty Mouse (M9087) */
+ .bcdDevice = 0x0150, /* fw rev 1.5 — looks plausible */
+ .iManufacturer = STR_AMT_MFR,
+ .iProduct = STR_AMT_PRODUCT,
+ .iSerialNumber = STR_AMT_SERIAL,
+ },
+ /*
+ * Real Magic Trackpad runs at USB full-speed (12 Mb/s) despite
+ * declaring bcdUSB=0x0200 — same gotcha as Magic Keyboard.
+ * Advertise the same config under both .full and .high so QEMU's
+ * USB stack can pick whichever speed the host controller
+ * (qemu-xhci) negotiates.
+ */
+ .full = &desc_device_apple_magic_tablet,
+ .high = &desc_device_apple_magic_tablet,
+ .str = desc_strings_amt,
+};
+
+/* Maximum queued boot-mouse reports waiting for the host to poll EP1.
+ * Must be a power of two. With dx/dy clamped to int8, a single large
+ * VNC pointer move (e.g. 2000 px) drains across ~16 reports — bumping
+ * to 64 keeps headroom for fast motion. */
+#define AMT_QUEUE_DEPTH 64
+#define AMT_REPORT_LEN 5 /* button + dx + dy + wheel + hwheel */
+
+typedef struct USBAppleMagicTabletReport {
+ uint8_t data[AMT_REPORT_LEN];
+} USBAppleMagicTabletReport;
+
+typedef struct USBAppleMagicTabletState {
+ USBDevice dev;
+ USBEndpoint *intr;
+
+ /* Pending input accumulation (drained on .sync). */
+ int32_t pending_dx;
+ int32_t pending_dy;
+ int32_t pending_wheel; /* vertical scroll, REL clicks */
+ int32_t pending_hwheel; /* horizontal scroll, REL clicks */
+ bool button_left;
+ bool button_right;
+ bool button_middle;
+ bool pending_event;
+
+ /* ABS→REL conversion state (VNC/SPICE/SDL deliver ABS). -1 = none yet. */
+ int32_t last_abs_x;
+ int32_t last_abs_y;
+
+ /* Ring queue of pending boot-mouse reports. */
+ USBAppleMagicTabletReport queue[AMT_QUEUE_DEPTH];
+ unsigned q_head;
+ unsigned q_tail;
+
+ /* QEMU input handler binding. */
+ QemuInputHandlerState *input_handler;
+} USBAppleMagicTabletState;
+
+/*
+ * Canonical type name is `apple-mighty-mouse` (this device emulates Apple
+ * Mighty Mouse PID 0x0304 — the wired ball mouse). Historic name
+ * `apple-magic-tablet` predates the 2026-05-09 fresh-eyes rewrite and is
+ * preserved as a legacy alias (registered below) for in-tree mos-docker
+ * `test.sh` back-compat. Internal symbols (`USBAppleMagicTabletState`,
+ * `usb_apple_magic_tablet_*`, `USB_APPLE_MAGIC_TABLET()` cast) keep their
+ * historic names to minimize diff churn — the rename is purely the
+ * user-facing `-device` string.
+ */
+#define TYPE_USB_APPLE_MAGIC_TABLET "apple-mighty-mouse"
+#define TYPE_USB_APPLE_MAGIC_TABLET_LEGACY "apple-magic-tablet"
+OBJECT_DECLARE_SIMPLE_TYPE(USBAppleMagicTabletState, USB_APPLE_MAGIC_TABLET)
+
+static inline unsigned amt_q_count(USBAppleMagicTabletState *s)
+{
+ return (s->q_head - s->q_tail) & (AMT_QUEUE_DEPTH - 1);
+}
+
+static inline bool amt_q_empty(USBAppleMagicTabletState *s)
+{
+ return s->q_head == s->q_tail;
+}
+
+static inline bool amt_q_full(USBAppleMagicTabletState *s)
+{
+ return amt_q_count(s) == AMT_QUEUE_DEPTH - 1;
+}
+
+static void amt_enqueue(USBAppleMagicTabletState *s, const uint8_t *data)
+{
+ if (amt_q_full(s)) {
+ /* Queue full — drop oldest. Latest motion matters more than stale. */
+ s->q_tail = (s->q_tail + 1) & (AMT_QUEUE_DEPTH - 1);
+ }
+ USBAppleMagicTabletReport *r = &s->queue[s->q_head];
+ memcpy(r->data, data, AMT_REPORT_LEN);
+ s->q_head = (s->q_head + 1) & (AMT_QUEUE_DEPTH - 1);
+}
+
+static int8_t amt_clamp_i8(int32_t v)
+{
+ if (v > 127) return 127;
+ if (v < -127) return -127;
+ return (int8_t)v;
+}
+
+/*
+ * Boot-mouse 3-byte report (no Report ID, per HID 1.11 boot mouse spec):
+ * byte 0: 3-bit button mask (bit 0 = left, 1 = right, 2 = middle) + padding
+ * byte 1: signed int8 dX
+ * byte 2: signed int8 dY
+ *
+ * macOS' generic IOHIDPointing / AppleHIDMouseEventDriver consumes this
+ * directly as cursor motion. Apple Mighty Mouse (PID 0x0304) uses exactly
+ * this layout for its primary HID interface.
+ */
+static void amt_emit_boot_mouse(USBAppleMagicTabletState *s)
+{
+ uint8_t buf[AMT_REPORT_LEN];
+ int8_t dx = amt_clamp_i8(s->pending_dx);
+ int8_t dy = amt_clamp_i8(s->pending_dy);
+ int8_t wheel = amt_clamp_i8(s->pending_wheel);
+ int8_t hwheel = amt_clamp_i8(s->pending_hwheel);
+
+ s->pending_dx -= dx;
+ s->pending_dy -= dy;
+ s->pending_wheel -= wheel;
+ s->pending_hwheel -= hwheel;
+
+ buf[0] = (s->button_left ? 0x01 : 0x00) |
+ (s->button_right ? 0x02 : 0x00) |
+ (s->button_middle ? 0x04 : 0x00);
+ buf[1] = (uint8_t)dx;
+ buf[2] = (uint8_t)dy;
+ buf[3] = (uint8_t)wheel;
+ buf[4] = (uint8_t)hwheel;
+
+ amt_enqueue(s, buf);
+ usb_wakeup(s->intr, 0);
+}
+
+/* QemuInputHandler.event — accumulate per-event state. */
+static void amt_input_event(DeviceState *dev, QemuConsole *src,
+ InputEvent *evt)
+{
+ USBAppleMagicTabletState *s = USB_APPLE_MAGIC_TABLET(dev);
+
+ switch (evt->type) {
+ case INPUT_EVENT_KIND_REL: {
+ InputMoveEvent *move = evt->u.rel.data;
+ if (move->axis == INPUT_AXIS_X) {
+ s->pending_dx += move->value;
+ } else if (move->axis == INPUT_AXIS_Y) {
+ s->pending_dy += move->value;
+ }
+ s->pending_event = true;
+ break;
+ }
+ case INPUT_EVENT_KIND_ABS: {
+ /* VNC/SPICE/SDL deliver ABS (0..0x7fff). Convert to REL by diffing
+ * against the prior ABS position; first ABS event seeds the anchor
+ * but emits no delta. Scale 0..32767 → 0..1920/1080 px. */
+ InputMoveEvent *move = evt->u.abs.data;
+ if (move->axis == INPUT_AXIS_X) {
+ int32_t scaled = (move->value * 1920) / 0x7fff;
+ if (s->last_abs_x >= 0) {
+ s->pending_dx += scaled - s->last_abs_x;
+ }
+ s->last_abs_x = scaled;
+ } else if (move->axis == INPUT_AXIS_Y) {
+ int32_t scaled = (move->value * 1080) / 0x7fff;
+ if (s->last_abs_y >= 0) {
+ s->pending_dy += scaled - s->last_abs_y;
+ }
+ s->last_abs_y = scaled;
+ }
+ s->pending_event = true;
+ break;
+ }
+ case INPUT_EVENT_KIND_BTN: {
+ InputBtnEvent *btn = evt->u.btn.data;
+ switch (btn->button) {
+ case INPUT_BUTTON_LEFT:
+ s->button_left = btn->down;
+ s->pending_event = true;
+ break;
+ case INPUT_BUTTON_RIGHT:
+ s->button_right = btn->down;
+ s->pending_event = true;
+ break;
+ case INPUT_BUTTON_MIDDLE:
+ s->button_middle = btn->down;
+ s->pending_event = true;
+ break;
+ case INPUT_BUTTON_WHEEL_UP:
+ if (btn->down) {
+ s->pending_wheel += 1;
+ s->pending_event = true;
+ }
+ break;
+ case INPUT_BUTTON_WHEEL_DOWN:
+ if (btn->down) {
+ s->pending_wheel -= 1;
+ s->pending_event = true;
+ }
+ break;
+ case INPUT_BUTTON_WHEEL_LEFT:
+ if (btn->down) {
+ s->pending_hwheel -= 1;
+ s->pending_event = true;
+ }
+ break;
+ case INPUT_BUTTON_WHEEL_RIGHT:
+ if (btn->down) {
+ s->pending_hwheel += 1;
+ s->pending_event = true;
+ }
+ break;
+ default:
+ break;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+/* QemuInputHandler.sync — drain pending_dx/dy in 3-byte boot-mouse reports.
+ *
+ * Each report's dx/dy is clamped to int8 (±127). Large motions (e.g.,
+ * a single VNC ABS event mapping to a 1000+ px delta after diffing
+ * against the prior absolute position) need multiple reports to drain.
+ * Loop until both axes are zero, queueing a report per iteration.
+ *
+ * Cap the loop at a reasonable max so a runaway delta can't lock us up;
+ * the queue depth + bInterval naturally back-pressure if the host falls
+ * behind. */
+static void amt_input_sync(DeviceState *dev)
+{
+ USBAppleMagicTabletState *s = USB_APPLE_MAGIC_TABLET(dev);
+
+ if (!s->pending_event) {
+ return;
+ }
+ s->pending_event = false;
+
+ /* Always emit at least once (catches button-only changes with no motion). */
+ int loops = 0;
+ do {
+ amt_emit_boot_mouse(s);
+ loops++;
+ } while ((s->pending_dx != 0 || s->pending_dy != 0 ||
+ s->pending_wheel != 0 || s->pending_hwheel != 0) && loops < 64);
+}
+
+static QemuInputHandler amt_input_handler = {
+ .name = "Apple Mighty Mouse",
+ /*
+ * Accept both REL and ABS pointer events. SDL/SPICE/HMP sendkey
+ * tend to send REL; VNC sends ABS. amt_input_event maps both onto
+ * the device's int8 dX/dY wire format.
+ */
+ .mask = INPUT_EVENT_MASK_REL | INPUT_EVENT_MASK_ABS |
+ INPUT_EVENT_MASK_BTN,
+ .event = amt_input_event,
+ .sync = amt_input_sync,
+};
+
+static void usb_apple_magic_tablet_realize(USBDevice *dev, Error **errp)
+{
+ USBAppleMagicTabletState *s = USB_APPLE_MAGIC_TABLET(dev);
+
+ usb_desc_create_serial(dev);
+ usb_desc_init(dev);
+ s->intr = usb_ep_get(dev, USB_TOKEN_IN, 1);
+
+ s->q_head = s->q_tail = 0;
+ s->pending_dx = s->pending_dy = 0;
+ s->pending_wheel = s->pending_hwheel = 0;
+ s->button_left = s->button_right = s->button_middle = false;
+ s->last_abs_x = -1;
+ s->last_abs_y = -1;
+ s->pending_event = false;
+
+ s->input_handler = qemu_input_handler_register(DEVICE(dev),
+ &amt_input_handler);
+ /* q35's built-in i8042 PS/2 mouse claims the input first; mark our
+ * handler active so VNC pointer events reach us. */
+ qemu_input_handler_activate(s->input_handler);
+}
+
+static void usb_apple_magic_tablet_unrealize(USBDevice *dev)
+{
+ USBAppleMagicTabletState *s = USB_APPLE_MAGIC_TABLET(dev);
+
+ if (s->input_handler) {
+ qemu_input_handler_unregister(s->input_handler);
+ s->input_handler = NULL;
+ }
+}
+
+static void usb_apple_magic_tablet_handle_reset(USBDevice *dev)
+{
+ USBAppleMagicTabletState *s = USB_APPLE_MAGIC_TABLET(dev);
+
+ s->q_head = s->q_tail = 0;
+ s->pending_dx = s->pending_dy = 0;
+ s->pending_wheel = s->pending_hwheel = 0;
+ s->button_left = s->button_right = s->button_middle = false;
+ s->pending_event = false;
+ s->last_abs_x = -1;
+ s->last_abs_y = -1;
+}
+
+static void usb_apple_magic_tablet_handle_control(USBDevice *dev, USBPacket *p,
+ int request, int value,
+ int index, int length,
+ uint8_t *data)
+{
+ int ret;
+
+ ret = usb_desc_handle_control(dev, p, request, value, index, length, data);
+ if (ret >= 0) {
+ return;
+ }
+
+ switch (request) {
+ case InterfaceRequest | USB_REQ_GET_DESCRIPTOR:
+ if ((value >> 8) == 0x22) {
+ uint16_t rd_len = sizeof(amt_boot_mouse_report_desc);
+ uint16_t copy = length < rd_len ? length : rd_len;
+ memcpy(data, amt_boot_mouse_report_desc, copy);
+ p->actual_length = copy;
+ return;
+ }
+ break;
+ case HID_GET_IDLE:
+ data[0] = 0;
+ p->actual_length = 1;
+ return;
+ case HID_SET_IDLE:
+ return;
+ case HID_GET_PROTOCOL:
+ data[0] = 1; /* report protocol */
+ p->actual_length = 1;
+ return;
+ case HID_SET_PROTOCOL:
+ return;
+ case HID_GET_REPORT: {
+ uint16_t reply_len = (length > 0 && length <= 64) ? length : 1;
+ if (reply_len > length) {
+ reply_len = length;
+ }
+ memset(data, 0, reply_len);
+ p->actual_length = reply_len;
+ return;
+ }
+ case HID_SET_REPORT:
+ /* Silently accept any host-pushed report (LED state etc.) */
+ p->actual_length = length;
+ return;
+ }
+
+ p->status = USB_RET_STALL;
+}
+
+static void usb_apple_magic_tablet_handle_data(USBDevice *dev, USBPacket *p)
+{
+ USBAppleMagicTabletState *s = USB_APPLE_MAGIC_TABLET(dev);
+
+ if (p->pid != USB_TOKEN_IN || p->ep->nr != 1) {
+ p->status = USB_RET_STALL;
+ return;
+ }
+ if (amt_q_empty(s)) {
+ p->status = USB_RET_NAK;
+ return;
+ }
+
+ USBAppleMagicTabletReport *r = &s->queue[s->q_tail];
+ s->q_tail = (s->q_tail + 1) & (AMT_QUEUE_DEPTH - 1);
+ usb_packet_copy(p, r->data, AMT_REPORT_LEN);
+}
+
+static const VMStateDescription vmstate_apple_magic_tablet = {
+ .name = "apple-mighty-mouse",
+ .unmigratable = 1,
+};
+
+static void usb_apple_magic_tablet_class_initfn(ObjectClass *klass,
+ const void *data)
+{
+ DeviceClass *dc = DEVICE_CLASS(klass);
+ USBDeviceClass *uc = USB_DEVICE_CLASS(klass);
+
+ uc->realize = usb_apple_magic_tablet_realize;
+ uc->product_desc = "Apple Mighty Mouse";
+ uc->usb_desc = &desc_apple_magic_tablet;
+ uc->handle_reset = usb_apple_magic_tablet_handle_reset;
+ uc->handle_control = usb_apple_magic_tablet_handle_control;
+ uc->handle_data = usb_apple_magic_tablet_handle_data;
+ uc->unrealize = usb_apple_magic_tablet_unrealize;
+ set_bit(DEVICE_CATEGORY_INPUT, dc->categories);
+ dc->desc = "Apple Mighty Mouse (USB HID boot mouse, "
+ "VID 0x05ac PID 0x0304)";
+ dc->vmsd = &vmstate_apple_magic_tablet;
+}
+
+static const TypeInfo usb_apple_magic_tablet_info = {
+ .name = TYPE_USB_APPLE_MAGIC_TABLET,
+ .parent = TYPE_USB_DEVICE,
+ .instance_size = sizeof(USBAppleMagicTabletState),
+ .class_init = usb_apple_magic_tablet_class_initfn,
+};
+
+/*
+ * Legacy alias: `-device apple-magic-tablet` resolves to the same Mighty
+ * Mouse implementation. Inheriting via .parent reuses the parent's
+ * class_init and instance_size — no code duplication. Drop this alias
+ * after all in-tree consumers (mos-docker test.sh TABLET_DEVICE env) move
+ * to the canonical `apple-mighty-mouse`.
+ */
+static const TypeInfo usb_apple_magic_tablet_legacy_info = {
+ .name = TYPE_USB_APPLE_MAGIC_TABLET_LEGACY,
+ .parent = TYPE_USB_APPLE_MAGIC_TABLET,
+};
+
static void usb_hid_register_types(void)
{
type_register_static(&usb_hid_type_info);
@@ -1709,6 +2280,8 @@ static void usb_hid_register_types(void)
type_register_static(&usb_keyboard_info);
usb_legacy_register("usb-kbd", "keyboard", NULL);
type_register_static(&usb_apple_magic_kbd_info);
+ type_register_static(&usb_apple_magic_tablet_info);
+ type_register_static(&usb_apple_magic_tablet_legacy_info);
}
type_init(usb_hid_register_types)
--
2.50.1 (Apple Git-155)
^ permalink raw reply related [flat|nested] 6+ messages in thread