From nobody Sat Jul 25 12:50:29 2026 Delivered-To: importer@patchew.org Authentication-Results: mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass(p=none dis=none) header.from=pq.io ARC-Seal: i=1; a=rsa-sha256; t=1784474633; cv=none; d=zohomail.com; s=zohoarc; b=d+icnie6Ceyw4JpegSu9OdDMultSfQF+zR1j4SoVXu3SDKU3NXnn7BvEJKH78kr+ktJ3PSuWEXttrH69ZhNC/+gFakyi/tdYoGL81bKwm8YPcKlGoKjoB6XDdvJMNiYy66fTiosFh/DuVajEUUpIg7NiypySblty//9+K4zUOwo= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1784474633; h=Content-Type:Content-Transfer-Encoding:Cc:Cc:Date:Date:From:From:In-Reply-To:List-Subscribe:List-Post:List-Id:List-Archive:List-Help:List-Unsubscribe:MIME-Version:Message-ID:References:Sender:Subject:Subject:To:To:Message-Id:Reply-To; bh=yIThXkFPPCOZ2/XVeO4VEk0sUXMM6QpaxkS6W7L6Axg=; b=b6N3a9AMkD5fdwhyyWEf1z182sG802FG0g41vm0gxWDhtALsp1XIwXZfraH0mtF7IrtJP05pNpy/oqiA6IbNtAot1JdUzlVr37HqWGPdb6qZ5wt9crJ1R+LqZk6zXKZpiO4SdnZvkmmvZ4gH2LUtU7moZr1GYzYedG4C5qo99w8= ARC-Authentication-Results: i=1; mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass header.from= (p=none dis=none) Return-Path: Received: from lists1p.gnu.org (lists1p.gnu.org [209.51.188.17]) by mx.zohomail.com with SMTPS id 1784474633170160.55222372244486; Sun, 19 Jul 2026 08:23:53 -0700 (PDT) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists1p.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1wlTMd-0001kj-K3; Sun, 19 Jul 2026 11:23:35 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists1p.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1wlTMc-0001k4-2R for qemu-devel@nongnu.org; Sun, 19 Jul 2026 11:23:34 -0400 Received: from mail-106112.protonmail.ch ([79.135.106.112]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1wlTMW-0006iI-Ky for qemu-devel@nongnu.org; Sun, 19 Jul 2026 11:23:33 -0400 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=pq.io; s=protonmail3; t=1784474604; x=1784733804; bh=yIThXkFPPCOZ2/XVeO4VEk0sUXMM6QpaxkS6W7L6Axg=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References:From:To: Cc:Date:Subject:Reply-To:Feedback-ID:Message-ID:BIMI-Selector; b=aotGgkNrE4NABNggIYM9/w8AGYOddMKYf80hkAj1uVbBhdL7M3Oe819DfLt07CZWb /UOBON7mFiOPkTFPiSIZEFdboynNJcOUsEf1adC0d8mp//dR4UKF5gFpYoPkcNEoVz Ybyc/q78CrWnZjohIy7pyfJKhnLYiR6dyMODpeJ6eYl3NcKoMgb33HwhikN/3Pky9o AbbrWVn+8QHHIle7YNbOq3DgDNlcPX0dVLw9t/Z8NiwdWNkPbavxqhDbkN9UvE3mH+ or8rOc/KiyDyyWVQF6kbQkXUyCKvHFhLvnTTrGnTdkBHiesLBJBK3ulj41zrIHgjTd mFJZcbFpZwzsw== X-Pm-Submission-Id: 4h36qG1bB2z1DFgc From: Matthew Jackson To: qemu-devel@nongnu.org Cc: kraxel@redhat.com Subject: [PATCH 1/2] hw/usb/dev-hid: add apple-magic-keyboard Date: Sun, 19 Jul 2026 08:23:17 -0700 Message-ID: <20260719152318.69501-2-matthew@pq.io> X-Mailer: git-send-email 2.50.1 In-Reply-To: <20260719152318.69501-1-matthew@pq.io> References: <20260719152318.69501-1-matthew@pq.io> MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Received-SPF: pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) client-ip=209.51.188.17; envelope-from=qemu-devel-bounces+importer=patchew.org@nongnu.org; helo=lists1p.gnu.org; Received-SPF: pass client-ip=79.135.106.112; envelope-from=matthew@pq.io; helo=mail-106112.protonmail.ch X-Spam_score_int: -27 X-Spam_score: -2.8 X-Spam_bar: -- X-Spam_report: (-2.8 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, RCVD_IN_DNSWL_LOW=-0.7, SPF_HELO_PASS=-0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: qemu development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+importer=patchew.org@nongnu.org Sender: qemu-devel-bounces+importer=patchew.org@nongnu.org X-ZohoMail-DKIM: pass (identity @pq.io) X-ZM-MESSAGEID: 1784474636010158500 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 =E2=80=94 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 =E2=80=94 boot keyboard (UsagePage 0x07). Standard 10-byte bo= ot 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 =E2=86=92 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 --- 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 @@ =20 #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 =3D { .class_init =3D usb_keyboard_class_initfn, }; =20 + +/* + * 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 =E2=80=94 Apple-vendor HID (UsagePage 0xff00). Carries th= ree + * vendor input report IDs that the macOS Apple HID driver chain + * (AppleUSBTopCaseHIDDriver =E2=86=92 AppleDeviceManagementHIDEventSe= rvice + * =E2=86=92 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 =E2=80=94 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 =3D vendor-defined HID reports (Interface 0). + * EP2 IN =3D 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 =E2=80=94 Report ID (0x01) + * byte 1 =E2=80=94 modifier byte (LCtrl=3D0x01 LShift=3D0x02 LAlt=3D0x0= 4 LGUI=3D0x08 + * RCtrl=3D0x10 RShift=3D0x20 RAlt=3D0x40 RGUI=3D0x80) + * byte 2 =E2=80=94 reserved (0) + * bytes 3..9 =E2=80=94 up to 7 simultaneous HID Usage codes, 0 in unuse= d slots + */ +#define AMK_BOOT_REPORT_ID 0x01 +/* + * Boot keyboard input report (10 bytes total =E2=80=94 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 =3D Eject (Consumer Page), bits1..7 =3D 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 =E2=80=94 using 7 slots m= akes + * 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 =3D 1, + STR_AMK_PRODUCT, + STR_AMK_SERIAL, + STR_AMK_INTERFACE, + STR_AMK_INTERFACE_BOOT, +}; + +static const USBDescStrings desc_strings_amk =3D { + [STR_AMK_MFR] =3D "Apple Inc.", + [STR_AMK_PRODUCT] =3D "Magic Keyboard with Numeric Keypad", + [STR_AMK_SERIAL] =3D "F0T924300PCJKNCAS", + [STR_AMK_INTERFACE] =3D "Device Management", + [STR_AMK_INTERFACE_BOOT] =3D "Keyboard / Boot", +}; + +/* + * HID Report Descriptor =E2=80=94 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 =E2=80=94 Apple-encoded keyboard event payload (keys /= mods) + * 0x9a: 1 byte =E2=80=94 short-form vendor signal + * 0x90: 3 bytes =E2=80=94 power/battery status (charging, AC, percent) + * + * Decoded layout (see project_apple_magic_hid_emulator_2026_05_07.md): + * Application 1: UsagePage 0xff00 / Usage 0x0b =E2=80=94 keystrokes + s= ignals + * Application 2: UsagePage 0xff00 / Usage 0x14 =E2=80=94 power/battery + */ +static const uint8_t apple_magic_kbd_hid_report_descriptor[] =3D { + /* 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 =3D 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 =3D 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 b= yte */ + 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 =E2=80=94 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 =3D ReportID byte + 9 data bytes =3D 10 bytes. + * Total output report (LEDs) =3D ReportID byte + 1 data byte =3D 2 bytes. + * Real Magic Keyboards expose a similar boot-keyboard interface + * alongside the vendor (UsagePage 0xff00) interface. + */ +/* + * Boot keyboard report descriptor =E2=80=94 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 =E2=80=94 boot keyboard (mod+reserved+6 keys), + * Consumer eject (1 bit), Vendor 0xff00 (7 bits) + * App 2, Report 0x52 =E2=80=94 consumer multimedia keys (mute, vol, etc= .) + * App 3, Report 0x09 =E2=80=94 generic desktop control (system on/off e= tc.) + * App 4, Report 0x3f =E2=80=94 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 =E2=80=94 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[] =3D { + /* 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[] =3D { + { + /* Interface 0: Apple-vendor HID. */ + .bInterfaceNumber =3D 0, + .bNumEndpoints =3D 1, + .bInterfaceClass =3D USB_CLASS_HID, + .bInterfaceSubClass =3D 0x00, /* NOT boot =E2=80=94 = vendor */ + .bInterfaceProtocol =3D 0x00, /* NOT keyboard =E2=80= =94 vendor */ + .iInterface =3D STR_AMK_INTERFACE, + .ndesc =3D 1, + .descs =3D (USBDescOther[]) { + { + /* HID descriptor */ + .data =3D (uint8_t[]) { + 0x09, /* bLength */ + USB_DT_HID, /* bDescriptorType */ + 0x11, 0x01, /* bcdHID 1.11 */ + 0x00, /* bCountryCode */ + 0x01, /* bNumDescriptors */ + USB_DT_REPORT, /* bDescriptorType: R= eport */ + sizeof(apple_magic_kbd_hid_report_descriptor) & 0xff, + sizeof(apple_magic_kbd_hid_report_descriptor) >> 8, + }, + }, + }, + .eps =3D (USBDescEndpoint[]) { + { + .bEndpointAddress =3D USB_DIR_IN | AMK_EP_VENDOR_IN, + .bmAttributes =3D USB_ENDPOINT_XFER_INT, + .wMaxPacketSize =3D 8, + .bInterval =3D 7, /* 2 ^ (8-1) * 125 us =3D 8 = ms */ + }, + }, + }, + { + /* Interface 1: standard HID boot keyboard. */ + .bInterfaceNumber =3D 1, + .bNumEndpoints =3D 1, + .bInterfaceClass =3D USB_CLASS_HID, + .bInterfaceSubClass =3D 0x01, /* boot */ + .bInterfaceProtocol =3D 0x01, /* keyboard */ + .iInterface =3D STR_AMK_INTERFACE_BOOT, + .ndesc =3D 1, + .descs =3D (USBDescOther[]) { + { + /* HID descriptor */ + .data =3D (uint8_t[]) { + 0x09, /* bLength */ + USB_DT_HID, /* bDescriptorType */ + 0x11, 0x01, /* bcdHID 1.11 */ + 0x00, /* bCountryCode */ + 0x01, /* bNumDescriptors */ + USB_DT_REPORT, /* bDescriptorType: R= eport */ + sizeof(apple_magic_kbd_boot_hid_report_descriptor) & 0= xff, + sizeof(apple_magic_kbd_boot_hid_report_descriptor) >> = 8, + }, + }, + }, + .eps =3D (USBDescEndpoint[]) { + { + .bEndpointAddress =3D USB_DIR_IN | AMK_EP_BOOT_IN, + .bmAttributes =3D USB_ENDPOINT_XFER_INT, + /* + * wMaxPacketSize=3D64 (full-speed interrupt max) =E2=80= =94 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 =E2=80=94 only the 10-byte boot kbd Repo= rt 0x01. + */ + .wMaxPacketSize =3D 64, + .bInterval =3D 8, /* 2 ^ (8-1) * 125 us =3D 8 = ms */ + }, + }, + }, +}; + +static const USBDescDevice desc_device_apple_magic_kbd =3D { + .bcdUSB =3D 0x0200, + .bMaxPacketSize0 =3D 64, + .bNumConfigurations =3D 1, + .confs =3D (USBDescConfig[]) { + { + .bNumInterfaces =3D 2, + .bConfigurationValue =3D 1, + .iConfiguration =3D STR_AMK_PRODUCT, + .bmAttributes =3D USB_CFG_ATT_ONE | USB_CFG_ATT_WAKEU= P, + .bMaxPower =3D 250, /* 500 mA =E2=80=94 matches re= al */ + .nif =3D ARRAY_SIZE(desc_iface_apple_magic_k= bd), + .ifs =3D desc_iface_apple_magic_kbd, + }, + }, +}; + +static const USBDesc desc_apple_magic_kbd =3D { + .id =3D { + .idVendor =3D 0x05ac, /* Apple Inc. */ + .idProduct =3D 0x026c, /* Magic Keyboard= with NumPad */ + .bcdDevice =3D 0x0870, + .iManufacturer =3D STR_AMK_MFR, + .iProduct =3D STR_AMK_PRODUCT, + .iSerialNumber =3D STR_AMK_SERIAL, + }, + /* + * Real Magic Keyboard runs at USB full-speed (12 Mb/s) despite + * declaring bcdUSB=3D0x0200. 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 =3D &desc_device_apple_magic_kbd, + .high =3D &desc_device_apple_magic_kbd, + .str =3D desc_strings_amk, +}; + +/* + * QKeyCode =E2=86=92 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 =E2=80=94 modifier vs slot keys are distinguished by + * value range (0xE0..0xE7 =3D modifier). + * + * Entries left at 0 are unmapped and ignored. + */ +static const uint8_t apple_magic_kbd_qcode_to_hid_usage[Q_KEY_CODE__MAX] = =3D { + /* Letters */ + [Q_KEY_CODE_A] =3D 0x04, [Q_KEY_CODE_B] =3D 0x05, [Q_KEY_CODE_C] =3D 0= x06, + [Q_KEY_CODE_D] =3D 0x07, [Q_KEY_CODE_E] =3D 0x08, [Q_KEY_CODE_F] =3D 0= x09, + [Q_KEY_CODE_G] =3D 0x0a, [Q_KEY_CODE_H] =3D 0x0b, [Q_KEY_CODE_I] =3D 0= x0c, + [Q_KEY_CODE_J] =3D 0x0d, [Q_KEY_CODE_K] =3D 0x0e, [Q_KEY_CODE_L] =3D 0= x0f, + [Q_KEY_CODE_M] =3D 0x10, [Q_KEY_CODE_N] =3D 0x11, [Q_KEY_CODE_O] =3D 0= x12, + [Q_KEY_CODE_P] =3D 0x13, [Q_KEY_CODE_Q] =3D 0x14, [Q_KEY_CODE_R] =3D 0= x15, + [Q_KEY_CODE_S] =3D 0x16, [Q_KEY_CODE_T] =3D 0x17, [Q_KEY_CODE_U] =3D 0= x18, + [Q_KEY_CODE_V] =3D 0x19, [Q_KEY_CODE_W] =3D 0x1a, [Q_KEY_CODE_X] =3D 0= x1b, + [Q_KEY_CODE_Y] =3D 0x1c, [Q_KEY_CODE_Z] =3D 0x1d, + /* Top-row digits 1..0 */ + [Q_KEY_CODE_1] =3D 0x1e, [Q_KEY_CODE_2] =3D 0x1f, [Q_KEY_CODE_3] =3D 0= x20, + [Q_KEY_CODE_4] =3D 0x21, [Q_KEY_CODE_5] =3D 0x22, [Q_KEY_CODE_6] =3D 0= x23, + [Q_KEY_CODE_7] =3D 0x24, [Q_KEY_CODE_8] =3D 0x25, [Q_KEY_CODE_9] =3D 0= x26, + [Q_KEY_CODE_0] =3D 0x27, + /* Editing / whitespace */ + [Q_KEY_CODE_RET] =3D 0x28, + [Q_KEY_CODE_ESC] =3D 0x29, + [Q_KEY_CODE_BACKSPACE] =3D 0x2a, + [Q_KEY_CODE_TAB] =3D 0x2b, + [Q_KEY_CODE_SPC] =3D 0x2c, + [Q_KEY_CODE_MINUS] =3D 0x2d, + [Q_KEY_CODE_EQUAL] =3D 0x2e, + [Q_KEY_CODE_BRACKET_LEFT] =3D 0x2f, + [Q_KEY_CODE_BRACKET_RIGHT] =3D 0x30, + [Q_KEY_CODE_BACKSLASH] =3D 0x31, + [Q_KEY_CODE_SEMICOLON] =3D 0x33, + [Q_KEY_CODE_APOSTROPHE] =3D 0x34, + [Q_KEY_CODE_GRAVE_ACCENT] =3D 0x35, + [Q_KEY_CODE_COMMA] =3D 0x36, + [Q_KEY_CODE_DOT] =3D 0x37, + [Q_KEY_CODE_SLASH] =3D 0x38, + [Q_KEY_CODE_CAPS_LOCK] =3D 0x39, + /* Function row F1..F12 */ + [Q_KEY_CODE_F1] =3D 0x3a, [Q_KEY_CODE_F2] =3D 0x3b, [Q_KEY_CODE_F3] = =3D 0x3c, + [Q_KEY_CODE_F4] =3D 0x3d, [Q_KEY_CODE_F5] =3D 0x3e, [Q_KEY_CODE_F6] = =3D 0x3f, + [Q_KEY_CODE_F7] =3D 0x40, [Q_KEY_CODE_F8] =3D 0x41, [Q_KEY_CODE_F9] = =3D 0x42, + [Q_KEY_CODE_F10] =3D 0x43, [Q_KEY_CODE_F11] =3D 0x44, [Q_KEY_CODE_F12]= =3D 0x45, + /* Print / lock / pause */ + [Q_KEY_CODE_PRINT] =3D 0x46, + [Q_KEY_CODE_SCROLL_LOCK] =3D 0x47, + [Q_KEY_CODE_PAUSE] =3D 0x48, + /* Editing block */ + [Q_KEY_CODE_INSERT] =3D 0x49, + [Q_KEY_CODE_HOME] =3D 0x4a, + [Q_KEY_CODE_PGUP] =3D 0x4b, + [Q_KEY_CODE_DELETE] =3D 0x4c, + [Q_KEY_CODE_END] =3D 0x4d, + [Q_KEY_CODE_PGDN] =3D 0x4e, + [Q_KEY_CODE_RIGHT] =3D 0x4f, + [Q_KEY_CODE_LEFT] =3D 0x50, + [Q_KEY_CODE_DOWN] =3D 0x51, + [Q_KEY_CODE_UP] =3D 0x52, + /* Keypad */ + [Q_KEY_CODE_NUM_LOCK] =3D 0x53, + [Q_KEY_CODE_KP_DIVIDE] =3D 0x54, + [Q_KEY_CODE_KP_MULTIPLY] =3D 0x55, + [Q_KEY_CODE_ASTERISK] =3D 0x55, /* duplicate name in qcode table */ + [Q_KEY_CODE_KP_SUBTRACT] =3D 0x56, + [Q_KEY_CODE_KP_ADD] =3D 0x57, + [Q_KEY_CODE_KP_ENTER] =3D 0x58, + [Q_KEY_CODE_KP_1] =3D 0x59, + [Q_KEY_CODE_KP_2] =3D 0x5a, + [Q_KEY_CODE_KP_3] =3D 0x5b, + [Q_KEY_CODE_KP_4] =3D 0x5c, + [Q_KEY_CODE_KP_5] =3D 0x5d, + [Q_KEY_CODE_KP_6] =3D 0x5e, + [Q_KEY_CODE_KP_7] =3D 0x5f, + [Q_KEY_CODE_KP_8] =3D 0x60, + [Q_KEY_CODE_KP_9] =3D 0x61, + [Q_KEY_CODE_KP_0] =3D 0x62, + [Q_KEY_CODE_KP_DECIMAL] =3D 0x63, + [Q_KEY_CODE_LESS] =3D 0x64, /* non-US backslash / ISO key */ + [Q_KEY_CODE_KP_EQUALS] =3D 0x67, + /* F13..F24 */ + [Q_KEY_CODE_F13] =3D 0x68, [Q_KEY_CODE_F14] =3D 0x69, [Q_KEY_CODE_F15]= =3D 0x6a, + [Q_KEY_CODE_F16] =3D 0x6b, [Q_KEY_CODE_F17] =3D 0x6c, [Q_KEY_CODE_F18]= =3D 0x6d, + [Q_KEY_CODE_F19] =3D 0x6e, [Q_KEY_CODE_F20] =3D 0x6f, [Q_KEY_CODE_F21]= =3D 0x70, + [Q_KEY_CODE_F22] =3D 0x71, [Q_KEY_CODE_F23] =3D 0x72, [Q_KEY_CODE_F24]= =3D 0x73, + /* Misc named keys */ + [Q_KEY_CODE_HELP] =3D 0x75, + [Q_KEY_CODE_MENU] =3D 0x76, + [Q_KEY_CODE_STOP] =3D 0x78, + [Q_KEY_CODE_AGAIN] =3D 0x79, + [Q_KEY_CODE_UNDO] =3D 0x7a, + [Q_KEY_CODE_CUT] =3D 0x7b, + [Q_KEY_CODE_COPY] =3D 0x7c, + [Q_KEY_CODE_PASTE] =3D 0x7d, + [Q_KEY_CODE_FIND] =3D 0x7e, + [Q_KEY_CODE_AUDIOMUTE] =3D 0x7f, + [Q_KEY_CODE_VOLUMEUP] =3D 0x80, + [Q_KEY_CODE_VOLUMEDOWN] =3D 0x81, + [Q_KEY_CODE_KP_COMMA] =3D 0x85, + [Q_KEY_CODE_RO] =3D 0x87, /* Intl1 (Japanese RO) */ + [Q_KEY_CODE_KATAKANAHIRAGANA]=3D 0x88, /* Intl2 */ + [Q_KEY_CODE_YEN] =3D 0x89, /* Intl3 */ + [Q_KEY_CODE_HENKAN] =3D 0x8a, /* Intl4 */ + [Q_KEY_CODE_MUHENKAN] =3D 0x8b, /* Intl5 */ + [Q_KEY_CODE_HIRAGANA] =3D 0x91, /* LANG4 (close enough) */ + [Q_KEY_CODE_LANG1] =3D 0x90, + [Q_KEY_CODE_LANG2] =3D 0x91, + /* Modifiers =E2=80=94 HID Usages 0xE0..0xE7 (also written into modifi= er byte). */ + [Q_KEY_CODE_CTRL] =3D 0xe0, + [Q_KEY_CODE_SHIFT] =3D 0xe1, + [Q_KEY_CODE_ALT] =3D 0xe2, + [Q_KEY_CODE_META_L] =3D 0xe3, + [Q_KEY_CODE_CTRL_R] =3D 0xe4, + [Q_KEY_CODE_SHIFT_R] =3D 0xe5, + [Q_KEY_CODE_ALT_R] =3D 0xe6, + [Q_KEY_CODE_META_R] =3D 0xe7, +}; + +/* + * Pack the live boot-keyboard state into a 10-byte report payload =E2=80= =94 + * matches real Apple Magic Keyboard Report 0x01 layout (boot keyboard + * + Consumer Eject + Vendor 0xff Usage 0x03): + * buf[0] =3D report ID (0x01) + * buf[1] =3D modifier byte (HID Usages 0xE0..0xE7) + * buf[2] =3D reserved (0) + * buf[3..8] =3D 6 keycode slots (HID Usage codes) + * buf[9] =3D bit0 Consumer Eject + bits1..7 Vendor 0xff Usage 0x03 + * (always 0 =E2=80=94 emulator does not generate eject or ve= ndor) + */ +static void apple_magic_kbd_pack_boot_report(USBAppleMagicKbdState *s, + uint8_t buf[AMK_BOOT_REPORT_L= EN]) +{ + buf[0] =3D AMK_BOOT_REPORT_ID; + buf[1] =3D s->boot_modifiers; + buf[2] =3D 0; + memcpy(&buf[3], s->boot_keys, AMK_BOOT_NUM_KEYS); /* 6 keycodes */ + buf[9] =3D 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 =3D=3D 0) { + return false; + } + + /* Modifiers =E2=80=94 packed bitmap into the modifier byte. */ + if (usage >=3D 0xe0 && usage <=3D 0xe7) { + uint8_t bit =3D 1u << (usage - 0xe0); + uint8_t prev =3D s->boot_modifiers; + if (down) { + s->boot_modifiers |=3D bit; + } else { + s->boot_modifiers &=3D ~bit; + } + return s->boot_modifiers !=3D prev; + } + + /* Slot keys =E2=80=94 7-slot array, no duplicates. */ + if (down) { + for (i =3D 0; i < AMK_BOOT_NUM_KEYS; i++) { + if (s->boot_keys[i] =3D=3D usage) { + return false; /* already pressed */ + } + } + for (i =3D 0; i < AMK_BOOT_NUM_KEYS; i++) { + if (s->boot_keys[i] =3D=3D 0) { + s->boot_keys[i] =3D usage; + return true; + } + } + /* Roll-over =E2=80=94 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 =3D 0; i < AMK_BOOT_NUM_KEYS; i++) { + if (s->boot_keys[i] =3D=3D usage) { + /* Compact slots so packed array stays contiguous. */ + int j; + for (j =3D i; j < AMK_BOOT_NUM_KEYS - 1; j++) { + s->boot_keys[j] =3D s->boot_keys[j + 1]; + } + s->boot_keys[AMK_BOOT_NUM_KEYS - 1] =3D 0; + return true; + } + } + return false; + } +} + +static void apple_magic_kbd_input_event(DeviceState *dev, QemuConsole *src, + InputEvent *evt) +{ + USBAppleMagicKbdState *s =3D USB_APPLE_MAGIC_KBD(dev); + InputKeyEvent *key; + int qcode; + uint8_t usage; + + if (evt->type !=3D INPUT_EVENT_KIND_KEY) { + return; + } + + key =3D evt->u.key.data; + qcode =3D qemu_input_key_value_to_qcode(key->key); + if (qcode < 0 || qcode >=3D Q_KEY_CODE__MAX) { + return; + } + usage =3D apple_magic_kbd_qcode_to_hid_usage[qcode]; + if (usage =3D=3D 0) { + return; + } + + if (apple_magic_kbd_apply_usage(s, usage, key->down)) { + s->boot_changed =3D true; + if (s->boot_intr) { + usb_wakeup(s->boot_intr, 0); + } + } +} + +static const QemuInputHandler apple_magic_kbd_input_handler =3D { + .name =3D "Apple Magic Keyboard (boot)", + .mask =3D INPUT_EVENT_MASK_KEY, + .event =3D apple_magic_kbd_input_event, +}; + +static void usb_apple_magic_kbd_realize(USBDevice *dev, Error **errp) +{ + USBAppleMagicKbdState *s =3D 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 =3D usb_ep_get(dev, USB_TOKEN_IN, AMK_EP_BOOT_IN); + s->input_handler =3D 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 =3D { + .name =3D "apple-magic-keyboard", + .unmigratable =3D 1, +}; + +static void usb_apple_magic_kbd_handle_reset(USBDevice *dev) +{ + USBAppleMagicKbdState *s =3D USB_APPLE_MAGIC_KBD(dev); + + s->boot_modifiers =3D 0; + memset(s->boot_keys, 0, sizeof(s->boot_keys)); + s->boot_changed =3D 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 =3D USB_APPLE_MAGIC_KBD(dev); + int ret; + + ret =3D usb_desc_handle_control(dev, p, request, value, index, length,= data); + if (ret >=3D 0) { + return; + } + + switch (request) { + case InterfaceRequest | USB_REQ_GET_DESCRIPTOR: + if ((value >> 8) =3D=3D 0x22) { + /* + * GET_DESCRIPTOR(REPORT). Pick the right report descriptor + * based on the interface index in wIndex. Interface 0 =3D + * Apple vendor HID; Interface 1 =3D boot keyboard. + */ + const uint8_t *rd; + uint16_t rd_len; + uint16_t copy; + if (index =3D=3D 1) { + rd =3D apple_magic_kbd_boot_hid_report_descriptor; + rd_len =3D sizeof(apple_magic_kbd_boot_hid_report_descript= or); + } else { + rd =3D apple_magic_kbd_hid_report_descriptor; + rd_len =3D sizeof(apple_magic_kbd_hid_report_descriptor); + } + copy =3D length < rd_len ? length : rd_len; + memcpy(data, rd, copy); + p->actual_length =3D copy; + return; + } + break; + case HID_GET_IDLE: + data[0] =3D 0; + p->actual_length =3D 1; + return; + case HID_SET_IDLE: + return; + case HID_GET_PROTOCOL: + data[0] =3D 1; /* report protocol */ + p->actual_length =3D 1; + return; + case HID_SET_PROTOCOL: + return; + case HID_GET_REPORT: { + /* + * GET_REPORT =E2=80=94 feature/input poll over EP0. Behaviour dep= ends + * 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 =E2=86=92 4 bytes (input only, but driver may probe Fea= ture) + * 0x9a =E2=86=92 1 byte + * 0x90 =E2=86=92 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 =3D value & 0xff; + uint8_t report_type =3D (value >> 8) & 0xff; + uint16_t reply_len =3D 0; + + if (index =3D=3D 1) { + uint8_t buf[AMK_BOOT_REPORT_LEN]; + if (report_type =3D=3D 0x01 /* Input */ && + report_id =3D=3D AMK_BOOT_REPORT_ID) { + apple_magic_kbd_pack_boot_report(s, buf); + reply_len =3D AMK_BOOT_REPORT_LEN; + if (reply_len > length) { + reply_len =3D length; + } + memcpy(data, buf, reply_len); + p->actual_length =3D reply_len; + return; + } + /* + * Unknown report type/ID on the boot iface =E2=80=94 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 =E2=80=94 only declared report IDs ans= wer. */ + switch (report_id) { + case 0xe0: reply_len =3D 4; break; + case 0x9a: reply_len =3D 1; break; + case 0x90: reply_len =3D 2; break; + default: + /* Unknown vendor report ID =E2=80=94 STALL (real device behav= iour). */ + break; + } + if (reply_len =3D=3D 0) { + break; /* falls through to STALL */ + } + if (reply_len > length) { + reply_len =3D length; + } + memset(data, 0, reply_len); + p->actual_length =3D 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 =E2=80=94 + * exactly that retry pattern. + */ + p->actual_length =3D length; + return; + } + + p->status =3D USB_RET_STALL; +} + +static void usb_apple_magic_kbd_handle_data(USBDevice *dev, USBPacket *p) +{ + USBAppleMagicKbdState *s =3D USB_APPLE_MAGIC_KBD(dev); + + if (p->pid !=3D USB_TOKEN_IN) { + p->status =3D 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 =3D 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 =3D USB_RET_NAK; + return; + } + s->boot_changed =3D false; + apple_magic_kbd_pack_boot_report(s, buf); + copy =3D p->iov.size < AMK_BOOT_REPORT_LEN + ? p->iov.size : AMK_BOOT_REPORT_LEN; + usb_packet_copy(p, buf, copy); + return; + } + default: + p->status =3D USB_RET_STALL; + return; + } +} + +static void usb_apple_magic_kbd_unrealize(USBDevice *dev) +{ + USBAppleMagicKbdState *s =3D USB_APPLE_MAGIC_KBD(dev); + + if (s->input_handler) { + qemu_input_handler_unregister(s->input_handler); + s->input_handler =3D NULL; + } +} + +static void usb_apple_magic_kbd_class_initfn(ObjectClass *klass, + const void *data) +{ + DeviceClass *dc =3D DEVICE_CLASS(klass); + USBDeviceClass *uc =3D USB_DEVICE_CLASS(klass); + + uc->realize =3D usb_apple_magic_kbd_realize; + uc->product_desc =3D "Magic Keyboard with Numeric Keypad"; + uc->usb_desc =3D &desc_apple_magic_kbd; + uc->handle_reset =3D usb_apple_magic_kbd_handle_reset; + uc->handle_control =3D usb_apple_magic_kbd_handle_control; + uc->handle_data =3D usb_apple_magic_kbd_handle_data; + uc->unrealize =3D usb_apple_magic_kbd_unrealize; + set_bit(DEVICE_CATEGORY_INPUT, dc->categories); + dc->desc =3D "Apple Magic Keyboard with Numeric Keypad " + "(USB-mode emulator, vendor HID protocol)"; + dc->vmsd =3D &vmstate_apple_magic_kbd; +} + +static const TypeInfo usb_apple_magic_kbd_info =3D { + .name =3D TYPE_USB_APPLE_MAGIC_KBD, + .parent =3D TYPE_USB_DEVICE, + .instance_size =3D sizeof(USBAppleMagicKbdState), + .class_init =3D 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); } =20 type_init(usb_hid_register_types) --=20 2.50.1 (Apple Git-155) From nobody Sat Jul 25 12:50:29 2026 Delivered-To: importer@patchew.org Authentication-Results: mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass(p=none dis=none) header.from=pq.io ARC-Seal: i=1; a=rsa-sha256; t=1784474671; cv=none; d=zohomail.com; s=zohoarc; b=EKBsqIuynvZxF8ObEHe+CvvSeCCS20Yt23QN1CgC28vHHLh70DaSvaY0ashPki6UEJkUVWjMK9ZmZquTPy39tjivyHihDdlbvgeKrwz1236nrpOJd2OVC3w7ZMOJBly/sDfHwvULXUP824WerCsyD5ql8AXYnR1Ds7a26EeyqNg= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1784474671; h=Content-Type:Content-Transfer-Encoding:Cc:Cc:Date:Date:From:From:In-Reply-To:List-Subscribe:List-Post:List-Id:List-Archive:List-Help:List-Unsubscribe:MIME-Version:Message-ID:References:Sender:Subject:Subject:To:To:Message-Id:Reply-To; bh=OivRrtZTNeQl5oxpa0HKSqkD+PjjYw5hTY1+o0U2lds=; b=FvbyVPN3MiT5Ld3BrouXkZua4ckTs0mX9OSF55fcPM51miMFrxtF33S3aPWA9TWOeqF6j+UdnQrSwhmPcw5wo1pTI0pFF0lRN9ym1QOe4eiBytqmZh/IMo4VZ4746FUJ0SbXqBL5LhlJ9cxkpOhhPr+mSRvP/sCWTnyxnq/Ms8M= ARC-Authentication-Results: i=1; mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass header.from= (p=none dis=none) Return-Path: Received: from lists1p.gnu.org (lists1p.gnu.org [209.51.188.17]) by mx.zohomail.com with SMTPS id 1784474671028566.6072225549578; Sun, 19 Jul 2026 08:24:31 -0700 (PDT) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists1p.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1wlTMg-0001nx-2g; Sun, 19 Jul 2026 11:23:38 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists1p.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1wlTMb-0001jg-O5 for qemu-devel@nongnu.org; Sun, 19 Jul 2026 11:23:33 -0400 Received: from mail-05.mail-europe.com ([85.9.206.169]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1wlTMY-0006ik-GF for qemu-devel@nongnu.org; Sun, 19 Jul 2026 11:23:33 -0400 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=pq.io; s=protonmail3; t=1784474603; x=1784733803; bh=OivRrtZTNeQl5oxpa0HKSqkD+PjjYw5hTY1+o0U2lds=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References:From:To: Cc:Date:Subject:Reply-To:Feedback-ID:Message-ID:BIMI-Selector; b=XE/l8K3szTqPpyossLH/yBoTDll9X/TowXcay5WEN85kKthJWP0aHzqmB/D70gw8S 42o7+++t4J8o5NfYZApnnh3z2AxlBWXGNHlpsg2NT3IB4DibPfcDH0Rgq655TprfQg ixRXzn+7Ux17EWKpQHSjUoxgBBv6kGgKKDOOm7r/sYJmW6XNearQG0Ksc7P8S7GN2G mrdFpzHdh9LERuC/mK2JL4RfKEAoXB7uuXNnAZXjriXv250BtkLgJ/NfJq8C/J6lXe xYdU6ZpyuFTSbvceCooirZ2AfcZVWG8BTn7DNaBfaqWIOqUU50JXpxZHjYQv6r03X7 0kh8tuhhIBjfg== X-Pm-Submission-Id: 4h36qG1Lbtz2ScWZ From: Matthew Jackson To: qemu-devel@nongnu.org Cc: kraxel@redhat.com Subject: [PATCH 2/2] hw/usb/dev-hid: add apple-mighty-mouse Date: Sun, 19 Jul 2026 08:23:18 -0700 Message-ID: <20260719152318.69501-3-matthew@pq.io> X-Mailer: git-send-email 2.50.1 In-Reply-To: <20260719152318.69501-1-matthew@pq.io> References: <20260719152318.69501-1-matthew@pq.io> MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Received-SPF: pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) client-ip=209.51.188.17; envelope-from=qemu-devel-bounces+importer=patchew.org@nongnu.org; helo=lists1p.gnu.org; Received-SPF: pass client-ip=85.9.206.169; envelope-from=matthew@pq.io; helo=mail-05.mail-europe.com X-Spam_score_int: -27 X-Spam_score: -2.8 X-Spam_bar: -- X-Spam_report: (-2.8 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, RCVD_IN_DNSWL_LOW=-0.7, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: qemu development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+importer=patchew.org@nongnu.org Sender: qemu-devel-bounces+importer=patchew.org@nongnu.org X-ZohoMail-DKIM: pass (identity @pq.io) X-ZM-MESSAGEID: 1784474672217158500 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=3D1, bProto=3D2 (boot mouse) =E2=80=94 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=3Dapple-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 --- 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 =3D { .class_init =3D usb_apple_magic_kbd_class_initfn, }; =20 + +/* + * apple-mighty-mouse (USB type name; legacy alias `apple-magic-tablet`) + * -------------------------------------------------------------------- + * + * Internal symbols carry the historic `amt` / `apple_magic_tablet` / + * `USBAppleMagicTabletState` prefixes =E2=80=94 kept to minimize diff chu= rn + * 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 =3D 1, + STR_AMT_PRODUCT, + STR_AMT_SERIAL, + STR_AMT_INTERFACE, +}; + +static const USBDescStrings desc_strings_amt =3D { + [STR_AMT_MFR] =3D "Apple Inc.", + [STR_AMT_PRODUCT] =3D "Apple Mighty Mouse", + [STR_AMT_SERIAL] =3D "CC2916600VBJ2XQA5", + [STR_AMT_INTERFACE] =3D "Mouse", +}; + +/* + * apple-mighty-mouse device =E2=80=94 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 =E2=80=94 that path needs deep RE= we don't + * have ground-truth wire bytes for. Instead, present as **Apple Mighty + * Mouse (PID 0x0304)** =E2=80=94 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) =E2=80=94 no proprietary 1387-byte Repor= t 0x44. + * - Single USB interface =E2=80=94 no SET_REPORT-gated multitouch enabl= e path. + * - bSubClass=3D1 bProto=3D2 (boot mouse) =E2=80=94 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 =E2=80=94 5-byte report (no R= eport 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[] =3D { + 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) =E2=80=94 3 butt= on bits */ + 0x95, 0x01, /* Report Count (1) */ + 0x75, 0x05, /* Report Size (5) */ + 0x81, 0x03, /* Input (Const, Var, Abs) =E2=80=94 5 pad= ding bits */ + 0x05, 0x01, /* Usage Page (Generic Desktop) */ + 0x09, 0x30, /* Usage (X) */ + 0x09, 0x31, /* Usage (Y) */ + 0x09, 0x38, /* Usage (Wheel =E2=80=94 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) =E2=80=94 X, Y, = Wheel */ + 0x05, 0x0c, /* Usage Page (Consumer) */ + 0x0a, 0x38, 0x02, /* Usage (AC Pan =E2=80=94 horizontal scro= ll) */ + 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) =E2=80=94 HWheel= */ + 0xc0, /* End Collection (Physical) */ + 0xc0, /* End Collection (Application) */ +}; + +static const USBDescIface desc_iface_apple_magic_tablet =3D { + .bInterfaceNumber =3D 0, + .bNumEndpoints =3D 1, + .bInterfaceClass =3D USB_CLASS_HID, + .bInterfaceSubClass =3D 0x01, /* Boot subclass */ + .bInterfaceProtocol =3D 0x02, /* Mouse protocol */ + .iInterface =3D STR_AMT_INTERFACE, + .ndesc =3D 1, + .descs =3D (USBDescOther[]) { + { + .data =3D (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 =3D (USBDescEndpoint[]) { + { + .bEndpointAddress =3D USB_DIR_IN | 0x01, + .bmAttributes =3D USB_ENDPOINT_XFER_INT, + .wMaxPacketSize =3D 8, + .bInterval =3D 8, + }, + }, +}; + +static const USBDescDevice desc_device_apple_magic_tablet =3D { + .bcdUSB =3D 0x0200, + .bMaxPacketSize0 =3D 64, + .bNumConfigurations =3D 1, + .confs =3D (USBDescConfig[]) { + { + .bNumInterfaces =3D 1, + .bConfigurationValue =3D 1, + .iConfiguration =3D STR_AMT_PRODUCT, + .bmAttributes =3D USB_CFG_ATT_ONE, + .bMaxPower =3D 50, /* 100 mA =E2=80=94 Mighty M= ouse is wired */ + .nif =3D 1, + .ifs =3D &desc_iface_apple_magic_tablet, + }, + }, +}; + +static const USBDesc desc_apple_magic_tablet =3D { + .id =3D { + .idVendor =3D 0x05ac, /* Apple Inc. */ + .idProduct =3D 0x0304, /* Apple Mighty Mouse (M9087) */ + .bcdDevice =3D 0x0150, /* fw rev 1.5 =E2=80=94 looks pl= ausible */ + .iManufacturer =3D STR_AMT_MFR, + .iProduct =3D STR_AMT_PRODUCT, + .iSerialNumber =3D STR_AMT_SERIAL, + }, + /* + * Real Magic Trackpad runs at USB full-speed (12 Mb/s) despite + * declaring bcdUSB=3D0x0200 =E2=80=94 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 =3D &desc_device_apple_magic_tablet, + .high =3D &desc_device_apple_magic_tablet, + .str =3D 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 =E2=80=94 bum= ping + * 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=E2=86=92REL conversion state (VNC/SPICE/SDL deliver ABS). -1 = =3D 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 =E2=80=94 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 =E2=80=94 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_TABLE= T) + +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 =3D=3D s->q_tail; +} + +static inline bool amt_q_full(USBAppleMagicTabletState *s) +{ + return amt_q_count(s) =3D=3D AMT_QUEUE_DEPTH - 1; +} + +static void amt_enqueue(USBAppleMagicTabletState *s, const uint8_t *data) +{ + if (amt_q_full(s)) { + /* Queue full =E2=80=94 drop oldest. Latest motion matters more th= an stale. */ + s->q_tail =3D (s->q_tail + 1) & (AMT_QUEUE_DEPTH - 1); + } + USBAppleMagicTabletReport *r =3D &s->queue[s->q_head]; + memcpy(r->data, data, AMT_REPORT_LEN); + s->q_head =3D (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 =3D left, 1 =3D right, 2 =3D 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 =3D amt_clamp_i8(s->pending_dx); + int8_t dy =3D amt_clamp_i8(s->pending_dy); + int8_t wheel =3D amt_clamp_i8(s->pending_wheel); + int8_t hwheel =3D amt_clamp_i8(s->pending_hwheel); + + s->pending_dx -=3D dx; + s->pending_dy -=3D dy; + s->pending_wheel -=3D wheel; + s->pending_hwheel -=3D hwheel; + + buf[0] =3D (s->button_left ? 0x01 : 0x00) | + (s->button_right ? 0x02 : 0x00) | + (s->button_middle ? 0x04 : 0x00); + buf[1] =3D (uint8_t)dx; + buf[2] =3D (uint8_t)dy; + buf[3] =3D (uint8_t)wheel; + buf[4] =3D (uint8_t)hwheel; + + amt_enqueue(s, buf); + usb_wakeup(s->intr, 0); +} + +/* QemuInputHandler.event =E2=80=94 accumulate per-event state. */ +static void amt_input_event(DeviceState *dev, QemuConsole *src, + InputEvent *evt) +{ + USBAppleMagicTabletState *s =3D USB_APPLE_MAGIC_TABLET(dev); + + switch (evt->type) { + case INPUT_EVENT_KIND_REL: { + InputMoveEvent *move =3D evt->u.rel.data; + if (move->axis =3D=3D INPUT_AXIS_X) { + s->pending_dx +=3D move->value; + } else if (move->axis =3D=3D INPUT_AXIS_Y) { + s->pending_dy +=3D move->value; + } + s->pending_event =3D 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 =E2=86=92 0..1920/1080 px. */ + InputMoveEvent *move =3D evt->u.abs.data; + if (move->axis =3D=3D INPUT_AXIS_X) { + int32_t scaled =3D (move->value * 1920) / 0x7fff; + if (s->last_abs_x >=3D 0) { + s->pending_dx +=3D scaled - s->last_abs_x; + } + s->last_abs_x =3D scaled; + } else if (move->axis =3D=3D INPUT_AXIS_Y) { + int32_t scaled =3D (move->value * 1080) / 0x7fff; + if (s->last_abs_y >=3D 0) { + s->pending_dy +=3D scaled - s->last_abs_y; + } + s->last_abs_y =3D scaled; + } + s->pending_event =3D true; + break; + } + case INPUT_EVENT_KIND_BTN: { + InputBtnEvent *btn =3D evt->u.btn.data; + switch (btn->button) { + case INPUT_BUTTON_LEFT: + s->button_left =3D btn->down; + s->pending_event =3D true; + break; + case INPUT_BUTTON_RIGHT: + s->button_right =3D btn->down; + s->pending_event =3D true; + break; + case INPUT_BUTTON_MIDDLE: + s->button_middle =3D btn->down; + s->pending_event =3D true; + break; + case INPUT_BUTTON_WHEEL_UP: + if (btn->down) { + s->pending_wheel +=3D 1; + s->pending_event =3D true; + } + break; + case INPUT_BUTTON_WHEEL_DOWN: + if (btn->down) { + s->pending_wheel -=3D 1; + s->pending_event =3D true; + } + break; + case INPUT_BUTTON_WHEEL_LEFT: + if (btn->down) { + s->pending_hwheel -=3D 1; + s->pending_event =3D true; + } + break; + case INPUT_BUTTON_WHEEL_RIGHT: + if (btn->down) { + s->pending_hwheel +=3D 1; + s->pending_event =3D true; + } + break; + default: + break; + } + break; + } + default: + break; + } +} + +/* QemuInputHandler.sync =E2=80=94 drain pending_dx/dy in 3-byte boot-mous= e reports. + * + * Each report's dx/dy is clamped to int8 (=C2=B1127). 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 =3D USB_APPLE_MAGIC_TABLET(dev); + + if (!s->pending_event) { + return; + } + s->pending_event =3D false; + + /* Always emit at least once (catches button-only changes with no moti= on). */ + int loops =3D 0; + do { + amt_emit_boot_mouse(s); + loops++; + } while ((s->pending_dx !=3D 0 || s->pending_dy !=3D 0 || + s->pending_wheel !=3D 0 || s->pending_hwheel !=3D 0) && loop= s < 64); +} + +static QemuInputHandler amt_input_handler =3D { + .name =3D "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 =3D INPUT_EVENT_MASK_REL | INPUT_EVENT_MASK_ABS | + INPUT_EVENT_MASK_BTN, + .event =3D amt_input_event, + .sync =3D amt_input_sync, +}; + +static void usb_apple_magic_tablet_realize(USBDevice *dev, Error **errp) +{ + USBAppleMagicTabletState *s =3D USB_APPLE_MAGIC_TABLET(dev); + + usb_desc_create_serial(dev); + usb_desc_init(dev); + s->intr =3D usb_ep_get(dev, USB_TOKEN_IN, 1); + + s->q_head =3D s->q_tail =3D 0; + s->pending_dx =3D s->pending_dy =3D 0; + s->pending_wheel =3D s->pending_hwheel =3D 0; + s->button_left =3D s->button_right =3D s->button_middle =3D false; + s->last_abs_x =3D -1; + s->last_abs_y =3D -1; + s->pending_event =3D false; + + s->input_handler =3D 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 =3D USB_APPLE_MAGIC_TABLET(dev); + + if (s->input_handler) { + qemu_input_handler_unregister(s->input_handler); + s->input_handler =3D NULL; + } +} + +static void usb_apple_magic_tablet_handle_reset(USBDevice *dev) +{ + USBAppleMagicTabletState *s =3D USB_APPLE_MAGIC_TABLET(dev); + + s->q_head =3D s->q_tail =3D 0; + s->pending_dx =3D s->pending_dy =3D 0; + s->pending_wheel =3D s->pending_hwheel =3D 0; + s->button_left =3D s->button_right =3D s->button_middle =3D false; + s->pending_event =3D false; + s->last_abs_x =3D -1; + s->last_abs_y =3D -1; +} + +static void usb_apple_magic_tablet_handle_control(USBDevice *dev, USBPacke= t *p, + int request, int value, + int index, int length, + uint8_t *data) +{ + int ret; + + ret =3D usb_desc_handle_control(dev, p, request, value, index, length,= data); + if (ret >=3D 0) { + return; + } + + switch (request) { + case InterfaceRequest | USB_REQ_GET_DESCRIPTOR: + if ((value >> 8) =3D=3D 0x22) { + uint16_t rd_len =3D sizeof(amt_boot_mouse_report_desc); + uint16_t copy =3D length < rd_len ? length : rd_len; + memcpy(data, amt_boot_mouse_report_desc, copy); + p->actual_length =3D copy; + return; + } + break; + case HID_GET_IDLE: + data[0] =3D 0; + p->actual_length =3D 1; + return; + case HID_SET_IDLE: + return; + case HID_GET_PROTOCOL: + data[0] =3D 1; /* report protocol */ + p->actual_length =3D 1; + return; + case HID_SET_PROTOCOL: + return; + case HID_GET_REPORT: { + uint16_t reply_len =3D (length > 0 && length <=3D 64) ? length : 1; + if (reply_len > length) { + reply_len =3D length; + } + memset(data, 0, reply_len); + p->actual_length =3D reply_len; + return; + } + case HID_SET_REPORT: + /* Silently accept any host-pushed report (LED state etc.) */ + p->actual_length =3D length; + return; + } + + p->status =3D USB_RET_STALL; +} + +static void usb_apple_magic_tablet_handle_data(USBDevice *dev, USBPacket *= p) +{ + USBAppleMagicTabletState *s =3D USB_APPLE_MAGIC_TABLET(dev); + + if (p->pid !=3D USB_TOKEN_IN || p->ep->nr !=3D 1) { + p->status =3D USB_RET_STALL; + return; + } + if (amt_q_empty(s)) { + p->status =3D USB_RET_NAK; + return; + } + + USBAppleMagicTabletReport *r =3D &s->queue[s->q_tail]; + s->q_tail =3D (s->q_tail + 1) & (AMT_QUEUE_DEPTH - 1); + usb_packet_copy(p, r->data, AMT_REPORT_LEN); +} + +static const VMStateDescription vmstate_apple_magic_tablet =3D { + .name =3D "apple-mighty-mouse", + .unmigratable =3D 1, +}; + +static void usb_apple_magic_tablet_class_initfn(ObjectClass *klass, + const void *data) +{ + DeviceClass *dc =3D DEVICE_CLASS(klass); + USBDeviceClass *uc =3D USB_DEVICE_CLASS(klass); + + uc->realize =3D usb_apple_magic_tablet_realize; + uc->product_desc =3D "Apple Mighty Mouse"; + uc->usb_desc =3D &desc_apple_magic_tablet; + uc->handle_reset =3D usb_apple_magic_tablet_handle_reset; + uc->handle_control =3D usb_apple_magic_tablet_handle_control; + uc->handle_data =3D usb_apple_magic_tablet_handle_data; + uc->unrealize =3D usb_apple_magic_tablet_unrealize; + set_bit(DEVICE_CATEGORY_INPUT, dc->categories); + dc->desc =3D "Apple Mighty Mouse (USB HID boot mouse, " + "VID 0x05ac PID 0x0304)"; + dc->vmsd =3D &vmstate_apple_magic_tablet; +} + +static const TypeInfo usb_apple_magic_tablet_info =3D { + .name =3D TYPE_USB_APPLE_MAGIC_TABLET, + .parent =3D TYPE_USB_DEVICE, + .instance_size =3D sizeof(USBAppleMagicTabletState), + .class_init =3D 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 =E2=80=94 no code duplication. Drop this a= lias + * 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 =3D { + .name =3D TYPE_USB_APPLE_MAGIC_TABLET_LEGACY, + .parent =3D 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); } =20 type_init(usb_hid_register_types) --=20 2.50.1 (Apple Git-155)