From d3ade65062e4346ded074de3e9e1a064e5e06d8e Mon Sep 17 00:00:00 2001 From: Adrien Bertrand Date: Wed, 12 Aug 2026 22:32:33 +0200 Subject: [PATCH 01/11] core: update battery comparator during USB power checks. TI-OS reuses the battery comparator configuration after a normal battery-level read, then selects a high threshold through control ports 0 and 9 before checking USB power safety. Model that continuous comparator behavior even when port 7 is not rewritten. Without this update, a successful TI-Innovator bulk write is followed by TI-OS event 0xBD, which surfaces as ERROR: BREAK. With it, Send("VERSION") and Get(Str0) complete correctly. --- core/control.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/control.c b/core/control.c index ad950687..9b520d71 100644 --- a/core/control.c +++ b/core/control.c @@ -147,6 +147,15 @@ static void control_write(const uint16_t pio, const uint8_t byte, bool poke) { control.readBatteryStatus = control.setBatteryStatus == BATTERY_DISCHARGED ? 0 : byte & 0x80 ? 0 : 3; break; } + /* + * The comparator inputs remain enabled after the battery-level sequence configures port 7. + * TI-OS's USB power check reuses that configuration and only selects the high threshold through + * ports 0 and 9, so update the comparator even without another port 7 write. + */ + if (!control.readBatteryStatus && control.setBatteryStatus != BATTERY_DISCHARGED && + (control.ports[7] & 0x90) && (control.ports[0] & 0x80) && (byte & 0xA0) == 0xA0) { + control.readBatteryStatus = 1; + } if ((control.ports[index] & ~byte) >> 2 & 1) { panel_hw_reset(); } From 577fbd5e69bc23362d2956cd65d4a1dd4374b209 Mon Sep 17 00:00:00 2001 From: Adrien Bertrand Date: Wed, 12 Aug 2026 15:39:04 +0200 Subject: [PATCH 02/11] core: usb: fix host schedule budgets and timings. Schedule USB frames at the controller's 12 MHz clock rate: 1 ms for full/low speed and 125 us for high speed. It was around 1500 times too quickly before. Budgets were swapped for full vs high. --- core/usb/usb.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/usb/usb.c b/core/usb/usb.c index ab31f979..a223c18c 100644 --- a/core/usb/usb.c +++ b/core/usb/usb.c @@ -878,15 +878,16 @@ static void usb_schedule_traverse(usb_traversal_state_t *state) { state->fake_recl_head = -1; state->fake_iter_cap = 1 << 10; usb.regs.hcor.usbsts |= USBSTS_RECLAMATION; + /* Budget one 1 ms frame, or one 125 us microframe at high speed. */ switch (usb.event.speed) { case USB_FULL_SPEED: - state->bit_times_remaining = UINT64_C(480) * 1000u * 1000u / 8000u; + state->bit_times_remaining = UINT64_C(12) * 1000u * 1000u / 1000u; break; case USB_LOW_SPEED: state->bit_times_remaining = UINT64_C(1500) * 1000u / 1000u; break; case USB_HIGH_SPEED: - state->bit_times_remaining = UINT64_C(12) * 1000u * 1000u / 1000u; + state->bit_times_remaining = UINT64_C(480) * 1000u * 1000u / 8000u; break; case USB_SUPER_SPEED: state->bit_times_remaining = UINT64_C(5) * 1000u * 1000u * 1000u / 8000u; @@ -1103,7 +1104,7 @@ static void usb_event(enum sched_item_id event) { } } } - sched_repeat(event, high_speed ? 1 : 8); + sched_repeat(event, high_speed ? 1500 : 12000); } static void usb_device_event(enum sched_item_id event) { From 5c93ed65b90ebbb1b62fc1c75810d7d65eb59eae Mon Sep 17 00:00:00 2001 From: Adrien Bertrand Date: Wed, 12 Aug 2026 16:10:55 +0200 Subject: [PATCH 03/11] core: usb: fix completion state. - Make device-mode DMA START self-clear when a response completes, including zero-length status stages - Only enable a host port after reset has actually completed. --- core/usb/usb.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/core/usb/usb.c b/core/usb/usb.c index a223c18c..6fc7ccf5 100644 --- a/core/usb/usb.c +++ b/core/usb/usb.c @@ -556,6 +556,7 @@ static int usb_dispatch_event(usb_traversal_state_t *state) { mem_dma_write(transfer->buffer, usb.regs.dma_addr, write_length); } } + usb.regs.dma_ctrl &= ~DMACTRL_START; if (transfer->length) { //gui_console_printf("usb_grp2_int(%s);\n", transfer->status == USB_TRANSFER_COMPLETED ? "GISR2_DMAFIN" : "GISR2_DMAERR"); usb_grp2_int(transfer->status == USB_TRANSFER_COMPLETED ? GISR2_DMAFIN : GISR2_DMAERR); @@ -1186,10 +1187,11 @@ static void usb_write(uint16_t pio, uint8_t value, bool poke) { PORTSC_EN_STATUS; // W[0/1]C mask (V or RO or W) usb.regs.hcor.portsc[0] |= (uint32_t)value << bit_offset & 0x7F0180; // W mask (RO) if ((old ^ usb.regs.hcor.portsc[0]) & PORTSC_RESET) { - // TODO: actually powered by gpio. - usb.event.type = old & PORTSC_RESET ? USB_RESET_EVENT : USB_POWER_EVENT; + // TODO: actually powered by gpio? + bool reset_completed = old & PORTSC_RESET; + usb.event.type = reset_completed ? USB_RESET_EVENT : USB_POWER_EVENT; usb_dispatch_event(NULL); - if (usb_update_status_change(usb.regs.hcor.portsc, PORTSC_EN_STATUS, PORTSC_EN_CHANGE, true)) { + if (usb_update_status_change(usb.regs.hcor.portsc, PORTSC_EN_STATUS, PORTSC_EN_CHANGE, reset_completed)) { usb_host_int(USBSTS_PORT_CHANGE); } usb.regs.otgcsr |= OTGCSR_A_VBUS_VLD | OTGCSR_A_SESS_VLD | OTGCSR_B_SESS_VLD; From c9ab739a233c85f7b3ad950e45128416d0ff08f3 Mon Sep 17 00:00:00 2001 From: Adrien Bertrand Date: Wed, 12 Aug 2026 15:42:38 +0200 Subject: [PATCH 04/11] core: usb: preserve host schedule state for pending transfers. A physical libusb request may remain submitted across multiple emulated frames. Advancing its iTD, siTD, or queue-head state before the response arrives loses the original transaction and can stall the async schedule or complete the wrong descriptor. Let backends mark a request pending, snapshot each schedule object before dispatch, and restore it while still charging the frame budget. The next frame retries the same transaction without monopolizing the rest of the schedule, and the response advances it exactly once. --- core/usb/device.h | 1 + core/usb/physical.c | 5 +++++ core/usb/usb.c | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/core/usb/device.h b/core/usb/device.h index 20b64902..180abc29 100644 --- a/core/usb/device.h +++ b/core/usb/device.h @@ -72,6 +72,7 @@ typedef struct usb_event { usb_progress_handler_t *progress_handler; void *progress_context, *context; bool host : 1; + bool pending : 1; uint8_t speed : 2; /* usb_speed_t */ usb_event_type_t type; union { diff --git a/core/usb/physical.c b/core/usb/physical.c index 876a9402..4441764a 100644 --- a/core/usb/physical.c +++ b/core/usb/physical.c @@ -1295,12 +1295,17 @@ int usb_physical_device(usb_event_t *event) { } break; case USB_TRANSFER_REQUEST_EVENT: + event->pending = false; NODE_FOREACH (device, &context->devices) { if (device->state <= DEVICE_STATE_POWERED || device->address != transfer->address) { continue; } + event->pending = true; error = device_process_transfer(context, device, event); + if (error != USB_SUCCESS || event->type == USB_TRANSFER_RESPONSE_EVENT) { + event->pending = false; + } break; } break; diff --git a/core/usb/usb.c b/core/usb/usb.c index 6fc7ccf5..8d1905dc 100644 --- a/core/usb/usb.c +++ b/core/usb/usb.c @@ -731,6 +731,8 @@ static void usb_itd_execute(usb_traversal_state_t *state) { for (uint8_t iTDTransactionCounter = state->itd.bufs[2].mult; iTDTransactionCounter && xact->active && state->bit_times_remaining >= max_bit_times; --iTDTransactionCounter) { + usb_itd_t itd_before = state->itd; + bool dirty_before = state->dirty; state->dirty = true; uint32_t length = sizeof(usb.buffer); if (usb_itd_gather(usb.buffer, &state->itd, xact, &length)) { @@ -749,7 +751,16 @@ static void usb_itd_execute(usb_traversal_state_t *state) { if (!usb.event.info.transfer.direction) { state->bit_times_remaining -= max_bit_times; } + usb.event.pending = false; usb_dispatch_event(state); + if (usb.event.pending) { + state->itd = itd_before; + state->dirty = dirty_before; + if (usb.event.info.transfer.direction) { + state->bit_times_remaining -= max_bit_times; + } + return; + } } } @@ -761,6 +772,8 @@ static void usb_sitd_execute(usb_traversal_state_t *state) { if (state->bit_times_remaining < max_bit_times) { return; } + usb_sitd_t sitd_before = state->sitd; + bool dirty_before = state->dirty; state->dirty = true; uint32_t length = sizeof(usb.buffer); if (usb_sitd_gather(usb.buffer, &state->sitd, &length)) { @@ -779,7 +792,15 @@ static void usb_sitd_execute(usb_traversal_state_t *state) { if (!usb.event.info.transfer.direction) { state->bit_times_remaining -= max_bit_times; } + usb.event.pending = false; usb_dispatch_event(state); + if (usb.event.pending) { + state->sitd = sitd_before; + state->dirty = dirty_before; + if (usb.event.info.transfer.direction) { + state->bit_times_remaining -= max_bit_times; + } + } } static void usb_qh_execute(usb_traversal_state_t *state) { @@ -806,6 +827,8 @@ static void usb_qh_execute(usb_traversal_state_t *state) { for (uint8_t qHTransactionCounter = state->qh.mult; qHTransactionCounter && state->qh.overlay.active && state->bit_times_remaining >= max_bit_times; --qHTransactionCounter) { + usb_qh_t qh_before = state->qh; + bool dirty_before = state->dirty; // Asynchronous Transfer Pre-condition Criteria if (state->qh.nak_rl) { if (!state->qh.s_mask && !state->qh.overlay.alt.nak_cnt) { @@ -836,7 +859,16 @@ static void usb_qh_execute(usb_traversal_state_t *state) { if (!usb.event.info.transfer.direction) { state->bit_times_remaining -= usb_compute_packet_bit_times(usb.event.info.transfer.length); } + usb.event.pending = false; usb_dispatch_event(state); + if (usb.event.pending) { + state->qh = qh_before; + state->dirty = dirty_before; + if (usb.event.info.transfer.direction) { + state->bit_times_remaining -= max_bit_times; + } + return; + } } } From 254e297e580cb5230d80f4691179b168cc339e53 Mon Sep 17 00:00:00 2001 From: Adrien Bertrand Date: Wed, 12 Aug 2026 15:44:12 +0200 Subject: [PATCH 05/11] core: usb: model USB OTG session transitions. Drive VBUS, session-valid, port connection, and enable state from A_BUSREQ/A_BUSDROP instead of treating the port as permanently present. Fix A-side unplugging so both connection and enable changes are reported and the B side is actually removed. Introduce explicit session start/end/request events so controller state can be propagated without advancing backend protocol machines. MSD and physical devices accept the notifications, while DUSB returns its session-start response directly. --- core/usb/device.h | 3 +++ core/usb/dusb.c | 3 +++ core/usb/msd.c | 3 +++ core/usb/physical.c | 9 ++++++++ core/usb/usb.c | 52 +++++++++++++++++++++++++++++++++++++++------ 5 files changed, 64 insertions(+), 6 deletions(-) diff --git a/core/usb/device.h b/core/usb/device.h index 180abc29..d2db4d5f 100644 --- a/core/usb/device.h +++ b/core/usb/device.h @@ -12,6 +12,9 @@ typedef enum usb_event_type { USB_INIT_EVENT, USB_POWER_EVENT, USB_RESET_EVENT, + USB_SESSION_START_EVENT, + USB_SESSION_END_EVENT, + USB_SESSION_REQUEST_EVENT, USB_TRANSFER_REQUEST_EVENT, USB_TRANSFER_RESPONSE_EVENT, USB_TIMER_EVENT, diff --git a/core/usb/dusb.c b/core/usb/dusb.c index cd104669..718a2f29 100644 --- a/core/usb/dusb.c +++ b/core/usb/dusb.c @@ -1247,6 +1247,9 @@ int usb_dusb_device(usb_event_t *event) { } event->context = context; break; + case USB_SESSION_REQUEST_EVENT: + event->type = USB_SESSION_START_EVENT; + return USB_SUCCESS; case USB_TRANSFER_REQUEST_EVENT: case USB_TRANSFER_RESPONSE_EVENT: command = context->command; diff --git a/core/usb/msd.c b/core/usb/msd.c index 1f84c569..1c1c2f77 100644 --- a/core/usb/msd.c +++ b/core/usb/msd.c @@ -87,6 +87,9 @@ int usb_msd_device(usb_event_t *event) { context->address = 0; context->configured = false; break; + case USB_SESSION_START_EVENT: + case USB_SESSION_END_EVENT: + break; case USB_TRANSFER_REQUEST_EVENT: event->type = USB_TRANSFER_RESPONSE_EVENT; if (transfer->address != context->address || diff --git a/core/usb/physical.c b/core/usb/physical.c index 4441764a..fe5d838d 100644 --- a/core/usb/physical.c +++ b/core/usb/physical.c @@ -1294,6 +1294,15 @@ int usb_physical_device(usb_event_t *event) { device = NULL; } break; + case USB_SESSION_START_EVENT: + case USB_SESSION_END_EVENT: + /* + * Session/VBUS state belongs to the emulated OTG controller. + * A physical libusb device has no additional operation to perform + * here; accepting the notification keeps it attached so the + * following bus reset and enumeration can reach the device. + */ + break; case USB_TRANSFER_REQUEST_EVENT: event->pending = false; NODE_FOREACH (device, &context->devices) { diff --git a/core/usb/usb.c b/core/usb/usb.c index 8d1905dc..c6036d0c 100644 --- a/core/usb/usb.c +++ b/core/usb/usb.c @@ -215,14 +215,16 @@ static void usb_plug_a(void) { usb_otg_int(OTGISR_IDCHG | OTGISR_RLCHG); } static void usb_unplug_a(void) { + bool port_changed; usb.regs.hcor.portsc[0] &= ~PORTSC_J_STATE; - if (usb_update_status_change(usb.regs.hcor.portsc, PORTSC_CONN_STATUS, PORTSC_CONN_CHANGE, false) || - usb_update_status_change(usb.regs.hcor.portsc, PORTSC_EN_STATUS, PORTSC_EN_CHANGE, false)) { + port_changed = usb_update_status_change(usb.regs.hcor.portsc, PORTSC_CONN_STATUS, PORTSC_CONN_CHANGE, false); + port_changed |= usb_update_status_change(usb.regs.hcor.portsc, PORTSC_EN_STATUS, PORTSC_EN_CHANGE, false); + if (port_changed) { usb_host_int(USBSTS_PORT_CHANGE); } usb.regs.otgcsr |= OTGCSR_DEV_B | OTGCSR_ROLE_D; usb_otg_int(OTGISR_APRM | OTGISR_IDCHG | OTGISR_RLCHG); - usb_plug_b(); + usb_unplug_b(); } static void usb_plug(void) { @@ -503,6 +505,18 @@ static int usb_dispatch_event(usb_traversal_state_t *state) { case USB_RESET_EVENT: usb_grp2_int(GISR2_RESET); break; + case USB_SESSION_START_EVENT: + if (usb.event.host && (usb.regs.otgcsr & OTGCSR_ROLE_D)) { + usb_plug_b(); + } + break; + case USB_SESSION_END_EVENT: + if (usb.event.host && (usb.regs.otgcsr & OTGCSR_ROLE_D)) { + usb_unplug_b(); + } + break; + case USB_SESSION_REQUEST_EVENT: + break; case USB_TRANSFER_REQUEST_EVENT: if (usb.event.host) { uint8_t endpoint = transfer->endpoint; @@ -1258,10 +1272,36 @@ static void usb_write(uint16_t pio, uint8_t value, bool poke) { write8(usb.regs.rsvd2[1], bit_offset, value & 0xFFF >> bit_offset); // W mask (V) break; case 0x080 >> 2: // OTG Control Status Register + old = usb.regs.otgcsr; write8(usb.regs.otgcsr, bit_offset, value & 0x1A00FFF7 >> bit_offset); // W mask (V) - if (usb_update_status_change(usb.regs.hcor.portsc, PORTSC_J_STATE | PORTSC_CONN_STATUS, PORTSC_CONN_CHANGE, - (usb.regs.otgcsr & (OTGCSR_A_BUSDROP | OTGCSR_A_BUSREQ)) == OTGCSR_A_BUSREQ)) { - usb_host_int(USBSTS_PORT_CHANGE); + if (usb.device != usb_disconnected_device && !usb.event.host && + !(usb.regs.otgcsr & OTGCSR_ROLE_D)) { + bool bus_requested = (usb.regs.otgcsr & (OTGCSR_A_BUSDROP | OTGCSR_A_BUSREQ)) == OTGCSR_A_BUSREQ; + if (bus_requested) { + usb.regs.otgcsr |= OTGCSR_A_VBUS_VLD | OTGCSR_A_SESS_VLD | OTGCSR_B_SESS_VLD; + usb.regs.otgcsr &= ~OTGCSR_B_SESS_END; + } else { + usb.regs.otgcsr &= ~(OTGCSR_A_VBUS_VLD | OTGCSR_A_SESS_VLD | OTGCSR_B_SESS_VLD); + usb.regs.otgcsr |= OTGCSR_B_SESS_END; + } + bool port_changed = usb_update_status_change( + usb.regs.hcor.portsc, PORTSC_J_STATE | PORTSC_CONN_STATUS, + PORTSC_CONN_CHANGE, bus_requested); + if (!bus_requested) { + port_changed |= usb_update_status_change(usb.regs.hcor.portsc, PORTSC_EN_STATUS, PORTSC_EN_CHANGE, false); + } + if (port_changed) { + usb_host_int(USBSTS_PORT_CHANGE); + } + if (((old & (OTGCSR_A_BUSDROP | OTGCSR_A_BUSREQ)) == OTGCSR_A_BUSREQ) != bus_requested) { + usb.event.type = bus_requested ? USB_SESSION_START_EVENT : USB_SESSION_END_EVENT; + usb_dispatch_event(NULL); + } + } else if (usb.device != usb_disconnected_device && usb.event.host && + (usb.regs.otgcsr & OTGCSR_ROLE_D) && !(old & OTGCSR_B_BUSREQ) && + (usb.regs.otgcsr & OTGCSR_B_BUSREQ)) { + usb.event.type = USB_SESSION_REQUEST_EVENT; + usb_dispatch_event(NULL); } break; case 0x084 >> 2: // OTG Interrupt Status Register From cf6b7d047927833f8a271dcaf2e967ad79850ceb Mon Sep 17 00:00:00 2001 From: Adrien Bertrand Date: Wed, 12 Aug 2026 15:44:19 +0200 Subject: [PATCH 06/11] core: usb: harden USB device lifecycle. usb_plug_device: do checks before a teardown and use usb_disconnected_device more correctly. Track claimed and detached interfaces explicitly, so teardown releases only interfaces CEmu owns and reattaches kernel drivers even after the device no longer has an active configuration descriptor. Defer DEVICE_LEFT cleanup until libusb event dispatch returns, tear down children before parents, and identify reset replacements by bus plus port path. Rebuild hub descendants parent-first, discard stale pending entries, restore hotplug registration after replacement, and stop retrying permanent open failures. --- core/usb/physical.c | 323 ++++++++++++++++++++++++++++++++------------ core/usb/usb.c | 26 ++-- 2 files changed, 253 insertions(+), 96 deletions(-) diff --git a/core/usb/physical.c b/core/usb/physical.c index fe5d838d..faf6dfff 100644 --- a/core/usb/physical.c +++ b/core/usb/physical.c @@ -127,6 +127,8 @@ struct device { endpoint_t endpoints[0x20]; uint8_t state : 2; /* enum device_state */ uint8_t address : 7, numPorts : 7; + bool disconnected; + uint32_t claimedInterfaces, detachedInterfaces; hub_t hub; }; @@ -134,7 +136,7 @@ struct context { libusb_context *context; node_t pending; node_t devices; - bool handled : 1; + bool handled : 1, hotplug_registered : 1; uint16_t throttle : 15; }; @@ -233,6 +235,12 @@ static void endpoint_init(endpoint_t *endpoint) { } static void device_attach(context_t *context, struct libusb_device *libusb_device) { + pending_t *queued; + NODE_FOREACH(queued, &context->pending) { + if (queued->device == libusb_device) { + return; + } + } pending_t *pending = malloc(sizeof(pending_t)); if (!pending) { return; @@ -245,7 +253,7 @@ static void device_attach(context_t *context, struct libusb_device *libusb_devic static int device_init(context_t *context, struct libusb_device *libusb_device) { struct libusb_device_descriptor dev_desc; int error = errno_from_libusb_error(libusb_get_device_descriptor(libusb_device, &dev_desc)); - libusb_device_handle *handle; + libusb_device_handle *handle = NULL; if (error == USB_SUCCESS) { error = errno_from_libusb_error(libusb_open(libusb_device, &handle)); } @@ -274,6 +282,7 @@ static int device_init(context_t *context, struct libusb_device *libusb_device) } device_t *device = malloc(size); if (!device) { + libusb_close(handle); return ENOMEM; } node_init(&device->node); @@ -284,6 +293,9 @@ static int device_init(context_t *context, struct libusb_device *libusb_device) device->state = DEVICE_STATE_ATTACHED; device->address = 0; device->numPorts = hub_desc.bNbrPorts; + device->disconnected = false; + device->claimedInterfaces = 0; + device->detachedInterfaces = 0; if (device->numPorts) { device->hub.statusChange.value = 0; for (uint8_t portIdx = 0; portIdx != device->numPorts; ++portIdx) { @@ -331,48 +343,68 @@ static void endpoint_cleanup(context_t *context, endpoint_t *endpoint) { transfer_cleanup(context, &endpoint->transfer); } +static int LIBUSB_CALL device_hotplugged( + libusb_context *libusb_context, struct libusb_device *libusb_device, + libusb_hotplug_event event, void *user_data); + +static bool device_register_hotplug(context_t *context) { + if (context->hotplug_registered + || !libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) { + return true; + } + if (libusb_hotplug_register_callback( + context->context, + LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED | + LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT, + LIBUSB_HOTPLUG_NO_FLAGS, + LIBUSB_HOTPLUG_MATCH_ANY, + LIBUSB_HOTPLUG_MATCH_ANY, + LIBUSB_HOTPLUG_MATCH_ANY, + device_hotplugged, context, NULL) != LIBUSB_SUCCESS) { + return false; + } + context->hotplug_registered = true; + return true; +} + static device_t *device_detach(context_t *context, device_t *device) { if (device) { for (uint8_t index = 0; index != 0x20; ++index) { endpoint_cleanup(context, &device->endpoints[index]); } - libusb_device_handle *handle = device->handle; - if (handle) { - struct libusb_device *libusb_device = libusb_get_device(handle), - *libusb_parent = libusb_get_parent(libusb_device); - device_t *parent; - NODE_FOREACH(parent, &context->devices) { - if (parent->handle && libusb_get_device(parent->handle) == libusb_parent) { - uint8_t portNum = libusb_get_port_number(libusb_device); - if (0 < portNum && portNum <= parent->numPorts) { - port_t *port = &parent->hub.ports[portNum - 1]; - port->device = NULL; - if (port->status.power) { - UPDATE_STATUS_CHANGE(port, connection, false); - port->status.enable = false; - } - } - break; + /* + * Unlink from the hub port by identity rather than by asking libusb + * who our parent is: device_reset() swaps a reacquired hub's handle + * for one referring to a new libusb_device, so a stale child's + * libusb_get_parent() no longer compares equal to its hub's handle + * and the port would keep pointing at the device we are about to + * free. + */ + device_t *parent; + NODE_FOREACH(parent, &context->devices) { + for (uint8_t portIdx = 0; portIdx != parent->numPorts; ++portIdx) { + port_t *port = &parent->hub.ports[portIdx]; + if (port->device != device) { + continue; + } + port->device = NULL; + if (port->status.power) { + UPDATE_STATUS_CHANGE(port, connection, false); + port->status.enable = false; } } - if (!device->numPorts) { - struct libusb_config_descriptor *config_desc; - if (libusb_get_active_config_descriptor(libusb_device, &config_desc) == LIBUSB_SUCCESS) { - for (uint8_t iface = 0; iface != config_desc->bNumInterfaces; ++iface) { - gui_console_printf("[USB] Info: Kernel driver was" - " active on interface %u: %s!\n", - iface, - libusb_kernel_driver_active(handle, iface) - ? "yes" : "no"); - libusb_attach_kernel_driver(handle, iface); - libusb_release_interface(handle, iface); - gui_console_printf("[USB] Info: Kernel driver now" - " active on interface %u: %s!\n", - iface, - libusb_kernel_driver_active(handle, iface) - ? "yes" : "no"); - } - libusb_free_config_descriptor(config_desc); + } + libusb_device_handle *handle = device->handle; + if (handle && !device->numPorts) { + for (uint8_t iface = 0; iface != 32; ++iface) { + uint32_t mask = UINT32_C(1) << iface; + if (device->claimedInterfaces & mask) { + libusb_release_interface(handle, iface); + device->claimedInterfaces &= ~mask; + } + if (device->detachedInterfaces & mask) { + libusb_attach_kernel_driver(handle, iface); + device->detachedInterfaces &= ~mask; } } } @@ -385,45 +417,166 @@ static device_t *device_detach(context_t *context, device_t *device) { return device; } +static void device_detach_children(context_t *context, device_t *device) { + for (uint8_t portIdx = 0; portIdx != device->numPorts; ++portIdx) { + device_t *child = device->hub.ports[portIdx].device; + if (child) { + device_detach_children(context, child); + device_detach(context, child); + } + } +} + +static void device_detach_disconnected(context_t *context) { + /* + * libusb's hotplug callback runs from inside event handling and its + * backend may still be using the device handle while delivering + * DEVICE_LEFT. Closing it from the callback can therefore recurse into + * teardown with partially destroyed backend state. Sweep after event + * handling returns instead, children before their parent. + */ + node_t *node = context->devices.prev; + while (node != &context->devices) { + device_t *device = NODE_ITEM(device_t, node); + node = node->prev; + if (device->disconnected) { + device_detach(context, device); + } + } +} + +static bool device_port_matches(libusb_device *libusb_device, + uint8_t bus, const uint8_t *ports, int num_ports) { + uint8_t device_ports[MAX_PORT_DEPTH]; + return libusb_get_bus_number(libusb_device) == bus + && num_ports > 0 + && libusb_get_port_numbers(libusb_device, device_ports, + MAX_PORT_DEPTH) == num_ports + && !memcmp(ports, device_ports, num_ports); +} + +static bool device_is_in_subtree(libusb_device *device, libusb_device *root) { + while (device && device != root) { + device = libusb_get_parent(device); + } + return device == root; +} + +static void device_discard_pending_in_subtree(context_t *context, + libusb_device *root) { + pending_t *pending; + NODE_FOREACH(pending, &context->pending) { + if (device_is_in_subtree(pending->device, root)) { + libusb_unref_device(pending->device); + node_remove(&pending->node); + free(pending); + } + } +} + +static void device_attach_current_descendants(context_t *context, + libusb_device *root) { + libusb_device **devices; + if (libusb_get_device_list(context->context, &devices) < 0) { + return; + } + uint8_t root_ports[MAX_PORT_DEPTH]; + int root_depth = libusb_get_port_numbers(root, root_ports, MAX_PORT_DEPTH); + /* Queue parents before children so device_init() can rebuild hub ports. */ + for (int depth = root_depth + 1; depth <= MAX_PORT_DEPTH; ++depth) { + for (libusb_device **device = devices; *device; ++device) { + uint8_t device_ports[MAX_PORT_DEPTH]; + if (libusb_get_port_numbers(*device, device_ports, MAX_PORT_DEPTH) != depth) { + continue; + } + libusb_device *ancestor = *device; + while (ancestor && ancestor != root) { + ancestor = libusb_get_parent(ancestor); + } + if (ancestor == root) { + device_attach(context, *device); + } + } + } + libusb_free_device_list(devices, true); +} + +static bool device_open_error_is_transient(int error) { + return error == LIBUSB_ERROR_BUSY + || error == LIBUSB_ERROR_NO_DEVICE + || error == LIBUSB_ERROR_NOT_FOUND; +} + static bool device_reset(context_t *context, device_t *device) { - switch (libusb_reset_device(device->handle)) { + libusb_device *previous_device = libusb_get_device(device->handle); + uint8_t bus = libusb_get_bus_number(previous_device); + uint8_t ports[MAX_PORT_DEPTH]; + int num_ports = libusb_get_port_numbers(previous_device, ports, MAX_PORT_DEPTH); + int reset_error = libusb_reset_device(device->handle); + switch (reset_error) { case LIBUSB_SUCCESS: return true; + case LIBUSB_ERROR_NOT_FOUND: case LIBUSB_ERROR_NO_DEVICE: break; default: return false; } - uint8_t ports[MAX_PORT_DEPTH]; - uint8_t num_ports = libusb_get_port_numbers( - libusb_get_device(device->handle), ports, MAX_PORT_DEPTH); + /* + * The port path is the only thing tying the replacement to the device + * that just went away, so without one the poll below can't match anything + */ + if (num_ports <= 0) { + device_detach(context, device); + return false; + } for (int iteration = 0; iteration != 100; ++iteration) { libusb_device **devices; if (errno_from_libusb_error( libusb_get_device_list(context->context, &devices)) == USB_SUCCESS) { libusb_device **enumerate_device; for (enumerate_device = devices; *enumerate_device; ++enumerate_device) { - uint8_t enumerate_ports[MAX_PORT_DEPTH]; - if (libusb_get_port_numbers(*enumerate_device, - enumerate_ports, MAX_PORT_DEPTH) == num_ports - && !memcmp(ports, enumerate_ports, num_ports)) { + if (*enumerate_device != previous_device + && device_port_matches(*enumerate_device, bus, ports, num_ports)) { break; } } - if (*enumerate_device) { + bool found_replacement = *enumerate_device; + libusb_device_handle *replacement_handle = NULL; + int open_error = found_replacement + ? libusb_open(*enumerate_device, &replacement_handle) + : LIBUSB_ERROR_NO_DEVICE; + if (open_error == LIBUSB_SUCCESS) { + libusb_device *replacement_device = + libusb_get_device(replacement_handle); + for (uint8_t index = 0; index != 0x20; ++index) { + endpoint_cleanup(context, &device->endpoints[index]); + } + device_discard_pending_in_subtree(context, previous_device); libusb_close(device->handle); - device->handle = NULL; - if (errno_from_libusb_error( - libusb_open(*enumerate_device, &device->handle)) != USB_SUCCESS) { - break; + device->handle = replacement_handle; + device->disconnected = false; + device->claimedInterfaces = 0; + device->detachedInterfaces = 0; + if (device->numPorts) { + device_detach_children(context, device); + } + device_discard_pending_in_subtree(context, replacement_device); + device_register_hotplug(context); + if (device->numPorts) { + device_attach_current_descendants(context, replacement_device); } + libusb_free_device_list(devices, true); return true; } libusb_free_device_list(devices, true); + if (found_replacement && !device_open_error_is_transient(open_error)) { + break; + } } struct timeval tv = { - .tv_sec = 0, - .tv_usec = 1000000, + .tv_sec = 1, + .tv_usec = 0, }; if (errno_from_libusb_error( libusb_handle_events_timeout(context->context, &tv)) != USB_SUCCESS) { @@ -724,32 +877,36 @@ static int device_intercept_control_setup(context_t *context, device_t *device, if (libusb_get_active_config_descriptor(libusb_get_device(handle), &config_desc) == LIBUSB_SUCCESS) { for (uint8_t iface = 0; iface != config_desc->bNumInterfaces; ++iface) { - gui_console_printf("[USB] Info: Kernel driver was" - " active on interface %u: %s!\n", - iface, - libusb_kernel_driver_active(handle, iface) - ? "yes" : "no"); - libusb_detach_kernel_driver(handle, iface); - libusb_release_interface(handle, iface); + if (iface < 32 && + (device->claimedInterfaces & (UINT32_C(1) << iface))) { + libusb_release_interface(handle, iface); + device->claimedInterfaces &= ~(UINT32_C(1) << iface); + } + if (iface < 32 && + libusb_kernel_driver_active(handle, iface) == 1 && + libusb_detach_kernel_driver(handle, iface) == LIBUSB_SUCCESS) { + device->detachedInterfaces |= UINT32_C(1) << iface; + } } libusb_free_config_descriptor(config_desc); config_desc = NULL; } - if ((*status = transfer_status_from_libusb_error( - error = libusb_get_config_descriptor_by_value( - libusb_get_device(handle), index, &config_desc))) - == LIBUSB_TRANSFER_COMPLETED - && (*status = transfer_status_from_libusb_error( - error = libusb_set_configuration(handle, index))) - == LIBUSB_TRANSFER_COMPLETED) { + error = libusb_get_config_descriptor_by_value( + libusb_get_device(handle), index, &config_desc); + if (error == LIBUSB_SUCCESS) { + error = libusb_get_configuration(handle, &configValueInt); + } + if (error == LIBUSB_SUCCESS && configValueInt != index) { + error = libusb_set_configuration(handle, index); + } + *status = transfer_status_from_libusb_error(error); + if (*status == LIBUSB_TRANSFER_COMPLETED) { device->state = DEVICE_STATE_CONFIGURED; for (uint8_t iface = 0; iface != config_desc->bNumInterfaces; ++iface) { - gui_console_printf("[USB] Info: Kernel driver now" - " active on interface %u: %s!\n", - iface, - libusb_kernel_driver_active(handle, iface) - ? "yes" : "no"); - libusb_claim_interface(handle, iface); + enum libusb_error claim_error = libusb_claim_interface(handle, iface); + if (iface < 32 && claim_error == LIBUSB_SUCCESS) { + device->claimedInterfaces |= UINT32_C(1) << iface; + } } } else { gui_console_printf("[USB] Error: Set configuration failed: %s!\n", @@ -1076,6 +1233,7 @@ static int LIBUSB_CALL device_hotplugged( } device_t *root = NODE_FIRST(device_t, &context->devices), *device; if (!root) { + context->hotplug_registered = false; return true; } NODE_FOREACH(device, &context->devices) { @@ -1087,11 +1245,12 @@ static int LIBUSB_CALL device_hotplugged( if (event & LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT) { if (device == root) { NODE_FOREACH (device, &context->devices) { - root = device = device_detach(context, device); + device->disconnected = true; } + context->hotplug_registered = false; return true; } else { - device = device_detach(context, device); + device->disconnected = true; } } } else { @@ -1131,6 +1290,7 @@ int usb_physical_device(usb_event_t *event) { }; error = errno_from_libusb_error( libusb_handle_events_timeout(context->context, &tv)); + device_detach_disconnected(context); event->type = USB_TIMER_EVENT; timer->mode = USB_TIMER_ABSOLUTE_MODE; timer->useconds = 1000; @@ -1143,7 +1303,7 @@ int usb_physical_device(usb_event_t *event) { context->handled = true; return error; } - if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG) && + if (!context->hotplug_registered && !NODE_EMPTY(&context->devices) && !context->throttle--) { libusb_device **devices; @@ -1183,22 +1343,13 @@ int usb_physical_device(usb_event_t *event) { node_init(&context->pending); node_init(&context->devices); context->handled = false; + context->hotplug_registered = false; context->throttle = 1000; error = errno_from_libusb_error(libusb_init(&context->context)); if (error != USB_SUCCESS) { return error; } - if (libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) { - libusb_hotplug_register_callback( - context->context, - LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED | - LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT, - 0, - LIBUSB_HOTPLUG_MATCH_ANY, - LIBUSB_HOTPLUG_MATCH_ANY, - LIBUSB_HOTPLUG_MATCH_ANY, - device_hotplugged, context, NULL); - } + device_register_hotplug(context); { int i, end; uint16_t vid, pid; diff --git a/core/usb/usb.c b/core/usb/usb.c index c6036d0c..cdf157e3 100644 --- a/core/usb/usb.c +++ b/core/usb/usb.c @@ -1075,16 +1075,29 @@ static void usb_reset_otg(void) { int usb_plug_device(int argc, const char *const *argv, usb_progress_handler_t *progress_handler, void *progress_context) { + usb_device_t *device; //gui_console_printf("usb"); //for (int i = 0; i < argc; ++i) { // gui_console_printf(" \"%s\"", argv[i]); //} //gui_console_printf("\n"); + if (argc < 1) { + device = usb_disconnected_device; + } else if (!strcasecmp(argv[0], "dusb")) { + device = usb_dusb_device; + } else if (!strcasecmp(argv[0], "physical")) { + device = usb_physical_device; + } else if (!strcasecmp(argv[0], "msd")) { + device = usb_msd_device; + } else { + return ENOEXEC; + } if (!usb.device) { usb.device = usb_disconnected_device; } usb.event.type = USB_DESTROY_EVENT; usb.device(&usb.event); + usb.device = usb_disconnected_device; usb_plug_complete(); usb.event.progress_handler = progress_handler; usb.event.progress_context = progress_context; @@ -1093,18 +1106,11 @@ int usb_plug_device(int argc, const char *const *argv, usb.event.type = USB_INIT_EVENT; usb.event.info.init.argc = argc; usb.event.info.init.argv = argv; - if (argc < 1) { + usb.device = device; + int error = usb_dispatch_event(NULL); + if (error) { usb.device = usb_disconnected_device; - } else if (!strcasecmp(argv[0], "dusb")) { - usb.device = usb_dusb_device; - } else if (!strcasecmp(argv[0], "physical")) { - usb.device = usb_physical_device; - } else if (!strcasecmp(argv[0], "msd")) { - usb.device = usb_msd_device; - } else { - return ENOEXEC; } - int error = usb_dispatch_event(NULL); usb_plug_complete(); return error; } From ebbd5a08e15303fc7f11463b79d345eb65a681ea Mon Sep 17 00:00:00 2001 From: Adrien Bertrand Date: Wed, 12 Aug 2026 22:34:13 +0200 Subject: [PATCH 07/11] core: usb: synchronize host and OTG controller state Keep OTG identity, role, VBUS/session, line state, and host-port state synchronized without injecting duplicate edge interrupts. Reset external connector state after image restore and avoid destroying an already-disconnected backend during no-op detach requests. Complete successful queue-head transfers with the updated qTD page, offset, remaining length, and data toggle. Traverse schedules only while the port is enabled. --- core/usb/usb.c | 181 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 133 insertions(+), 48 deletions(-) diff --git a/core/usb/usb.c b/core/usb/usb.c index cdf157e3..e4d0c2a3 100644 --- a/core/usb/usb.c +++ b/core/usb/usb.c @@ -190,15 +190,21 @@ void usb_grp2_int(uint16_t which) { // ... // 0 -> 1 OTGCSR_B_SESS_END static void usb_plug_b(void) { + uint32_t old = usb.regs.otgcsr; usb.regs.otgcsr |= OTGCSR_A_VBUS_VLD | OTGCSR_A_SESS_VLD | OTGCSR_B_SESS_VLD; usb.regs.otgcsr &= ~OTGCSR_B_SESS_END; - usb.regs.sof_fnr = 0; - usb_grp2_int(GISR2_RESUME); + if (old != usb.regs.otgcsr) { + usb.regs.sof_fnr = 0; + usb_grp2_int(GISR2_RESUME); + } } static void usb_unplug_b(void) { + uint32_t old = usb.regs.otgcsr; usb.regs.otgcsr &= ~(OTGCSR_A_VBUS_VLD | OTGCSR_A_SESS_VLD | OTGCSR_B_SESS_VLD); usb.regs.otgcsr |= OTGCSR_B_SESS_END; - usb_otg_int(OTGISR_BSESSEND); + if (old != usb.regs.otgcsr) { + usb_otg_int(OTGISR_BSESSEND); + } } // Plug A: @@ -209,24 +215,78 @@ static void usb_unplug_b(void) { // OTGCSR_DEV_A -> OTGCSR_DEV_B // OTGCSR_ROLE_H -> OTGCSR_ROLE_D static void usb_plug_a(void) { - usb_unplug_b(); + uint32_t old = usb.regs.otgcsr; + uint16_t interrupts = 0; + if (old & (OTGCSR_DEV_B | OTGCSR_ROLE_D)) { + usb_unplug_b(); + } usb.regs.otgcsr &= ~(OTGCSR_DEV_B | OTGCSR_ROLE_D); - usb.regs.sof_fnr = 0; - usb_otg_int(OTGISR_IDCHG | OTGISR_RLCHG); + if (old & OTGCSR_DEV_B) { + interrupts |= OTGISR_IDCHG; + } + if (old & OTGCSR_ROLE_D) { + interrupts |= OTGISR_RLCHG; + } + if (interrupts) { + usb.regs.sof_fnr = 0; + usb_otg_int(interrupts); + } } static void usb_unplug_a(void) { + uint32_t old = usb.regs.otgcsr; + uint16_t interrupts = 0; bool port_changed; - usb.regs.hcor.portsc[0] &= ~PORTSC_J_STATE; + usb.regs.hcor.portsc[0] &= ~(PORTSC_J_STATE | PORTSC_K_STATE); port_changed = usb_update_status_change(usb.regs.hcor.portsc, PORTSC_CONN_STATUS, PORTSC_CONN_CHANGE, false); port_changed |= usb_update_status_change(usb.regs.hcor.portsc, PORTSC_EN_STATUS, PORTSC_EN_CHANGE, false); if (port_changed) { usb_host_int(USBSTS_PORT_CHANGE); } usb.regs.otgcsr |= OTGCSR_DEV_B | OTGCSR_ROLE_D; - usb_otg_int(OTGISR_APRM | OTGISR_IDCHG | OTGISR_RLCHG); + if (!(old & OTGCSR_DEV_B)) { + interrupts |= OTGISR_APRM | OTGISR_IDCHG; + } + if (!(old & OTGCSR_ROLE_D)) { + interrupts |= OTGISR_RLCHG; + } + if (interrupts) { + usb_otg_int(interrupts); + } usb_unplug_b(); } +static bool usb_a_host_bus_powered(uint32_t otgcsr) { + return !(otgcsr & (OTGCSR_DEV_B | OTGCSR_ROLE_D)) && + (otgcsr & (OTGCSR_A_BUSDROP | OTGCSR_A_BUSREQ)) == OTGCSR_A_BUSREQ; +} + +static void usb_sync_a_host_port(void) { + bool bus_powered = usb_a_host_bus_powered(usb.regs.otgcsr); + bool port_connected = bus_powered && usb.device != usb_disconnected_device && !usb.event.host; + uint32_t line_state = usb.event.speed == USB_LOW_SPEED + ? PORTSC_K_STATE : PORTSC_J_STATE; + + if (bus_powered) { + usb.regs.otgcsr |= OTGCSR_A_VBUS_VLD | OTGCSR_A_SESS_VLD | OTGCSR_B_SESS_VLD; + usb.regs.otgcsr &= ~OTGCSR_B_SESS_END; + } else { + usb.regs.otgcsr &= ~(OTGCSR_A_VBUS_VLD | OTGCSR_A_SESS_VLD | OTGCSR_B_SESS_VLD); + usb.regs.otgcsr |= OTGCSR_B_SESS_END; + } + + usb.regs.hcor.portsc[0] &= ~(PORTSC_J_STATE | PORTSC_K_STATE); + if (port_connected) { + usb.regs.hcor.portsc[0] |= line_state; + } + bool port_changed = usb_update_status_change(usb.regs.hcor.portsc, PORTSC_CONN_STATUS, PORTSC_CONN_CHANGE, port_connected); + if (!port_connected) { + port_changed |= usb_update_status_change(usb.regs.hcor.portsc, PORTSC_EN_STATUS, PORTSC_EN_CHANGE, false); + } + if (port_changed) { + usb_host_int(USBSTS_PORT_CHANGE); + } +} + static void usb_plug(void) { if (usb.event.host) { usb_plug_b(); @@ -235,7 +295,7 @@ static void usb_plug(void) { } } static void usb_unplug(void) { - if (usb.event.host) { + if (usb.regs.otgcsr & OTGCSR_DEV_B) { usb_unplug_b(); } else { usb_unplug_a(); @@ -245,6 +305,9 @@ static void usb_unplug(void) { static void usb_plug_complete(void) { if (usb.device != usb_disconnected_device) { usb_plug(); + if (!usb.event.host) { + usb_sync_a_host_port(); + } sched_set(SCHED_USB, 1); } else { usb_unplug(); @@ -276,7 +339,10 @@ static void usb_write_back_qtd(usb_qh_t *qh) { qtd.halted = qh->overlay.halted; qtd.active = qh->overlay.active; qtd.cerr = qh->overlay.cerr; + qtd.page = qh->overlay.page; qtd.length = qh->overlay.length; + qtd.dt = qh->overlay.dt; + qtd.bufs[0].off = qh->overlay.bufs[0].off; mem_dma_write(&qtd, qh->cur.ptr << 5, sizeof(qtd)); // TODO: defer these interrupts? if (qh->overlay.halted) { @@ -290,6 +356,15 @@ static void usb_qh_completed(usb_qh_t *qh) { qh->overlay.active = false; usb_write_back_qtd(qh); } +static void usb_qh_success(usb_qh_t *qh, uint32_t actual_length) { + uint32_t packets = (actual_length && qh->max_pkt_size) + ? (actual_length + qh->max_pkt_size - 1) / qh->max_pkt_size + : 1; + if (packets & 1) { + qh->overlay.dt = !qh->overlay.dt; + } + usb_qh_completed(qh); +} static void usb_qh_halted(usb_qh_t *qh) { qh->overlay.halted = true; usb_qh_completed(qh); @@ -641,7 +716,7 @@ static int usb_dispatch_event(usb_traversal_state_t *state) { state->qh.overlay.missed = true; usb_qh_halted(&state->qh); } else { - usb_qh_completed(&state->qh); + usb_qh_success(&state->qh, transfer->length); } break; case USB_TRANSFER_STALLED: @@ -1095,10 +1170,15 @@ int usb_plug_device(int argc, const char *const *argv, if (!usb.device) { usb.device = usb_disconnected_device; } - usb.event.type = USB_DESTROY_EVENT; - usb.device(&usb.event); - usb.device = usb_disconnected_device; - usb_plug_complete(); + if (usb.device == usb_disconnected_device && device == usb_disconnected_device) { + return USB_SUCCESS; + } + if (usb.device != usb_disconnected_device) { + usb.event.type = USB_DESTROY_EVENT; + usb.device(&usb.event); + usb.device = usb_disconnected_device; + usb_plug_complete(); + } usb.event.progress_handler = progress_handler; usb.event.progress_context = progress_context; usb.event.context = NULL; @@ -1138,17 +1218,18 @@ static void usb_event(enum sched_item_id event) { if (!frame) { usb_host_int(USBSTS_FRAME_LIST_OVER); } - if (usb.regs.hcor.usbsts & USBSTS_PERIOD_SCHED) { - mem_dma_read(&state.link, (usb.regs.hcor.periodiclistbase & ~((1 << 12) - 1)) | - frame << 2, sizeof(state.link)); - usb_schedule_traverse(&state); - } - if (usb.regs.hcor.usbsts & USBSTS_ASYNC_SCHED) { - state.link.term = false; - state.link.type = QTYPE_QH; - state.link.ptr = usb.regs.hcor.asynclistaddr >> 5; - usb_schedule_traverse(&state); - usb.regs.hcor.asynclistaddr = state.link.val; + if (usb.regs.hcor.portsc[0] & PORTSC_EN_STATUS) { + if (usb.regs.hcor.usbsts & USBSTS_PERIOD_SCHED) { + mem_dma_read(&state.link, (usb.regs.hcor.periodiclistbase & ~((1 << 12) - 1)) | + frame << 2, sizeof(state.link)); + usb_schedule_traverse(&state); + } + if (usb.regs.hcor.usbsts & USBSTS_ASYNC_SCHED) { + state.link.term = false; + state.link.type = QTYPE_QH; + state.link.ptr = usb.regs.hcor.asynclistaddr >> 5; + usb_schedule_traverse(&state); + } } if (usb.regs.hcor.usbcmd & USBCMD_ASYNC_ADV_DRBL) { usb_host_int(USBSTS_ASYNC_ADV); @@ -1235,8 +1316,10 @@ static void usb_write(uint16_t pio, uint8_t value, bool poke) { break; case 0x030 >> 2: // PORTSC - Port Status and Control Register old = usb.regs.hcor.portsc[0]; - usb.regs.hcor.portsc[0] &= ~(((uint32_t)value << bit_offset & 0x2E) | (0x7F0100 & 0xFF << bit_offset)) ^ - PORTSC_EN_STATUS; // W[0/1]C mask (V or RO or W) + uint32_t byte_mask = UINT32_C(0xFF) << bit_offset; + usb.regs.hcor.portsc[0] &= + ~(((uint32_t)value << bit_offset & UINT32_C(0x2E)) | + (UINT32_C(0x7F0100) & byte_mask)) ^ (PORTSC_EN_STATUS & byte_mask); // W[0/1]C mask (V or RO or W) usb.regs.hcor.portsc[0] |= (uint32_t)value << bit_offset & 0x7F0180; // W mask (RO) if ((old ^ usb.regs.hcor.portsc[0]) & PORTSC_RESET) { // TODO: actually powered by gpio? @@ -1280,27 +1363,13 @@ static void usb_write(uint16_t pio, uint8_t value, bool poke) { case 0x080 >> 2: // OTG Control Status Register old = usb.regs.otgcsr; write8(usb.regs.otgcsr, bit_offset, value & 0x1A00FFF7 >> bit_offset); // W mask (V) - if (usb.device != usb_disconnected_device && !usb.event.host && - !(usb.regs.otgcsr & OTGCSR_ROLE_D)) { - bool bus_requested = (usb.regs.otgcsr & (OTGCSR_A_BUSDROP | OTGCSR_A_BUSREQ)) == OTGCSR_A_BUSREQ; - if (bus_requested) { - usb.regs.otgcsr |= OTGCSR_A_VBUS_VLD | OTGCSR_A_SESS_VLD | OTGCSR_B_SESS_VLD; - usb.regs.otgcsr &= ~OTGCSR_B_SESS_END; - } else { - usb.regs.otgcsr &= ~(OTGCSR_A_VBUS_VLD | OTGCSR_A_SESS_VLD | OTGCSR_B_SESS_VLD); - usb.regs.otgcsr |= OTGCSR_B_SESS_END; - } - bool port_changed = usb_update_status_change( - usb.regs.hcor.portsc, PORTSC_J_STATE | PORTSC_CONN_STATUS, - PORTSC_CONN_CHANGE, bus_requested); - if (!bus_requested) { - port_changed |= usb_update_status_change(usb.regs.hcor.portsc, PORTSC_EN_STATUS, PORTSC_EN_CHANGE, false); - } - if (port_changed) { - usb_host_int(USBSTS_PORT_CHANGE); - } - if (((old & (OTGCSR_A_BUSDROP | OTGCSR_A_BUSREQ)) == OTGCSR_A_BUSREQ) != bus_requested) { - usb.event.type = bus_requested ? USB_SESSION_START_EVENT : USB_SESSION_END_EVENT; + if (!(usb.regs.otgcsr & (OTGCSR_DEV_B | OTGCSR_ROLE_D))) { + bool was_bus_powered = usb_a_host_bus_powered(old); + usb_sync_a_host_port(); + bool bus_powered = usb_a_host_bus_powered(usb.regs.otgcsr); + if (usb.device != usb_disconnected_device && !usb.event.host && + was_bus_powered != bus_powered) { + usb.event.type = bus_powered ? USB_SESSION_START_EVENT : USB_SESSION_END_EVENT; usb_dispatch_event(NULL); } } else if (usb.device != usb_disconnected_device && usb.event.host && @@ -1538,5 +1607,21 @@ bool usb_restore(FILE *image) { usb.regs.gimr0 &= GIMR0_MASK; usb.regs.gimr1 &= GIMR1_MASK; usb.regs.gimr2 &= GIMR2_MASK; + + /* + * External USB backends are not part of an emulator image. Keep the + * guest-owned controller setup, but return the physical connector and + * role to their disconnected defaults and quietly remove line, session, + * and port state that depended on the missing backend. A later attachment + * will then produce real identity, role, and connection edges. + */ + usb.regs.hcor.portsc[0] &= ~(PORTSC_J_STATE | PORTSC_K_STATE | PORTSC_RESET | PORTSC_EN_CHANGE | + PORTSC_EN_STATUS | PORTSC_CONN_CHANGE | PORTSC_CONN_STATUS); + usb.regs.hcor.usbsts &= ~USBSTS_PORT_CHANGE; + usb.regs.otgcsr &= ~(OTGCSR_SPD_MASK | OTGCSR_A_VBUS_VLD | OTGCSR_A_SESS_VLD | OTGCSR_B_SESS_VLD); + usb.regs.otgcsr |= OTGCSR_DEV_B | OTGCSR_ROLE_D | OTGCSR_B_SESS_END; + usb.regs.otgisr &= ~(OTGISR_APRM | OTGISR_BPRM | OTGISR_IDCHG | OTGISR_RLCHG | OTGISR_BSESSEND); + usb.regs.gisr2 &= ~GISR2_RESUME; + usb_update(); return success; } From 4223c0549402b36d3d5a90a207948868fcbb7023 Mon Sep 17 00:00:00 2001 From: Adrien Bertrand Date: Sun, 16 Aug 2026 01:18:11 +0200 Subject: [PATCH 08/11] core: usb: make physical reset recovery asynchronous Avoid blocking the emulation thread while a physical USB reset re-enumerates a device. Poll replacement handles from the backend timer, ignore the reset hotplug pair, and keep teardown outside libusb hotplug callbacks. Treat synchronous libusb submission failures as failed transfers instead of tearing down the entire physical backend, so one inaccessible interface does not disconnect an otherwise usable composite device. --- core/usb/physical.c | 304 +++++++++++++++++++++++++++++++++----------- 1 file changed, 230 insertions(+), 74 deletions(-) diff --git a/core/usb/physical.c b/core/usb/physical.c index faf6dfff..10eb597b 100644 --- a/core/usb/physical.c +++ b/core/usb/physical.c @@ -16,6 +16,12 @@ #define MAX_PORT_DEPTH 7 +#ifdef CEMU_USB_TRACE +#define PHYSICAL_TRACE(...) fprintf(stderr, "[USBPHY] " __VA_ARGS__) +#else +#define PHYSICAL_TRACE(...) ((void)0) +#endif + #define NODE_EMPTY(head) \ ((head)->next == (head)) @@ -127,7 +133,11 @@ struct device { endpoint_t endpoints[0x20]; uint8_t state : 2; /* enum device_state */ uint8_t address : 7, numPorts : 7; - bool disconnected; + bool disconnected, reset_pending; + uint8_t reset_hotplug_events; + uint8_t reset_bus, reset_num_ports; + uint16_t reset_polls_remaining, reset_hotplug_polls_remaining; + uint8_t reset_ports[MAX_PORT_DEPTH]; uint32_t claimedInterfaces, detachedInterfaces; hub_t hub; }; @@ -294,6 +304,12 @@ static int device_init(context_t *context, struct libusb_device *libusb_device) device->address = 0; device->numPorts = hub_desc.bNbrPorts; device->disconnected = false; + device->reset_pending = false; + device->reset_hotplug_events = 0; + device->reset_bus = 0; + device->reset_num_ports = 0; + device->reset_polls_remaining = 0; + device->reset_hotplug_polls_remaining = 0; device->claimedInterfaces = 0; device->detachedInterfaces = 0; if (device->numPorts) { @@ -369,6 +385,9 @@ static bool device_register_hotplug(context_t *context) { static device_t *device_detach(context_t *context, device_t *device) { if (device) { + PHYSICAL_TRACE("detach state=%u disconnected=%u reset=%u hotplug=%u\n", + device->state, device->disconnected, + device->reset_pending, device->reset_hotplug_events); for (uint8_t index = 0; index != 0x20; ++index) { endpoint_cleanup(context, &device->endpoints[index]); } @@ -439,12 +458,27 @@ static void device_detach_disconnected(context_t *context) { while (node != &context->devices) { device_t *device = NODE_ITEM(device_t, node); node = node->prev; - if (device->disconnected) { + if (device->disconnected && !device->reset_pending) { device_detach(context, device); } } } +static void device_age_reset_hotplug(context_t *context) { + device_t *device; + NODE_FOREACH(device, &context->devices) { + if (device->reset_hotplug_events) { + if (device->reset_hotplug_polls_remaining) { + --device->reset_hotplug_polls_remaining; + } else { + PHYSICAL_TRACE("reset hotplug guard expired with %u events\n", + device->reset_hotplug_events); + device->reset_hotplug_events = 0; + } + } + } +} + static bool device_port_matches(libusb_device *libusb_device, uint8_t bus, const uint8_t *ports, int num_ports) { uint8_t device_ports[MAX_PORT_DEPTH]; @@ -507,84 +541,162 @@ static bool device_open_error_is_transient(int error) { || error == LIBUSB_ERROR_NOT_FOUND; } -static bool device_reset(context_t *context, device_t *device) { +typedef enum device_reset_result { + DEVICE_RESET_FAILED, + DEVICE_RESET_COMPLETED, + DEVICE_RESET_PENDING, +} device_reset_result_t; + +static device_reset_result_t device_finish_reset(context_t *context, + device_t *device) { libusb_device *previous_device = libusb_get_device(device->handle); - uint8_t bus = libusb_get_bus_number(previous_device); - uint8_t ports[MAX_PORT_DEPTH]; - int num_ports = libusb_get_port_numbers(previous_device, ports, MAX_PORT_DEPTH); - int reset_error = libusb_reset_device(device->handle); - switch (reset_error) { - case LIBUSB_SUCCESS: - return true; - case LIBUSB_ERROR_NOT_FOUND: - case LIBUSB_ERROR_NO_DEVICE: + libusb_device **devices; + if (errno_from_libusb_error( + libusb_get_device_list(context->context, &devices)) != USB_SUCCESS) { + return DEVICE_RESET_PENDING; + } + libusb_device **enumerate_device; + for (enumerate_device = devices; *enumerate_device; ++enumerate_device) { + if (device_port_matches(*enumerate_device, device->reset_bus, + device->reset_ports, + device->reset_num_ports)) { break; - default: - return false; + } + } + bool found_replacement = *enumerate_device; + libusb_device_handle *replacement_handle = NULL; + int open_error = found_replacement + ? libusb_open(*enumerate_device, &replacement_handle) + : LIBUSB_ERROR_NO_DEVICE; + if (open_error == LIBUSB_SUCCESS) { + libusb_device *replacement_device = libusb_get_device(replacement_handle); + for (uint8_t index = 0; index != 0x20; ++index) { + endpoint_cleanup(context, &device->endpoints[index]); + } + device_discard_pending_in_subtree(context, previous_device); + libusb_close(device->handle); + device->handle = replacement_handle; + device->disconnected = false; + device->reset_pending = false; + device->claimedInterfaces = 0; + device->detachedInterfaces = 0; + if (device->numPorts) { + device_detach_children(context, device); + } + device_discard_pending_in_subtree(context, replacement_device); + device_register_hotplug(context); + if (device->numPorts) { + device_attach_current_descendants(context, replacement_device); + } + libusb_free_device_list(devices, true); + return DEVICE_RESET_COMPLETED; + } + libusb_free_device_list(devices, true); + return found_replacement && !device_open_error_is_transient(open_error) + ? DEVICE_RESET_FAILED + : DEVICE_RESET_PENDING; +} + +static device_reset_result_t device_reset(context_t *context, device_t *device) { + libusb_device *previous_device = libusb_get_device(device->handle); + /* A bus reset cancels every pending transfer. */ + for (uint8_t index = 0; index != 0x20; ++index) { + endpoint_cleanup(context, &device->endpoints[index]); + } + device->reset_bus = libusb_get_bus_number(previous_device); + int num_ports = libusb_get_port_numbers(previous_device, + device->reset_ports, + MAX_PORT_DEPTH); + /* Hotplug notifications generated by this reset are delivered later. */ + uint8_t reset_hotplug_events = device->reset_hotplug_events; + if (device->reset_hotplug_events <= UINT8_MAX - 2) { + device->reset_hotplug_events += 2; + } + int reset_error = libusb_reset_device(device->handle); + PHYSICAL_TRACE("reset result=%d prior-hotplug=%u expected-hotplug=%u\n", + reset_error, reset_hotplug_events, + device->reset_hotplug_events); + if (reset_error == LIBUSB_SUCCESS) { + /* + * A reset can deliver DEVICE_LEFT/ARRIVED while libusb keeps the + * existing device object and handle valid. The hotplug callback marks + * the root disconnected and unregisters itself on DEVICE_LEFT, so + * reconcile that transient state before the deferred detach sweep. + */ + device->disconnected = false; + device->reset_pending = false; + device->reset_hotplug_polls_remaining = 1000; + device_register_hotplug(context); + return DEVICE_RESET_COMPLETED; + } + device->reset_hotplug_events = reset_hotplug_events; + if (reset_error == LIBUSB_ERROR_NOT_FOUND) { + /* + * A backend may reject a physical reset once an interface is claimed + * (Darwin reports this when the handle lacks exclusive device access). + * The device is still present and usable, so complete the guest's bus + * reset logically instead of waiting for a re-enumeration that did not + * occur and eventually unplugging it. + */ + return DEVICE_RESET_COMPLETED; + } + if (reset_error != LIBUSB_ERROR_NO_DEVICE) { + return DEVICE_RESET_FAILED; } /* - * The port path is the only thing tying the replacement to the device - * that just went away, so without one the poll below can't match anything + * Some platforms re-enumerate a device during reset. Waiting for that + * replacement inside this call blocks the emulation thread, so remember + * its stable port path and let the normal physical-backend timer finish + * reacquiring it. */ if (num_ports <= 0) { - device_detach(context, device); - return false; + return DEVICE_RESET_FAILED; } - for (int iteration = 0; iteration != 100; ++iteration) { - libusb_device **devices; - if (errno_from_libusb_error( - libusb_get_device_list(context->context, &devices)) == USB_SUCCESS) { - libusb_device **enumerate_device; - for (enumerate_device = devices; *enumerate_device; ++enumerate_device) { - if (*enumerate_device != previous_device - && device_port_matches(*enumerate_device, bus, ports, num_ports)) { - break; - } - } - bool found_replacement = *enumerate_device; - libusb_device_handle *replacement_handle = NULL; - int open_error = found_replacement - ? libusb_open(*enumerate_device, &replacement_handle) - : LIBUSB_ERROR_NO_DEVICE; - if (open_error == LIBUSB_SUCCESS) { - libusb_device *replacement_device = - libusb_get_device(replacement_handle); - for (uint8_t index = 0; index != 0x20; ++index) { - endpoint_cleanup(context, &device->endpoints[index]); - } - device_discard_pending_in_subtree(context, previous_device); - libusb_close(device->handle); - device->handle = replacement_handle; - device->disconnected = false; - device->claimedInterfaces = 0; - device->detachedInterfaces = 0; - if (device->numPorts) { - device_detach_children(context, device); - } - device_discard_pending_in_subtree(context, replacement_device); - device_register_hotplug(context); - if (device->numPorts) { - device_attach_current_descendants(context, replacement_device); + device->reset_num_ports = num_ports; + device->reset_polls_remaining = 5000; + device->reset_pending = true; + device->disconnected = false; + device_reset_result_t result = device_finish_reset(context, device); + if (result == DEVICE_RESET_FAILED) { + device->reset_pending = false; + } + return result; +} + +static void device_poll_resets(context_t *context) { + node_t *node = context->devices.next; + while (node != &context->devices) { + device_t *device = NODE_ITEM(device_t, node); + node = node->next; + if (!device->reset_pending) { + continue; + } + device_reset_result_t result = device_finish_reset(context, device); + if (result == DEVICE_RESET_PENDING && device->reset_polls_remaining) { + --device->reset_polls_remaining; + continue; + } + if (result == DEVICE_RESET_COMPLETED) { + device->state = DEVICE_STATE_DEFAULT_OR_ADDRESS; + device->address = 0; + device_t *parent; + NODE_FOREACH(parent, &context->devices) { + for (uint8_t portIdx = 0; portIdx != parent->numPorts; ++portIdx) { + port_t *port = &parent->hub.ports[portIdx]; + if (port->device == device) { + port->status.enable = true; + port->status.low_speed = libusb_get_device_speed( + libusb_get_device(device->handle)) == LIBUSB_SPEED_LOW; + port->status.high_speed = false; + port->change.reset = true; + } } - libusb_free_device_list(devices, true); - return true; - } - libusb_free_device_list(devices, true); - if (found_replacement && !device_open_error_is_transient(open_error)) { - break; } - } - struct timeval tv = { - .tv_sec = 1, - .tv_usec = 0, - }; - if (errno_from_libusb_error( - libusb_handle_events_timeout(context->context, &tv)) != USB_SUCCESS) { - break; + } else { + device->reset_pending = false; + device_detach(context, device); } } - device_detach(context, device); - return false; } static void LIBUSB_CALL transfer_completed(struct libusb_transfer *libusb_transfer) { @@ -717,7 +829,9 @@ static int device_intercept_control_setup(context_t *context, device_t *device, port->change.enable = true; break; } - if (device_reset(context, port->device)) { + device_reset_result_t reset_result = + device_reset(context, port->device); + if (reset_result == DEVICE_RESET_COMPLETED) { port->device->state = DEVICE_STATE_DEFAULT_OR_ADDRESS; port->device->address = 0; port->status.enable = true; @@ -726,7 +840,7 @@ static int device_intercept_control_setup(context_t *context, device_t *device, == LIBUSB_SPEED_LOW; port->status.high_speed = false; port->change.reset = true; - } else { + } else if (reset_result == DEVICE_RESET_FAILED) { port->status.enable = false; port->change.enable = true; } @@ -904,6 +1018,8 @@ static int device_intercept_control_setup(context_t *context, device_t *device, device->state = DEVICE_STATE_CONFIGURED; for (uint8_t iface = 0; iface != config_desc->bNumInterfaces; ++iface) { enum libusb_error claim_error = libusb_claim_interface(handle, iface); + PHYSICAL_TRACE("claim interface %u: %s\n", iface, + libusb_error_name(claim_error)); if (iface < 32 && claim_error == LIBUSB_SUCCESS) { device->claimedInterfaces |= UINT32_C(1) << iface; } @@ -1169,11 +1285,28 @@ static int device_process_transfer(context_t *context, device_t *device, usb_eve } if (error == USB_SUCCESS && transfer->state != TRANSFER_STATE_SUBMITTED) { error = device_intercept_transfer(context, device, transfer, info->length); + if (error != USB_SUCCESS) { + PHYSICAL_TRACE("intercept failed error=%d\n", error); + } } if (error == USB_SUCCESS && transfer->state == TRANSFER_STATE_NONE) { - error = errno_from_libusb_error(libusb_submit_transfer(libusb_transfer)); - if (error == USB_SUCCESS) { + enum libusb_error submit_error = libusb_submit_transfer(libusb_transfer); + if (submit_error == LIBUSB_SUCCESS) { transfer->state = TRANSFER_STATE_SUBMITTED; + } else { + PHYSICAL_TRACE("submit failed error=%s endpoint=%02x type=%u\n", + libusb_error_name(submit_error), + libusb_transfer->endpoint, + libusb_transfer->type); + /* A synchronous submission failure belongs to this USB transfer, + * not to the physical backend as a whole. Report it to the guest + * just like an asynchronous failed completion so one inaccessible + * interface cannot tear down an otherwise usable composite + * device. */ + libusb_transfer->status = + transfer_status_from_libusb_error(submit_error); + libusb_transfer->actual_length = 0; + transfer->state = TRANSFER_STATE_COMPLETED; } } if (error == USB_SUCCESS && transfer->state == TRANSFER_STATE_SUBMITTED) { @@ -1242,6 +1375,16 @@ static int LIBUSB_CALL device_hotplugged( } } if (device) { + PHYSICAL_TRACE("hotplug event=%u root=%u disconnected=%u expected=%u\n", + event, device == root, device->disconnected, + device->reset_hotplug_events); + if (device->reset_hotplug_events) { + /* Ignore the transient LEFT/ARRIVED pair produced by reset. */ + --device->reset_hotplug_events; + PHYSICAL_TRACE("ignored reset hotplug; remaining=%u\n", + device->reset_hotplug_events); + return false; + } if (event & LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT) { if (device == root) { NODE_FOREACH (device, &context->devices) { @@ -1290,6 +1433,8 @@ int usb_physical_device(usb_event_t *event) { }; error = errno_from_libusb_error( libusb_handle_events_timeout(context->context, &tv)); + device_age_reset_hotplug(context); + device_poll_resets(context); device_detach_disconnected(context); event->type = USB_TIMER_EVENT; timer->mode = USB_TIMER_ABSOLUTE_MODE; @@ -1438,9 +1583,12 @@ int usb_physical_device(usb_event_t *event) { device->address = 0; } else if (device->state >= DEVICE_STATE_POWERED && type == USB_RESET_EVENT) { device->state = DEVICE_STATE_POWERED; - if (device_reset(context, device)) { + device_reset_result_t reset_result = device_reset(context, device); + if (reset_result == DEVICE_RESET_COMPLETED) { device->state = DEVICE_STATE_DEFAULT_OR_ADDRESS; device->address = 0; + } else if (reset_result == DEVICE_RESET_FAILED) { + device->reset_pending = false; } device = NULL; } @@ -1457,6 +1605,10 @@ int usb_physical_device(usb_event_t *event) { case USB_TRANSFER_REQUEST_EVENT: event->pending = false; NODE_FOREACH (device, &context->devices) { + if (device->reset_pending && !transfer->address) { + event->pending = true; + break; + } if (device->state <= DEVICE_STATE_POWERED || device->address != transfer->address) { continue; @@ -1472,6 +1624,7 @@ int usb_physical_device(usb_event_t *event) { case USB_TIMER_EVENT: break; case USB_DESTROY_EVENT: + PHYSICAL_TRACE("destroy event\n"); if (event->progress_handler) { event->progress_handler(event->progress_context, 0, 0); event->progress_handler = NULL; @@ -1497,6 +1650,9 @@ int usb_physical_device(usb_event_t *event) { break; } context->handled = false; + if (error != USB_SUCCESS) { + PHYSICAL_TRACE("event type=%u failed with error=%d\n", type, error); + } return error; } From da63dcde74426a975eba5e62cbe886a1b434e449 Mon Sep 17 00:00:00 2001 From: Adrien Bertrand Date: Sun, 16 Aug 2026 01:18:29 +0200 Subject: [PATCH 09/11] core: usb: support HID composite devices on macOS Open HID interfaces unavailable to libusb through IOHID while libusb continues to own the remaining interfaces. Keep HID waits separate from libusb transfers and clean them up during reset and detach. Correlate each IOHID device with its USB interface number, maintain one bridge per interface, and route interrupt-IN endpoints to the correct bridge. Link CoreFoundation and IOKit in both Qt build systems. --- core/usb/physical.c | 175 ++++++++++++++++- core/usb/physical_macos.c | 393 ++++++++++++++++++++++++++++++++++++++ core/usb/physical_macos.h | 27 +++ gui/qt/CEmu.pro | 3 +- gui/qt/CMakeLists.txt | 7 +- 5 files changed, 602 insertions(+), 3 deletions(-) create mode 100644 core/usb/physical_macos.c create mode 100644 core/usb/physical_macos.h diff --git a/core/usb/physical.c b/core/usb/physical.c index 10eb597b..5bdbb68b 100644 --- a/core/usb/physical.c +++ b/core/usb/physical.c @@ -14,6 +14,10 @@ #include #include +#ifdef __APPLE__ +#include "physical_macos.h" +#endif + #define MAX_PORT_DEPTH 7 #ifdef CEMU_USB_TRACE @@ -64,6 +68,7 @@ struct pending { enum transfer_state { TRANSFER_STATE_SUBMITTED, // must be 0 + TRANSFER_STATE_HID_PENDING, TRANSFER_STATE_COMPLETED, TRANSFER_STATE_PENDING, TRANSFER_STATE_NONE, @@ -139,6 +144,10 @@ struct device { uint16_t reset_polls_remaining, reset_hotplug_polls_remaining; uint8_t reset_ports[MAX_PORT_DEPTH]; uint32_t claimedInterfaces, detachedInterfaces; +#ifdef __APPLE__ + physical_hid_device_t *hid_interfaces[32]; + physical_hid_device_t *hid_endpoint_devices[32]; +#endif hub_t hub; }; @@ -312,6 +321,11 @@ static int device_init(context_t *context, struct libusb_device *libusb_device) device->reset_hotplug_polls_remaining = 0; device->claimedInterfaces = 0; device->detachedInterfaces = 0; +#ifdef __APPLE__ + memset(device->hid_interfaces, 0, sizeof(device->hid_interfaces)); + memset(device->hid_endpoint_devices, 0, + sizeof(device->hid_endpoint_devices)); +#endif if (device->numPorts) { device->hub.statusChange.value = 0; for (uint8_t portIdx = 0; portIdx != device->numPorts; ++portIdx) { @@ -391,6 +405,14 @@ static device_t *device_detach(context_t *context, device_t *device) { for (uint8_t index = 0; index != 0x20; ++index) { endpoint_cleanup(context, &device->endpoints[index]); } +#ifdef __APPLE__ + for (uint8_t interface = 0; interface < 32; ++interface) { + physical_hid_close(device->hid_interfaces[interface]); + } + memset(device->hid_interfaces, 0, sizeof(device->hid_interfaces)); + memset(device->hid_endpoint_devices, 0, + sizeof(device->hid_endpoint_devices)); +#endif /* * Unlink from the hub port by identity rather than by asking libusb * who our parent is: device_reset() swaps a reacquired hub's handle @@ -599,10 +621,18 @@ static device_reset_result_t device_finish_reset(context_t *context, static device_reset_result_t device_reset(context_t *context, device_t *device) { libusb_device *previous_device = libusb_get_device(device->handle); - /* A bus reset cancels every pending transfer. */ + /* A bus reset cancels every transfer, including waits owned by IOHID. */ for (uint8_t index = 0; index != 0x20; ++index) { endpoint_cleanup(context, &device->endpoints[index]); } +#ifdef __APPLE__ + for (uint8_t interface = 0; interface < 32; ++interface) { + physical_hid_close(device->hid_interfaces[interface]); + } + memset(device->hid_interfaces, 0, sizeof(device->hid_interfaces)); + memset(device->hid_endpoint_devices, 0, + sizeof(device->hid_endpoint_devices)); +#endif device->reset_bus = libusb_get_bus_number(previous_device); int num_ports = libusb_get_port_numbers(previous_device, device->reset_ports, @@ -704,6 +734,124 @@ static void LIBUSB_CALL transfer_completed(struct libusb_transfer *libusb_transf transfer->state = TRANSFER_STATE_COMPLETED; } +#ifdef __APPLE__ +static bool device_can_bridge_hid_claim(enum libusb_error error) { + switch (error) { + case LIBUSB_ERROR_ACCESS: + case LIBUSB_ERROR_NOT_FOUND: + case LIBUSB_ERROR_BUSY: + case LIBUSB_ERROR_NOT_SUPPORTED: + return true; + default: + return false; + } +} + +static bool device_open_hid_bridge(device_t *device, uint8_t interface_number) { + if (interface_number >= 32) { + return false; + } + if (device->hid_interfaces[interface_number]) { + return true; + } + struct libusb_device_descriptor descriptor; + if (libusb_get_device_descriptor(libusb_get_device(device->handle), + &descriptor) != LIBUSB_SUCCESS) { + return false; + } + physical_hid_open_result_t result = physical_hid_open( + descriptor.idVendor, descriptor.idProduct, interface_number, + &device->hid_interfaces[interface_number]); + switch (result) { + case PHYSICAL_HID_OPEN_SUCCESS: + gui_console_printf( + "[USB] Using the macOS HID bridge for interface %u unavailable to libusb.\n", + interface_number); + return true; + case PHYSICAL_HID_OPEN_PERMISSION_DENIED: + gui_console_printf( + "[USB] macOS denied HID input access. Enable CEmu in System Settings > Privacy & Security > Input Monitoring, then reconnect the USB device.\n"); + break; + case PHYSICAL_HID_OPEN_AMBIGUOUS: + gui_console_printf( + "[USB] Multiple matching macOS HID devices were found; the libusb device could not be correlated safely.\n"); + break; + case PHYSICAL_HID_OPEN_NOT_FOUND: + case PHYSICAL_HID_OPEN_FAILED: + gui_console_printf( + "[USB] The HID interface unavailable to libusb could not be opened through macOS HID APIs.\n"); + break; + } + return false; +} + +static void device_mark_hid_endpoints(device_t *device, + const struct libusb_interface *interface) { + uint32_t endpoints = 0; + for (int alt = 0; alt != interface->num_altsetting; ++alt) { + const struct libusb_interface_descriptor *descriptor = + &interface->altsetting[alt]; + if (descriptor->bInterfaceClass != LIBUSB_CLASS_HID) { + continue; + } + for (uint8_t endpoint = 0; endpoint != descriptor->bNumEndpoints; + ++endpoint) { + const struct libusb_endpoint_descriptor *endpoint_descriptor = + &descriptor->endpoint[endpoint]; + if ((endpoint_descriptor->bmAttributes & LIBUSB_TRANSFER_TYPE_MASK) + != LIBUSB_TRANSFER_TYPE_INTERRUPT + || !(endpoint_descriptor->bEndpointAddress & LIBUSB_ENDPOINT_IN)) { + continue; + } + uint8_t number = endpoint_descriptor->bEndpointAddress + & LIBUSB_ENDPOINT_ADDRESS_MASK; + if (number < 16) { + endpoints |= UINT32_C(1) << (number * 2 + 1); + } + } + } + uint8_t interface_number = interface->altsetting[0].bInterfaceNumber; + if (endpoints && device_open_hid_bridge(device, interface_number)) { + for (uint8_t index = 0; index < 32; ++index) { + if (endpoints & (UINT32_C(1) << index)) { + device->hid_endpoint_devices[index] = + device->hid_interfaces[interface_number]; + } + } + } +} + +static bool device_hid_transfer(device_t *device, transfer_t *transfer) { + struct libusb_transfer *libusb_transfer = transfer->transfer; + uint8_t endpoint = libusb_transfer->endpoint; + uint8_t number = endpoint & LIBUSB_ENDPOINT_ADDRESS_MASK; + uint8_t index = number * 2 + !!(endpoint & LIBUSB_ENDPOINT_IN); + if (index >= 32 || !device->hid_endpoint_devices[index]) { + return false; + } + size_t length = 0; + physical_hid_read_result_t result = physical_hid_read( + device->hid_endpoint_devices[index], libusb_transfer->buffer, + (size_t)libusb_transfer->length, &length); + switch (result) { + case PHYSICAL_HID_READ_PENDING: + transfer->state = TRANSFER_STATE_HID_PENDING; + break; + case PHYSICAL_HID_READ_COMPLETED: + libusb_transfer->status = LIBUSB_TRANSFER_COMPLETED; + libusb_transfer->actual_length = (int)length; + transfer->state = TRANSFER_STATE_COMPLETED; + break; + case PHYSICAL_HID_READ_DISCONNECTED: + libusb_transfer->status = LIBUSB_TRANSFER_NO_DEVICE; + libusb_transfer->actual_length = 0; + transfer->state = TRANSFER_STATE_COMPLETED; + break; + } + return true; +} +#endif + static void transfer_append(transfer_t *transfer, const void *src, uint32_t length) { struct libusb_transfer *libusb_transfer = transfer->transfer; uint8_t *dest = libusb_transfer->buffer; @@ -1023,6 +1171,12 @@ static int device_intercept_control_setup(context_t *context, device_t *device, if (iface < 32 && claim_error == LIBUSB_SUCCESS) { device->claimedInterfaces |= UINT32_C(1) << iface; } +#ifdef __APPLE__ + if (device_can_bridge_hid_claim(claim_error)) { + device_mark_hid_endpoints( + device, &config_desc->interface[iface]); + } +#endif } } else { gui_console_printf("[USB] Error: Set configuration failed: %s!\n", @@ -1218,6 +1372,19 @@ static int device_process_transfer(context_t *context, device_t *device, usb_eve endpoint_t *endpoint = &device->endpoints[index]; transfer_t *transfer = &endpoint->transfer; struct libusb_transfer *libusb_transfer = transfer->transfer; + if (transfer->state == TRANSFER_STATE_HID_PENDING) { +#ifdef __APPLE__ + if (device_hid_transfer(device, transfer) + && transfer->state != TRANSFER_STATE_HID_PENDING) { + libusb_transfer = transfer->transfer; + } else { + return error; + } +#else + /* A HID-pending transfer is only produced by the macOS bridge. */ + transfer->state = TRANSFER_STATE_NONE; +#endif + } if (transfer->state == TRANSFER_STATE_SUBMITTED) { return error; } @@ -1289,6 +1456,12 @@ static int device_process_transfer(context_t *context, device_t *device, usb_eve PHYSICAL_TRACE("intercept failed error=%d\n", error); } } +#ifdef __APPLE__ + if (error == USB_SUCCESS && transfer->state == TRANSFER_STATE_NONE + && device_hid_transfer(device, transfer)) { + /* Completion is consumed below; a pending read is retried next frame. */ + } else +#endif if (error == USB_SUCCESS && transfer->state == TRANSFER_STATE_NONE) { enum libusb_error submit_error = libusb_submit_transfer(libusb_transfer); if (submit_error == LIBUSB_SUCCESS) { diff --git a/core/usb/physical_macos.c b/core/usb/physical_macos.c new file mode 100644 index 00000000..9a451cd1 --- /dev/null +++ b/core/usb/physical_macos.c @@ -0,0 +1,393 @@ +#include "physical_macos.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +#ifdef CEMU_USB_TRACE +#include +#define HID_TRACE(...) fprintf(stderr, "[USBHID] " __VA_ARGS__) +#else +#define HID_TRACE(...) ((void)0) +#endif + +#define MAX_QUEUED_HID_REPORTS 32 +#define MAX_HID_REPORT_LENGTH 65536 + +typedef struct physical_hid_report physical_hid_report_t; + +struct physical_hid_report { + physical_hid_report_t *next; + size_t length; + uint8_t data[]; +}; + +struct physical_hid_device { + IOHIDDeviceRef device; + CFStringRef run_loop_mode; + CFRunLoopRef run_loop; + pthread_t thread; + pthread_mutex_t mutex; + pthread_cond_t condition; + physical_hid_report_t *reports, **reports_tail; + uint8_t *input_buffer; + CFIndex input_buffer_length; + size_t report_count; + bool mutex_initialized, condition_initialized; + bool device_opened, thread_started, thread_ready, shutdown, disconnected; +}; + +static long hid_number_property(IOHIDDeviceRef device, CFStringRef key) { + CFTypeRef value = IOHIDDeviceGetProperty(device, key); + long number = -1; + if (value && CFGetTypeID(value) == CFNumberGetTypeID()) { + CFNumberGetValue(value, kCFNumberLongType, &number); + } + return number; +} + +static long hid_interface_number(IOHIDDeviceRef device) { + io_registry_entry_t entry = IOHIDDeviceGetService(device); + bool entry_owned = false; + while (entry) { + CFTypeRef value = IORegistryEntryCreateCFProperty(entry, CFSTR("bInterfaceNumber"), kCFAllocatorDefault, 0); + long interface_number = -1; + if (value && CFGetTypeID(value) == CFNumberGetTypeID()) { + CFNumberGetValue(value, kCFNumberLongType, &interface_number); + } + if (value) { + CFRelease(value); + } + if (interface_number >= 0) { + if (entry_owned) { + IOObjectRelease(entry); + } + return interface_number; + } + io_registry_entry_t parent = IO_OBJECT_NULL; + kern_return_t result = IORegistryEntryGetParentEntry(entry, kIOServicePlane, &parent); + if (entry_owned) { + IOObjectRelease(entry); + } + if (result != KERN_SUCCESS) { + break; + } + entry = parent; + entry_owned = true; + } + return -1; +} + +static void hid_discard_first_report(physical_hid_device_t *device) { + physical_hid_report_t *report = device->reports; + if (!report) { + return; + } + device->reports = report->next; + if (!device->reports) { + device->reports_tail = &device->reports; + } + --device->report_count; + free(report); +} + +static void hid_report_callback(void *context, IOReturn result, void *sender, + IOHIDReportType type, uint32_t report_id, + uint8_t *report, CFIndex report_length) { + (void)sender; + (void)report_id; + if (result != kIOReturnSuccess || type != kIOHIDReportTypeInput || report_length <= 0) { + return; + } + physical_hid_device_t *device = context; + physical_hid_report_t *queued = + malloc(sizeof(*queued) + (size_t)report_length); + if (!queued) { + return; + } + queued->next = NULL; + queued->length = (size_t)report_length; + memcpy(queued->data, report, queued->length); + HID_TRACE("input report length=%zu\n", queued->length); + + pthread_mutex_lock(&device->mutex); + if (device->shutdown || device->disconnected) { + pthread_mutex_unlock(&device->mutex); + free(queued); + return; + } + *device->reports_tail = queued; + device->reports_tail = &queued->next; + if (++device->report_count > MAX_QUEUED_HID_REPORTS) { + hid_discard_first_report(device); + } + pthread_mutex_unlock(&device->mutex); +} + +static void hid_removal_callback(void *context, IOReturn result, void *sender) { + (void)result; + (void)sender; + physical_hid_device_t *device = context; + pthread_mutex_lock(&device->mutex); + device->disconnected = true; + CFRunLoopRef run_loop = device->run_loop; + if (run_loop) { + CFRetain(run_loop); + } + pthread_mutex_unlock(&device->mutex); + if (run_loop) { + CFRunLoopStop(run_loop); + CFRunLoopWakeUp(run_loop); + CFRelease(run_loop); + } +} + +static void *hid_run_loop(void *context) { + physical_hid_device_t *device = context; + CFRunLoopRef run_loop = CFRunLoopGetCurrent(); + CFRetain(run_loop); + IOHIDDeviceScheduleWithRunLoop(device->device, run_loop, device->run_loop_mode); + + pthread_mutex_lock(&device->mutex); + device->run_loop = run_loop; + device->thread_ready = true; + pthread_cond_signal(&device->condition); + pthread_mutex_unlock(&device->mutex); + + while (true) { + pthread_mutex_lock(&device->mutex); + bool done = device->shutdown || device->disconnected; + pthread_mutex_unlock(&device->mutex); + if (done) { + break; + } + SInt32 status = CFRunLoopRunInMode(device->run_loop_mode, 1, false); + if (status == kCFRunLoopRunFinished || status == kCFRunLoopRunStopped) { + pthread_mutex_lock(&device->mutex); + done = device->shutdown || device->disconnected; + pthread_mutex_unlock(&device->mutex); + if (!done) { + continue; + } + break; + } + } + + IOHIDDeviceUnscheduleFromRunLoop(device->device, run_loop, device->run_loop_mode); + pthread_mutex_lock(&device->mutex); + device->run_loop = NULL; + pthread_mutex_unlock(&device->mutex); + CFRelease(run_loop); + return NULL; +} + +static CFMutableDictionaryRef hid_matching_dictionary(uint16_t vendor_id, uint16_t product_id) { + CFMutableDictionaryRef matching = CFDictionaryCreateMutable( + kCFAllocatorDefault, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + if (!matching) { + return NULL; + } + int vendor = vendor_id, product = product_id; + CFNumberRef vendor_number = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &vendor); + CFNumberRef product_number = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &product); + if (!vendor_number || !product_number) { + if (vendor_number) { + CFRelease(vendor_number); + } + if (product_number) { + CFRelease(product_number); + } + CFRelease(matching); + return NULL; + } + CFDictionarySetValue(matching, CFSTR(kIOHIDVendorIDKey), vendor_number); + CFDictionarySetValue(matching, CFSTR(kIOHIDProductIDKey), product_number); + CFRelease(vendor_number); + CFRelease(product_number); + return matching; +} + +physical_hid_open_result_t physical_hid_open(uint16_t vendor_id, + uint16_t product_id, + uint8_t interface_number, + physical_hid_device_t **result) { + *result = NULL; + if (IOHIDCheckAccess(kIOHIDRequestTypeListenEvent) != kIOHIDAccessTypeGranted + && !IOHIDRequestAccess(kIOHIDRequestTypeListenEvent)) { + return PHYSICAL_HID_OPEN_PERMISSION_DENIED; + } + + IOHIDManagerRef manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); + CFMutableDictionaryRef matching = hid_matching_dictionary(vendor_id, product_id); + if (!manager || !matching) { + if (manager) { + CFRelease(manager); + } + if (matching) { + CFRelease(matching); + } + return PHYSICAL_HID_OPEN_FAILED; + } + IOHIDManagerSetDeviceMatching(manager, matching); + CFRelease(matching); + CFSetRef devices = IOHIDManagerCopyDevices(manager); + CFRelease(manager); + if (!devices || !CFSetGetCount(devices)) { + if (devices) { + CFRelease(devices); + } + return PHYSICAL_HID_OPEN_NOT_FOUND; + } + CFIndex device_count = CFSetGetCount(devices); + IOHIDDeviceRef *device_values = calloc((size_t)device_count, sizeof(*device_values)); + if (!device_values) { + CFRelease(devices); + return PHYSICAL_HID_OPEN_FAILED; + } + CFSetGetValues(devices, (const void **)device_values); + IOHIDDeviceRef hid_device = NULL; + for (CFIndex index = 0; index < device_count; ++index) { + if (hid_interface_number(device_values[index]) != interface_number) { + continue; + } + if (hid_device) { + free(device_values); + CFRelease(devices); + return PHYSICAL_HID_OPEN_AMBIGUOUS; + } + hid_device = device_values[index]; + } + if (!hid_device) { + free(device_values); + CFRelease(devices); + return PHYSICAL_HID_OPEN_NOT_FOUND; + } + CFRetain(hid_device); + free(device_values); + CFRelease(devices); + + physical_hid_device_t *device = calloc(1, sizeof(*device)); + if (!device) { + CFRelease(hid_device); + return PHYSICAL_HID_OPEN_FAILED; + } + device->device = hid_device; + device->reports_tail = &device->reports; + device->input_buffer_length = hid_number_property(hid_device, CFSTR(kIOHIDMaxInputReportSizeKey)); + if (device->input_buffer_length <= 0 || device->input_buffer_length > MAX_HID_REPORT_LENGTH) { + physical_hid_close(device); + return PHYSICAL_HID_OPEN_FAILED; + } + device->input_buffer = calloc((size_t)device->input_buffer_length, 1); + device->run_loop_mode = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, CFSTR("CEmuHID_%p"), device); + if (!device->input_buffer || !device->run_loop_mode) { + physical_hid_close(device); + return PHYSICAL_HID_OPEN_FAILED; + } + if (pthread_mutex_init(&device->mutex, NULL)) { + physical_hid_close(device); + return PHYSICAL_HID_OPEN_FAILED; + } + device->mutex_initialized = true; + if (pthread_cond_init(&device->condition, NULL)) { + physical_hid_close(device); + return PHYSICAL_HID_OPEN_FAILED; + } + device->condition_initialized = true; + + IOReturn open_result = IOHIDDeviceOpen(hid_device, kIOHIDOptionsTypeNone); + if (open_result != kIOReturnSuccess) { + physical_hid_close(device); + return open_result == kIOReturnNotPermitted + ? PHYSICAL_HID_OPEN_PERMISSION_DENIED + : PHYSICAL_HID_OPEN_FAILED; + } + device->device_opened = true; + IOHIDDeviceRegisterInputReportCallback( + hid_device, device->input_buffer, device->input_buffer_length, + hid_report_callback, device); + IOHIDDeviceRegisterRemovalCallback(hid_device, hid_removal_callback, device); + if (pthread_create(&device->thread, NULL, hid_run_loop, device)) { + physical_hid_close(device); + return PHYSICAL_HID_OPEN_FAILED; + } + device->thread_started = true; + pthread_mutex_lock(&device->mutex); + while (!device->thread_ready) { + pthread_cond_wait(&device->condition, &device->mutex); + } + pthread_mutex_unlock(&device->mutex); + *result = device; + HID_TRACE("opened %04x:%04x with input reports up to %ld bytes\n", + vendor_id, product_id, (long)device->input_buffer_length); + return PHYSICAL_HID_OPEN_SUCCESS; +} + +void physical_hid_close(physical_hid_device_t *device) { + if (!device) { + return; + } + if (device->thread_started) { + pthread_mutex_lock(&device->mutex); + device->shutdown = true; + CFRunLoopRef run_loop = device->run_loop; + if (run_loop) { + CFRetain(run_loop); + } + pthread_mutex_unlock(&device->mutex); + if (run_loop) { + CFRunLoopStop(run_loop); + CFRunLoopWakeUp(run_loop); + CFRelease(run_loop); + } + pthread_join(device->thread, NULL); + } + if (device->device_opened) { + IOHIDDeviceClose(device->device, kIOHIDOptionsTypeNone); + } + if (device->device) { + CFRelease(device->device); + } + if (device->run_loop_mode) { + CFRelease(device->run_loop_mode); + } + while (device->reports) { + hid_discard_first_report(device); + } + free(device->input_buffer); + if (device->condition_initialized) { + pthread_cond_destroy(&device->condition); + } + if (device->mutex_initialized) { + pthread_mutex_destroy(&device->mutex); + } + free(device); +} + +physical_hid_read_result_t physical_hid_read(physical_hid_device_t *device, + uint8_t *buffer, + size_t capacity, + size_t *length) { + *length = 0; + pthread_mutex_lock(&device->mutex); + if (device->disconnected) { + pthread_mutex_unlock(&device->mutex); + return PHYSICAL_HID_READ_DISCONNECTED; + } + physical_hid_report_t *report = device->reports; + if (!report) { + pthread_mutex_unlock(&device->mutex); + return PHYSICAL_HID_READ_PENDING; + } + *length = report->length < capacity ? report->length : capacity; + memcpy(buffer, report->data, *length); + hid_discard_first_report(device); + pthread_mutex_unlock(&device->mutex); + return PHYSICAL_HID_READ_COMPLETED; +} diff --git a/core/usb/physical_macos.h b/core/usb/physical_macos.h new file mode 100644 index 00000000..07a977aa --- /dev/null +++ b/core/usb/physical_macos.h @@ -0,0 +1,27 @@ +#ifndef CEMU_USB_PHYSICAL_MACOS_H +#define CEMU_USB_PHYSICAL_MACOS_H + +#include +#include + +typedef struct physical_hid_device physical_hid_device_t; + +typedef enum physical_hid_open_result { + PHYSICAL_HID_OPEN_SUCCESS, + PHYSICAL_HID_OPEN_NOT_FOUND, + PHYSICAL_HID_OPEN_AMBIGUOUS, + PHYSICAL_HID_OPEN_PERMISSION_DENIED, + PHYSICAL_HID_OPEN_FAILED, +} physical_hid_open_result_t; + +typedef enum physical_hid_read_result { + PHYSICAL_HID_READ_PENDING, + PHYSICAL_HID_READ_COMPLETED, + PHYSICAL_HID_READ_DISCONNECTED, +} physical_hid_read_result_t; + +physical_hid_open_result_t physical_hid_open(uint16_t vendor_id, uint16_t product_id, uint8_t interface_number, physical_hid_device_t **result); +void physical_hid_close(physical_hid_device_t *device); +physical_hid_read_result_t physical_hid_read(physical_hid_device_t *device, uint8_t *buffer, size_t capacity, size_t *length); + +#endif diff --git a/gui/qt/CEmu.pro b/gui/qt/CEmu.pro index f20238df..3906d944 100644 --- a/gui/qt/CEmu.pro +++ b/gui/qt/CEmu.pro @@ -200,7 +200,8 @@ QMAKE_LFLAGS += $$GLOBAL_FLAGS if(macx) { QMAKE_MACOSX_DEPLOYMENT_TARGET = 10.15 ICON = resources/icons/icon.icns - LIBS += -framework Cocoa + LIBS += -framework Cocoa -framework CoreFoundation -framework IOKit + SOURCES += ../../core/usb/physical_macos.c } SOURCES += \ diff --git a/gui/qt/CMakeLists.txt b/gui/qt/CMakeLists.txt index 571f78ea..935e8cae 100644 --- a/gui/qt/CMakeLists.txt +++ b/gui/qt/CMakeLists.txt @@ -292,11 +292,16 @@ if(APPLE) set(app_icon_macos "${CMAKE_CURRENT_SOURCE_DIR}/resources/icons/icon.icns") set_source_files_properties(${app_icon_macos} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") target_sources(CEmu PUBLIC + ../../core/usb/physical_macos.c os/mac/kdmactouchbar.h os/mac/kdmactouchbar.mm os/mac/kdmactouchbar_global.h ${app_icon_macos} ) - target_link_libraries(CEmu PRIVATE "-framework Cocoa") + target_link_libraries(CEmu PRIVATE + "-framework Cocoa" + "-framework CoreFoundation" + "-framework IOKit" + ) set_target_properties(CEmu PROPERTIES MACOSX_FRAMEWORK_IDENTIFIER "com.adriweb.CEmu" MACOSX_BUNDLE_COPYRIGHT "CE-Programming team" From ec3a10eb289da2d9107938df40049c4ea5edcd51 Mon Sep 17 00:00:00 2001 From: Adrien Bertrand Date: Sat, 15 Aug 2026 20:02:21 +0200 Subject: [PATCH 10/11] core: usb: model opt-in asynchronous HNP peers --- core/usb/device.h | 10 ++ core/usb/usb.c | 278 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 277 insertions(+), 11 deletions(-) diff --git a/core/usb/device.h b/core/usb/device.h index d2db4d5f..95d88440 100644 --- a/core/usb/device.h +++ b/core/usb/device.h @@ -15,6 +15,12 @@ typedef enum usb_event_type { USB_SESSION_START_EVENT, USB_SESSION_END_EVENT, USB_SESSION_REQUEST_EVENT, + USB_HNP_EVENT, + USB_ROLE_SWITCH_EVENT, + USB_ROLE_SWITCH_READY_EVENT, + USB_ROLE_RESTORE_REQUEST_EVENT, + USB_ROLE_RESTORE_EVENT, + USB_ROLE_RESTORE_READY_EVENT, USB_TRANSFER_REQUEST_EVENT, USB_TRANSFER_RESPONSE_EVENT, USB_TIMER_EVENT, @@ -75,6 +81,10 @@ typedef struct usb_event { usb_progress_handler_t *progress_handler; void *progress_context, *context; bool host : 1; + /* Set by an asynchronous peer backend for the current dispatch only. */ + bool deferred : 1; + /* Set during initialization by backends which coordinate OTG HNP. */ + bool supports_hnp : 1; bool pending : 1; uint8_t speed : 2; /* usb_speed_t */ usb_event_type_t type; diff --git a/core/usb/usb.c b/core/usb/usb.c index e4d0c2a3..38fdac29 100644 --- a/core/usb/usb.c +++ b/core/usb/usb.c @@ -14,8 +14,15 @@ #include #define CONTROL_MPS 0x40 +#define PORTSC_SUSPEND (UINT32_C(1) << 7) usb_state_t usb; +static uint8_t usb_in_pending; +static bool usb_role_switch_pending; +static bool usb_a_role_switch_pending; +static bool usb_role_restore_pending; +static bool usb_a_role_restore_pending; +static bool usb_b_role_restore_pending; typedef enum usb_qtype { QTYPE_ITD, QTYPE_QH, QTYPE_SITD, QTYPE_FSTN @@ -287,6 +294,80 @@ static void usb_sync_a_host_port(void) { } } +/* HNP changes controller ownership without changing which end of the cable + * carries the A/B connector identity. Keep the identity bit stable and only + * move the role bit; the guest driver owns controller teardown and + * reinitialization after the role-change interrupt. */ +static void usb_hnp_b_to_host(void) { + usb.regs.otgcsr |= OTGCSR_DEV_B; + usb.regs.otgcsr &= ~OTGCSR_ROLE_D; + usb.regs.hcor.portsc[0] &= ~(PORTSC_J_STATE | PORTSC_K_STATE | + PORTSC_CONN_STATUS | PORTSC_EN_STATUS); + usb_otg_int(OTGISR_RLCHG); +} + +static void usb_hnp_a_to_device(void) { + bool port_changed; + usb.regs.hcor.portsc[0] &= ~(PORTSC_J_STATE | PORTSC_K_STATE); + port_changed = usb_update_status_change(usb.regs.hcor.portsc, + PORTSC_CONN_STATUS, + PORTSC_CONN_CHANGE, false); + port_changed |= usb_update_status_change(usb.regs.hcor.portsc, + PORTSC_EN_STATUS, + PORTSC_EN_CHANGE, false); + if (port_changed) { + usb_host_int(USBSTS_PORT_CHANGE); + } + usb.regs.otgcsr &= ~OTGCSR_DEV_B; + usb.regs.otgcsr |= OTGCSR_ROLE_D; + usb.regs.hcor.portsc[0] &= ~PORTSC_SUSPEND; + usb_otg_int(OTGISR_RLCHG); +} + +static void usb_hnp_b_host_peer_ready(void) { + uint32_t line_state = usb.event.speed == USB_LOW_SPEED + ? PORTSC_K_STATE : PORTSC_J_STATE; + usb.regs.hcor.portsc[0] &= ~(PORTSC_J_STATE | PORTSC_K_STATE); + usb.regs.hcor.portsc[0] |= line_state; + if (usb_update_status_change(usb.regs.hcor.portsc, + PORTSC_CONN_STATUS, + PORTSC_CONN_CHANGE, true)) { + usb_host_int(USBSTS_PORT_CHANGE); + } +} + +static void usb_hnp_b_to_device(void) { + usb.regs.hcor.portsc[0] &= ~(PORTSC_J_STATE | PORTSC_K_STATE); + usb.regs.hcor.portsc[0] &= ~(PORTSC_CONN_STATUS | PORTSC_CONN_CHANGE | + PORTSC_EN_STATUS | PORTSC_EN_CHANGE | + PORTSC_SUSPEND); + usb.regs.hcor.usbcmd &= ~USBCMD_RUN; + usb.regs.hcor.usbsts &= ~UINT32_C(0x3F); + usb.regs.hcor.usbsts |= USBSTS_HCHALTED; + usb.regs.otgcsr |= OTGCSR_DEV_B | OTGCSR_ROLE_D; + usb_update(); + usb_otg_int(OTGISR_RLCHG); +} + +static void usb_hnp_a_to_host(void) { + /* A resumes ownership of the same live OTG session. Driving VBUS again + * is part of returning to A-host; retaining A_BUSDROP makes the freshly + * restored port disconnect on the next status synchronization. */ + usb.regs.otgcsr &= ~(OTGCSR_DEV_B | OTGCSR_ROLE_D | OTGCSR_A_BUSDROP); + usb.regs.otgcsr |= OTGCSR_A_BUSREQ; + usb_sync_a_host_port(); + usb_otg_int(OTGISR_RLCHG); +} + +static void usb_reset_peer_state(void) { + usb_in_pending = 0; + usb_role_switch_pending = false; + usb_a_role_switch_pending = false; + usb_role_restore_pending = false; + usb_a_role_restore_pending = false; + usb_b_role_restore_pending = false; +} + static void usb_plug(void) { if (usb.event.host) { usb_plug_b(); @@ -565,6 +646,7 @@ static int usb_dispatch_event(usb_traversal_state_t *state) { usb_transfer_info_t *transfer = &usb.event.info.transfer; usb_timer_info_t *timer = &usb.event.info.timer; do { + usb.event.deferred = false; error = usb.device(&usb.event); if (error) { usb.event.type = USB_DESTROY_EVENT; @@ -583,6 +665,10 @@ static int usb_dispatch_event(usb_traversal_state_t *state) { case USB_SESSION_START_EVENT: if (usb.event.host && (usb.regs.otgcsr & OTGCSR_ROLE_D)) { usb_plug_b(); + if (usb.event.supports_hnp && + (usb.regs.otgcsr & OTGCSR_B_BUSREQ)) { + usb_otg_int(OTGISR_BSRP); + } } break; case USB_SESSION_END_EVENT: @@ -591,6 +677,64 @@ static int usb_dispatch_event(usb_traversal_state_t *state) { } break; case USB_SESSION_REQUEST_EVENT: + if (usb.event.supports_hnp && !usb.event.host && + !(usb.regs.otgcsr & (OTGCSR_DEV_B | OTGCSR_ROLE_D))) { + usb_otg_int(OTGISR_ASRP); + } + break; + case USB_HNP_EVENT: + if (usb.event.supports_hnp && usb.event.host && + (usb.regs.otgcsr & OTGCSR_ROLE_D)) { + if (!(usb.regs.otgcsr & OTGCSR_DEV_B)) { + usb_a_role_restore_pending = true; + } + usb_grp2_int(GISR2_SUSPEND); + if (usb.regs.otgcsr & OTGCSR_B_BUSREQ) { + usb.event.type = USB_ROLE_SWITCH_EVENT; + continue; + } + } + break; + case USB_ROLE_SWITCH_EVENT: + if (usb.event.supports_hnp && !usb.event.host && + !(usb.regs.otgcsr & OTGCSR_ROLE_D)) { + bool port_changed; + usb.regs.hcor.portsc[0] &= ~(PORTSC_J_STATE | PORTSC_K_STATE); + port_changed = usb_update_status_change( + usb.regs.hcor.portsc, PORTSC_CONN_STATUS, + PORTSC_CONN_CHANGE, false); + port_changed |= usb_update_status_change( + usb.regs.hcor.portsc, PORTSC_EN_STATUS, + PORTSC_EN_CHANGE, false); + if (port_changed) { + usb_host_int(USBSTS_PORT_CHANGE); + } + usb_a_role_switch_pending = true; + } + break; + case USB_ROLE_SWITCH_READY_EVENT: + if (usb.event.supports_hnp && !usb.event.host && + (usb.regs.otgcsr & (OTGCSR_DEV_B | OTGCSR_ROLE_D)) == + (OTGCSR_DEV_B | OTGCSR_ROLE_D)) { + usb_hnp_b_to_host(); + usb_hnp_b_host_peer_ready(); + } + break; + case USB_ROLE_RESTORE_REQUEST_EVENT: + break; + case USB_ROLE_RESTORE_EVENT: + if (usb.event.supports_hnp && !usb.event.host && + (usb.regs.otgcsr & (OTGCSR_DEV_B | OTGCSR_ROLE_D)) == + OTGCSR_DEV_B) { + usb_b_role_restore_pending = true; + } + break; + case USB_ROLE_RESTORE_READY_EVENT: + if (usb.event.supports_hnp && usb.event.host && + (usb.regs.otgcsr & (OTGCSR_DEV_B | OTGCSR_ROLE_D)) == + OTGCSR_ROLE_D) { + usb_hnp_a_to_host(); + } break; case USB_TRANSFER_REQUEST_EVENT: if (usb.event.host) { @@ -638,17 +782,48 @@ static int usb_dispatch_event(usb_traversal_state_t *state) { break; case USB_TRANSFER_RESPONSE_EVENT: if (usb.event.host) { - if (!transfer->direction) { + if (usb.event.deferred && !transfer->direction) { uint32_t dma_length = DMACTRL_LEN(usb.regs.dma_ctrl); uint32_t write_length = transfer->length < dma_length ? transfer->length : dma_length; if (write_length) { mem_dma_write(transfer->buffer, usb.regs.dma_addr, write_length); } - } - usb.regs.dma_ctrl &= ~DMACTRL_START; - if (transfer->length) { - //gui_console_printf("usb_grp2_int(%s);\n", transfer->status == USB_TRANSFER_COMPLETED ? "GISR2_DMAFIN" : "GISR2_DMAERR"); - usb_grp2_int(transfer->status == USB_TRANSFER_COMPLETED ? GISR2_DMAFIN : GISR2_DMAERR); + usb.regs.dma_ctrl &= ~DMACTRL_START; + if (transfer->endpoint && transfer->endpoint <= 8) { + uint8_t fifo = EPMAP_GET_OUT( + usb.regs.epmap[transfer->endpoint - 1]); + if (!(usb_in_pending & 1U << fifo)) { + usb.regs.cxfifo |= CXFIFO_FIFOE(fifo); + usb_grp1_int(GISR1_IN_FIFO(fifo)); + } + usb.regs.fifocsr[fifo] &= FIFOCSR_RESET; + } + if (transfer->length) { + usb_grp2_int(transfer->status == USB_TRANSFER_COMPLETED + ? GISR2_DMAFIN : GISR2_DMAERR); + } + } else if (usb.event.deferred && transfer->endpoint && + transfer->endpoint <= 8) { + uint8_t fifo = EPMAP_GET_IN( + usb.regs.epmap[transfer->endpoint - 1]); + usb_in_pending &= ~(1U << fifo); + usb.regs.cxfifo |= CXFIFO_FIFOE(fifo); + usb_grp1_int(GISR1_IN_FIFO(fifo)); + } else { + if (!transfer->direction) { + uint32_t dma_length = DMACTRL_LEN(usb.regs.dma_ctrl); + uint32_t write_length = transfer->length < dma_length + ? transfer->length : dma_length; + if (write_length) { + mem_dma_write(transfer->buffer, usb.regs.dma_addr, + write_length); + } + } + usb.regs.dma_ctrl &= ~DMACTRL_START; + if (transfer->length) { + usb_grp2_int(transfer->status == USB_TRANSFER_COMPLETED + ? GISR2_DMAFIN : GISR2_DMAERR); + } } } else if (state) { usb_itd_xact_t *xact = &state->itd.xacts[usb.regs.hcor.frindex & 7]; @@ -1073,6 +1248,9 @@ static void usb_schedule_traverse(usb_traversal_state_t *state) { if (state->dirty) { mem_dma_write(&state->qh, state->link.ptr << 5, sizeof(state->qh)); } + if (usb.event.deferred && usb.event.pending) { + return; + } state->link = state->qh.horiz; break; case QTYPE_SITD: @@ -1139,6 +1317,7 @@ static void usb_reset_otg(void) { usb.regs.fifocfg = 0; clear(usb.regs.fifocsr); usb.regs.dma_fifo = 0; + usb_reset_peer_state(); clear(usb.regs.rsvd8); usb.regs.dma_ctrl = 0; clear(usb.ep0_data); @@ -1182,6 +1361,8 @@ int usb_plug_device(int argc, const char *const *argv, usb.event.progress_handler = progress_handler; usb.event.progress_context = progress_context; usb.event.context = NULL; + usb.event.deferred = false; + usb.event.supports_hnp = false; usb.event.speed = USB_FULL_SPEED; usb.event.type = USB_INIT_EVENT; usb.event.info.init.argc = argc; @@ -1197,6 +1378,11 @@ int usb_plug_device(int argc, const char *const *argv, static void usb_event(enum sched_item_id event) { bool high_speed = false; + if (usb_a_role_switch_pending) { + usb_a_role_switch_pending = false; + usb_hnp_a_to_device(); + usb_role_switch_pending = true; + } if (usb.regs.otgcsr & OTGCSR_A_VBUS_VLD) { if (usb.regs.otgcsr & OTGCSR_ROLE_D) { high_speed = usb.regs.dev_ctrl & DEVCTRL_HS; @@ -1238,6 +1424,11 @@ static void usb_event(enum sched_item_id event) { } } } + if (usb_b_role_restore_pending) { + usb_b_role_restore_pending = false; + usb_hnp_b_to_device(); + usb_role_restore_pending = true; + } sched_repeat(event, high_speed ? 1500 : 12000); } @@ -1249,7 +1440,7 @@ static void usb_device_event(enum sched_item_id event) { uint8_t usb_status(void) { return (usb.regs.otgcsr & (OTGCSR_A_VBUS_VLD | OTGCSR_A_SESS_VLD | OTGCSR_B_SESS_VLD) ? 0x80 : 0) | - (usb.regs.otgcsr & (OTGCSR_DEV_B | OTGCSR_ROLE_D) ? 0x40 : 0); + (usb.regs.otgcsr & OTGCSR_DEV_B ? 0x40 : 0); } static uint8_t usb_ep0_idx_update(void) { @@ -1350,6 +1541,14 @@ static void usb_write(uint16_t pio, uint8_t value, bool poke) { break; } } + if (usb.event.supports_hnp && !(old & PORTSC_SUSPEND) && + (usb.regs.hcor.portsc[0] & PORTSC_SUSPEND) && + (usb.regs.otgcsr & OTGCSR_A_HNP) && + !(usb.regs.otgcsr & (OTGCSR_DEV_B | OTGCSR_ROLE_D)) && + usb.device != usb_disconnected_device && !usb.event.host) { + usb.event.type = USB_HNP_EVENT; + usb_dispatch_event(NULL); + } break; case 0x040 >> 2: // Miscellaneous Register write8(usb.regs.miscr, bit_offset, value & 0xFFF >> bit_offset); // W mask (V) @@ -1380,7 +1579,21 @@ static void usb_write(uint16_t pio, uint8_t value, bool poke) { } break; case 0x084 >> 2: // OTG Interrupt Status Register - usb.regs.otgisr &= ~((uint32_t)value << bit_offset & OTGISR_MASK); // WC mask (V) + { + uint32_t cleared = (uint32_t)value << bit_offset & OTGISR_MASK; + usb.regs.otgisr &= ~cleared; // WC mask (V) + if (usb.event.supports_hnp && usb_role_switch_pending && + (cleared & OTGISR_RLCHG) && + (usb.regs.otgcsr & (OTGCSR_DEV_B | OTGCSR_ROLE_D)) == + OTGCSR_ROLE_D) { + /* A's guest has acknowledged that it is now the device. + * TI-OS does not necessarily rewrite PHYTMSR when it was + * already clear. */ + usb_role_switch_pending = false; + usb.event.type = USB_ROLE_SWITCH_READY_EVENT; + usb_dispatch_event(NULL); + } + } break; case 0x088 >> 2: // OTG Interrupt Enable Register write8(usb.regs.otgier, bit_offset, value & OTGISR_MASK >> bit_offset); // W mask (V) @@ -1405,6 +1618,27 @@ static void usb_write(uint16_t pio, uint8_t value, bool poke) { break; case 0x114 >> 2: // PHY Test Mode Selector Register write8(usb.regs.phy_tmsr, bit_offset, value & 0x1F >> bit_offset); // W mask (V) + if (usb.event.supports_hnp && usb_a_role_restore_pending && + (usb.regs.otgcsr & (OTGCSR_DEV_B | OTGCSR_ROLE_D)) == + OTGCSR_ROLE_D && + (usb.regs.phy_tmsr & PHYTMSR_UNPLUG)) { + usb_a_role_restore_pending = false; + usb.event.type = USB_ROLE_RESTORE_REQUEST_EVENT; + usb_dispatch_event(NULL); + } else if (usb.event.supports_hnp && usb_role_restore_pending && + (usb.regs.otgcsr & (OTGCSR_DEV_B | OTGCSR_ROLE_D)) == + (OTGCSR_DEV_B | OTGCSR_ROLE_D) && + !(usb.regs.phy_tmsr & PHYTMSR_UNPLUG)) { + usb_role_restore_pending = false; + usb.event.type = USB_ROLE_RESTORE_READY_EVENT; + usb_dispatch_event(NULL); + } else if (usb.event.supports_hnp && usb_role_switch_pending && + (usb.regs.otgcsr & OTGCSR_ROLE_D) && + !(usb.regs.phy_tmsr & PHYTMSR_UNPLUG)) { + usb_role_switch_pending = false; + usb.event.type = USB_ROLE_SWITCH_READY_EVENT; + usb_dispatch_event(NULL); + } break; case 0x118 >> 2: // unknown write8(usb.regs.rsvd5[0], bit_offset, value & 0x3F >> bit_offset); // W mask (V) @@ -1436,7 +1670,13 @@ static void usb_write(uint16_t pio, uint8_t value, bool poke) { write8(usb.regs.gimr0, bit_offset, value & GIMR0_MASK >> bit_offset); // W mask (V) break; case 0x138 >> 2: // Group Interrupt Mask Register 1 + old = usb.regs.gimr1; write8(usb.regs.gimr1, bit_offset, value & GIMR1_MASK >> bit_offset); // W mask (V) + if (usb.event.host && usb.event.deferred && + (old & ~usb.regs.gimr1 & 0xFF)) { + usb.event.type = USB_TIMER_EVENT; + usb_dispatch_event(NULL); + } break; case 0x13C >> 2: // Group Interrupt Mask Register 2 write8(usb.regs.gimr2, bit_offset, value & GIMR2_MASK >> bit_offset); // W mask (V) @@ -1515,9 +1755,17 @@ static void usb_write(uint16_t pio, uint8_t value, bool poke) { transfer->max_pkt_size = EP_MAXPS((transfer->direction ? usb.regs.iep : usb.regs.oep)[transfer->endpoint - 1]); - usb.regs.cxfifo |= CXFIFO_FIFOE(fifo); - usb.regs.gisr1 &= ~GISR1_RX_FIFO(fifo); - usb_grp1_int(GISR1_IN_FIFO(fifo)); + if (usb.event.deferred && transfer->direction) { + usb_in_pending |= 1U << fifo; + usb.regs.cxfifo &= ~CXFIFO_FIFOE(fifo); + usb.regs.gisr1 &= ~GISR1_IN_FIFO(fifo); + } else if (usb.event.deferred) { + usb.regs.gisr1 &= ~GISR1_RX_FIFO(fifo); + } else { + usb.regs.cxfifo |= CXFIFO_FIFOE(fifo); + usb.regs.gisr1 &= ~GISR1_RX_FIFO(fifo); + usb_grp1_int(GISR1_IN_FIFO(fifo)); + } usb.regs.fifocsr[fifo] &= FIFOCSR_RESET; break; } @@ -1529,6 +1777,10 @@ static void usb_write(uint16_t pio, uint8_t value, bool poke) { } //usb_transfer_info_t debug_transfer = *transfer; mem_dma_read(transfer->buffer, usb.regs.dma_addr, dma_length); + if (usb.event.deferred && transfer->direction) { + usb.regs.dma_ctrl &= ~DMACTRL_START; + usb_grp2_int(GISR2_DMAFIN); + } usb_dispatch_event(NULL); //gui_console_printf("[USB] %c", debug_transfer.direction ? 'R' : 'S'); //for (uint32_t i = 0; i != DMACTRL_LEN(usb.regs.dma_ctrl); ++i) @@ -1595,6 +1847,10 @@ bool usb_restore(FILE *image) { void *context = usb.event.context; bool success = fread(&usb, offsetof(usb_state_t, device), 1, image) == 1; usb.event.context = context; + usb.event.deferred = false; + usb.event.supports_hnp = false; + usb.event.pending = false; + usb_reset_peer_state(); usb_init_hccr(); // hccr is read only // these bits are raz usb.regs.hcor.periodiclistbase &= 0xFFFFF000; From 80590f82fed015dbc61b71010fa822498577e33f Mon Sep 17 00:00:00 2001 From: Adrien Bertrand Date: Sat, 15 Aug 2026 23:56:20 +0200 Subject: [PATCH 11/11] core: usb: keep native calculator USB sessions host-owned --- core/usb/physical.c | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/core/usb/physical.c b/core/usb/physical.c index 5bdbb68b..fe08b93d 100644 --- a/core/usb/physical.c +++ b/core/usb/physical.c @@ -20,6 +20,9 @@ #define MAX_PORT_DEPTH 7 +/* USB 2.0 OTG feature selector. */ +#define USB_FEATURE_A_HNP_SUPPORT 4 + #ifdef CEMU_USB_TRACE #define PHYSICAL_TRACE(...) fprintf(stderr, "[USBPHY] " __VA_ARGS__) #else @@ -626,6 +629,22 @@ static device_reset_result_t device_reset(context_t *context, device_t *device) endpoint_cleanup(context, &device->endpoints[index]); } #ifdef __APPLE__ + int active_configuration = 0; + if (!device->numPorts && + libusb_get_configuration(device->handle, &active_configuration) == + LIBUSB_SUCCESS && + active_configuration > 0) { + /* + * Darwin may re-enumerate an already-configured device well after + * libusb_reset_device() returns, invalidating the live TI-OS DUSB + * session. Reset the emulated host-controller view while retaining + * the physical session which already has the requested configuration. + */ + device->disconnected = false; + device->reset_pending = false; + PHYSICAL_TRACE("virtualized guest reset for configured Darwin device\n"); + return DEVICE_RESET_COMPLETED; + } for (uint8_t interface = 0; interface < 32; ++interface) { physical_hid_close(device->hid_interfaces[interface]); } @@ -948,6 +967,23 @@ static int device_intercept_control_setup(context_t *context, device_t *device, break; case LIBUSB_REQUEST_SET_FEATURE: switch (setup->bmRequestType) { + case LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_STANDARD | LIBUSB_RECIPIENT_DEVICE: + if (!device->numPorts + && setup->wValue == USB_FEATURE_A_HNP_SUPPORT + && !setup->wIndex && !setup->wLength) { + /* + * The emulated A-device can service this peer only + * through the computer's host controller. Passing + * A_HNP_SUPPORT downstream promises a physical role + * swap which libusb cannot represent, so acknowledge + * it only in the emulated host-controller view. + */ + PHYSICAL_TRACE("accepted A_HNP_SUPPORT without forwarding it to the physical peer\n"); + *status = LIBUSB_TRANSFER_COMPLETED; + } else { + return USB_SUCCESS; + } + break; case LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_DEVICE: if (!device->numPorts) { return USB_SUCCESS;