* [PATCH 0/2] hw/usb/dev-hid: Apple Magic Keyboard and Mighty Mouse emulators
@ 2026-07-19 15:23 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 ` [PATCH 2/2] hw/usb/dev-hid: add apple-mighty-mouse Matthew Jackson
0 siblings, 2 replies; 6+ messages in thread
From: Matthew Jackson @ 2026-07-19 15:23 UTC (permalink / raw)
To: qemu-devel; +Cc: kraxel
This series adds two new self-contained USB HID devices to
hw/usb/dev-hid.c: apple-magic-keyboard and apple-mighty-mouse,
emulating the USB-cable face of the real Apple Magic Keyboard with
Numeric Keypad (PID 0x026c) and Apple Mighty Mouse (PID 0x0304).
Why this exists
---------------
QEMU has no USB-HID device that macOS recognises as a real Apple
peripheral. With usb-kbd / usb-tablet, macOS guests:
* Run Keyboard Setup Assistant on first boot - costs 3-5 minutes
on interactive installs and effectively hangs headless
(VNC / SPICE) installs.
* Bind the generic IOUSBHostHIDDevice driver instead of the Apple
HID driver chain (AppleUSBTopCaseHIDDriver,
AppleDeviceManagementHIDEventService,
AppleHIDKeyboardEventDriverV2, AppleHIDMouseEventDriver). Several
recovery / install / multi-touch UI panels behave correctly only
when the chain matches an Apple-VID peripheral.
The new devices ship the real wire format (descriptors, HID report
descriptors, endpoint topology) byte-identical to real hardware in
USB-cable mode. The keyboard's boot-interface report descriptor is
byte-for-byte the real Apple Magic Keyboard with Numeric Keypad
descriptor (182 bytes, four application collections), captured from
a real keyboard plugged into a Mac via the macOS IOReg dump.
Self-contained, additive only
-----------------------------
Zero existing files are modified outside the new device blocks in
hw/usb/dev-hid.c. The existing usb-kbd / usb-mouse / usb-tablet
devices and their vmstate are unchanged. Migration of existing VMs
is unaffected.
Apple-vendor identity
---------------------
Both devices declare idVendor 0x05ac (Apple) and the real PIDs.
Precedent: isa-applesmc ships the verbatim Apple OSK string; mac99
emulates real Apple PowerPC hardware; apple-gfx.m (Phil Dennis-Jordan,
2024) emulates Apple's host paravirt GPU. The Apple PnP IDs are
public identifiers (linux-usb's usb.ids database) and emulating them
does not bypass any Apple licensing check beyond what isa-applesmc
already requires.
If reviewers prefer the Apple PID/VID gated behind an
apple-id=on/off device property (default off), a v2 patch is prepared
addressing that. The v1 carries the Apple IDs unconditionally,
matching the precedent set by sibling devices.
Why Mighty Mouse, not Magic Trackpad
------------------------------------
An earlier draft of this series shipped apple-magic-tablet emulating
Apple Magic Trackpad 2 (PID 0x0265). Magic Trackpad 2 binds
AppleMultitouchTrackpadHIDEventDriver in macOS, which gates cursor on
a 1387-byte vendor-multitouch frame format we do not have ground-truth
wire bytes for. Guessing the wire layout produced AppleHIDStack hangs
during recovery boots that wasted multiple debug cycles.
Apple Mighty Mouse (PID 0x0304) is Apple's wired ball mouse, M9087,
released 2005. It binds the generic AppleHIDMouseEventDriver /
IOHIDPointing chain natively — handles standard 5-byte boot mouse
reports (3-bit button mask + signed int8 dX/dY + signed int8 vWheel +
signed int8 hWheel) without any vendor-specific protocol. Same
"Apple-VID-or-fail" property the Apple HID stack checks for; cursor +
click + scroll all work unconditionally. v1 of this series ships this
device.
For backward compatibility with downstream consumers using the earlier
draft's name (which exists in some out-of-tree downstream forks),
patch 0002 also registers a TypeInfo legacy alias `apple-magic-tablet`
inheriting from the canonical `apple-mighty-mouse` type — both
`-device` strings resolve to the same implementation. The alias has
no permanent place in upstream and can be dropped any time after
in-tree consumers move to the canonical name.
Tested
------
Tested on macOS 15.7.5 against a stock retail installer, mos-docker
test rig (privileged container, network_mode=host, --device /dev/kvm,
QEMU 11.0.0 patched binary).
* Phase 2 regression (patched QEMU + macOS HD + isa-applesmc + ICH9
globals + generic usb-kbd / usb-tablet) — boot to login PASS,
visual gold-image diff well under 2% threshold.
* Phase 3 regression (+ apple-magic-keyboard + apple-mighty-mouse
swap) — boot to login PASS, visual gold-image diff well under 2%
threshold. macOS' AppleUSBTopCaseHIDDriver +
AppleHIDKeyboardEventDriverV2 bind on the keyboard;
AppleHIDMouseEventDriver / IOHIDPointing binds on the mouse.
* HID delivery verified via a second screendump after driving a
deterministic input sequence (QMP send-key for 'a','b','c' →
three password dots in the loginwindow field; QMP
input-send-event with absolute pointer to centre coords → cursor
visible at centre). Gold-image diff against
phase-3-input-gold.png passes — proves keyboard and mouse
delivery, not just enumeration.
* Linux-guest descriptor walk (xHCI + libusb) — descriptors and HID
report descriptors decode cleanly. PID/VID/strings as expected.
* Visible keystroke proof — QMP send-key Return advances macOS
recovery's language-picker UI; cursor moves on QMP
input-send-event abs-pointer.
* Migration smoke test — VM saves + restores without device
breakage on both new types.
Per-patch summary
-----------------
0001 — apple-magic-keyboard: composite USB device, two HID
interfaces. Interface 0 carries the Apple-vendor descriptor (182
bytes, byte-for-byte real device) with vendor reports 0xe0 / 0x9a
/ 0x90 the Apple driver chain probes. Interface 1 carries the
Magic Keyboard boot-keyboard descriptor (boot kbd Report 0x01 +
Consumer Eject + Vendor 0xff Usage 0x03; consumer multimedia
Report 0x52; system control feature Report 0x09; vendor Report
0x3f 64-byte feature blob). Wired to the QEMU input subsystem via
qemu_input_handler_register(). STALLs on GET_REPORT for undeclared
report IDs (matches real-device USB HID behaviour — returning
zero-fill there causes macOS' HID parser to wedge during
IOHIDInterface::start). SET_REPORT sets actual_length so the host
doesn't retry indefinitely.
0002 — apple-mighty-mouse: single HID interface, single IN endpoint,
standard boot mouse + scroll Report Descriptor (5-byte report —
3-bit button mask + dX + dY + vWheel + hWheel). Wired to QEMU's
input subsystem accepting REL motion + ABS motion + BTN events.
ABS events (from VNC) are converted to REL via a last_abs_x/y
delta tracker. Bounded queue (depth 64) drains accumulated motion
across multiple reports per input sync so cursor movement stays
responsive under large pointer deltas. Includes the
`apple-magic-tablet` legacy TypeInfo alias as described above.
Series root
-----------
Generated against qemu.git v11.0.0. Each patch applies cleanly and
`make check` is green on x86_64.
git fetch origin master
git checkout -b apple-magic-hid origin/master
git am 0001-*.patch
git am 0002-*.patch
Matthew Jackson (2):
hw/usb/dev-hid: add apple-magic-keyboard
hw/usb/dev-hid: add apple-mighty-mouse
hw/usb/dev-hid.c | 1411 ++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 1411 insertions(+)
--
2.50.1 (Apple Git-155)
^ permalink raw reply [flat|nested] 6+ messages in thread
* [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
* Re: [PATCH 2/2] hw/usb/dev-hid: add apple-mighty-mouse
2026-07-19 15:23 ` [PATCH 2/2] hw/usb/dev-hid: add apple-mighty-mouse Matthew Jackson
@ 2026-07-20 8:22 ` Peter Maydell
2026-07-20 16:51 ` Matthew Jackson
0 siblings, 1 reply; 6+ messages in thread
From: Peter Maydell @ 2026-07-20 8:22 UTC (permalink / raw)
To: Matthew Jackson; +Cc: qemu-devel, kraxel
On Sun, 19 Jul 2026 at 16:24, Matthew Jackson <matthew@pq.io> wrote:
> +
> +/*
> + * 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.
> + */
For QEMU upstream this is a fresh new piece of code and we
don't care about its development history -- we would rather
have it be consistent.
> +/*
> + * 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.
> + */
Reading this comment prompts me to check with you whether you have
read our AI contribution policy, which at present is essentially
a "no AI generated code or text in contributions" policy:
https://www.qemu.org/docs/master/devel/code-provenance.html#use-of-ai-generated-content
thanks
-- PMM
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH 2/2] hw/usb/dev-hid: add apple-mighty-mouse
2026-07-20 8:22 ` Peter Maydell
@ 2026-07-20 16:51 ` Matthew Jackson
2026-07-20 18:24 ` Peter Maydell
0 siblings, 1 reply; 6+ messages in thread
From: Matthew Jackson @ 2026-07-20 16:51 UTC (permalink / raw)
To: Peter Maydell; +Cc: qemu-devel, kraxel
On Mon, 20 Jul 2026 at 08:22, Peter Maydell <peter.maydell@linaro.org> wrote:
> Reading this comment prompts me to check with you whether you have
> read our AI contribution policy, which at present is essentially
> a "no AI generated code or text in contributions" policy:
> https://www.qemu.org/docs/master/devel/code-provenance.html#use-of-ai-generated-content
Thanks Peter, and fair question -- the answer is yes, AI tooling was
used in producing this series, so under the current policy I should not
have sent it in this form. I apologise for that, and I appreciate you
flagging it directly rather than just dropping the patches.
To be transparent about what that means for these patches: this work
comes out of a project that runs macOS guests on QEMU/KVM, and I use an
AI coding assistant in that project. The comment you quoted is a good
example -- it's essentially the assistant's working notes shipped
verbatim, which is exactly the kind of thing that shouldn't be in an
upstream submission (and, as you say, the development-history narrative
doesn't belong there regardless of how it was written).
What is *not* AI-derived is the hardware ground truth: the USB
descriptors, HID report descriptors and endpoint topology are captured
byte-for-byte from real Apple peripherals, and the design decisions
(which device to emulate, why Mighty Mouse rather than Magic Trackpad,
how it binds in macOS) were mine. But I recognise that doesn't resolve
the DCO clause (b) concern about the generated code and text itself, and
I don't want to hand-wave that.
So I'll withdraw this series for now rather than ask you to review it
under a cloud. If I reimplement the device code and comments myself,
from the captured hardware descriptors as the spec, would a resubmission
be welcome -- or would you rather this category of contribution wait
until the AI policy itself is revisited?
Not looking to litigate the policy -- just want to be straight about
provenance and do the right thing here. Thanks again.
-- Matthew
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH 2/2] hw/usb/dev-hid: add apple-mighty-mouse
2026-07-20 16:51 ` Matthew Jackson
@ 2026-07-20 18:24 ` Peter Maydell
0 siblings, 0 replies; 6+ messages in thread
From: Peter Maydell @ 2026-07-20 18:24 UTC (permalink / raw)
To: Matthew Jackson; +Cc: qemu-devel, kraxel, Mohamed Mediouni
On Mon, 20 Jul 2026 at 17:51, Matthew Jackson <matthew@pq.io> wrote:
>
> On Mon, 20 Jul 2026 at 08:22, Peter Maydell <peter.maydell@linaro.org> wrote:
> > Reading this comment prompts me to check with you whether you have
> > read our AI contribution policy, which at present is essentially
> > a "no AI generated code or text in contributions" policy:
> > https://www.qemu.org/docs/master/devel/code-provenance.html#use-of-ai-generated-content
>
> Thanks Peter, and fair question -- the answer is yes, AI tooling was
> used in producing this series, so under the current policy I should not
> have sent it in this form. I apologise for that, and I appreciate you
> flagging it directly rather than just dropping the patches.
>
> To be transparent about what that means for these patches: this work
> comes out of a project that runs macOS guests on QEMU/KVM, and I use an
> AI coding assistant in that project. The comment you quoted is a good
> example -- it's essentially the assistant's working notes shipped
> verbatim, which is exactly the kind of thing that shouldn't be in an
> upstream submission (and, as you say, the development-history narrative
> doesn't belong there regardless of how it was written).
>
> What is *not* AI-derived is the hardware ground truth: the USB
> descriptors, HID report descriptors and endpoint topology are captured
> byte-for-byte from real Apple peripherals, and the design decisions
> (which device to emulate, why Mighty Mouse rather than Magic Trackpad,
> how it binds in macOS) were mine. But I recognise that doesn't resolve
> the DCO clause (b) concern about the generated code and text itself, and
> I don't want to hand-wave that.
>
> So I'll withdraw this series for now rather than ask you to review it
> under a cloud. If I reimplement the device code and comments myself,
> from the captured hardware descriptors as the spec, would a resubmission
> be welcome -- or would you rather this category of contribution wait
> until the AI policy itself is revisited?
I think that if you reimplement the code and comments that would
be OK with our AI policy -- as you say the descriptor stuff is
basically just data.
What I don't know is whether there's anybody interested in reviewing
this particular device model: the whole QEMU USB subsystem is currently
in the "Orphan" state, and I'm not sure how many people upstream are
interested in macos guests. So it's possible that if you do the
work to reimplement and submit a human-written version of this
functionality that the patches will still struggle to get review.
I suspect your applesmc patches are in a similar limbo, so maybe the
path forward is if you can find somebody who's interested in reviewing
those, see if they'd also be interested in reviewing a reworked
equivalent of these apple mouse/kbd devices, and don't do the rework
until you're reasonably confident you have a reviewer lined up ?
I have cc'd Mohamed Mediouni, who said on IRC that they might be
interested in reviewing some of these series.
thanks
-- PMM
^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-07-20 18:25 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [PATCH 2/2] hw/usb/dev-hid: add apple-mighty-mouse Matthew Jackson
2026-07-20 8:22 ` Peter Maydell
2026-07-20 16:51 ` Matthew Jackson
2026-07-20 18:24 ` Peter Maydell
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.