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 <[email protected]>
---
 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)



Reply via email to