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(); } diff --git a/core/usb/device.h b/core/usb/device.h index 20b64902..95d88440 100644 --- a/core/usb/device.h +++ b/core/usb/device.h @@ -12,6 +12,15 @@ 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_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, @@ -72,6 +81,11 @@ 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; union { 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 876a9402..fe08b93d 100644 --- a/core/usb/physical.c +++ b/core/usb/physical.c @@ -14,8 +14,21 @@ #include #include +#ifdef __APPLE__ +#include "physical_macos.h" +#endif + #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 +#define PHYSICAL_TRACE(...) ((void)0) +#endif + #define NODE_EMPTY(head) \ ((head)->next == (head)) @@ -58,6 +71,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, @@ -127,6 +141,16 @@ struct device { endpoint_t endpoints[0x20]; uint8_t state : 2; /* enum device_state */ uint8_t address : 7, numPorts : 7; + 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; +#ifdef __APPLE__ + physical_hid_device_t *hid_interfaces[32]; + physical_hid_device_t *hid_endpoint_devices[32]; +#endif hub_t hub; }; @@ -134,7 +158,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 +257,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 +275,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 +304,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 +315,20 @@ 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->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; +#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) { @@ -331,48 +376,79 @@ 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) { + 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]); } - 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; +#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 + * 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,53 +461,291 @@ static device_t *device_detach(context_t *context, device_t *device) { return device; } -static bool device_reset(context_t *context, device_t *device) { - switch (libusb_reset_device(device->handle)) { - case LIBUSB_SUCCESS: - return true; - case LIBUSB_ERROR_NO_DEVICE: - break; - default: - return false; +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); + } } - uint8_t ports[MAX_PORT_DEPTH]; - uint8_t num_ports = libusb_get_port_numbers( - libusb_get_device(device->handle), ports, MAX_PORT_DEPTH); - 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)) { - break; - } +} + +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->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; } - if (*enumerate_device) { - libusb_close(device->handle); - device->handle = NULL; - if (errno_from_libusb_error( - libusb_open(*enumerate_device, &device->handle)) != USB_SUCCESS) { - break; - } - return true; + } + } +} + +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); } - struct timeval tv = { - .tv_sec = 0, - .tv_usec = 1000000, - }; - if (errno_from_libusb_error( - libusb_handle_events_timeout(context->context, &tv)) != USB_SUCCESS) { + } + 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; +} + +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); + 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; } } - device_detach(context, device); - 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 transfer, including waits owned by IOHID. */ + for (uint8_t index = 0; index != 0x20; ++index) { + 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]); + } + 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, + 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; + } + /* + * 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) { + return DEVICE_RESET_FAILED; + } + 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; + } + } + } + } else { + device->reset_pending = false; + device_detach(context, device); + } + } } static void LIBUSB_CALL transfer_completed(struct libusb_transfer *libusb_transfer) { @@ -439,6 +753,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; @@ -535,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; @@ -564,7 +1013,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; @@ -573,7 +1024,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; } @@ -724,32 +1175,44 @@ 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); + 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; + } +#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", @@ -945,6 +1408,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; } @@ -1012,11 +1488,34 @@ 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); + } } +#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) { - 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) { @@ -1076,6 +1575,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) { @@ -1084,14 +1584,25 @@ 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) { - 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 +1642,9 @@ 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; timer->useconds = 1000; @@ -1143,7 +1657,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 +1697,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; @@ -1287,26 +1792,48 @@ 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; } 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) { + if (device->reset_pending && !transfer->address) { + event->pending = true; + break; + } 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; 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; @@ -1332,6 +1859,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; } 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/core/usb/usb.c b/core/usb/usb.c index ab31f979..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 @@ -190,15 +197,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,20 +222,150 @@ 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) { - 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)) { + uint32_t old = usb.regs.otgcsr; + uint16_t interrupts = 0; + 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 | OTGCSR_ROLE_D; + 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); + } +} + +/* 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_otg_int(OTGISR_APRM | OTGISR_IDCHG | OTGISR_RLCHG); - usb_plug_b(); + 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) { @@ -233,7 +376,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(); @@ -243,6 +386,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(); @@ -274,7 +420,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) { @@ -288,6 +437,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); @@ -488,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; @@ -503,6 +662,80 @@ 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(); + if (usb.event.supports_hnp && + (usb.regs.otgcsr & OTGCSR_B_BUSREQ)) { + usb_otg_int(OTGISR_BSRP); + } + } + 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: + 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) { uint8_t endpoint = transfer->endpoint; @@ -549,16 +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); } - } - 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]; @@ -626,7 +891,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: @@ -730,6 +995,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)) { @@ -748,7 +1015,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; + } } } @@ -760,6 +1036,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)) { @@ -778,7 +1056,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) { @@ -805,6 +1091,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) { @@ -835,7 +1123,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; + } } } @@ -878,15 +1175,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; @@ -950,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: @@ -1016,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); @@ -1027,42 +1329,60 @@ 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_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; + 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; 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; } 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; @@ -1084,17 +1404,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); @@ -1103,7 +1424,12 @@ static void usb_event(enum sched_item_id event) { } } } - sched_repeat(event, high_speed ? 1 : 8); + 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); } static void usb_device_event(enum sched_item_id event) { @@ -1114,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) { @@ -1181,14 +1507,17 @@ 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. - 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; @@ -1212,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) @@ -1223,14 +1560,40 @@ 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.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 && + (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 - 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) @@ -1255,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) @@ -1286,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) @@ -1365,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; } @@ -1379,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) @@ -1445,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; @@ -1457,5 +1863,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; } 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"