From 541dbdfead2a4a6cb08d1436fec1094b1a198276 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Wed, 11 Jun 2025 07:07:55 -0500 Subject: [PATCH 1/6] IOS: Move DoStateForMessage from BTEmu to BTBase. --- Source/Core/Core/IOS/USB/Bluetooth/BTBase.h | 15 ++++++++++++--- Source/Core/Core/IOS/USB/Bluetooth/BTEmu.cpp | 12 ------------ 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/Source/Core/Core/IOS/USB/Bluetooth/BTBase.h b/Source/Core/Core/IOS/USB/Bluetooth/BTBase.h index 44cc011290..7a93f4f7d4 100644 --- a/Source/Core/Core/IOS/USB/Bluetooth/BTBase.h +++ b/Source/Core/Core/IOS/USB/Bluetooth/BTBase.h @@ -3,9 +3,6 @@ #pragma once -#include -#include - #include "Common/CommonTypes.h" #include "Core/IOS/Device.h" #include "Core/IOS/IOS.h" @@ -15,6 +12,18 @@ class SysConf; namespace IOS::HLE { +template +static void DoStateForMessage(EmulationKernel& ios, PointerWrap& p, std::unique_ptr& message) +{ + u32 request_address = (message != nullptr) ? message->ios_request.address : 0; + p.Do(request_address); + if (request_address != 0) + { + IOCtlVRequest request{ios.GetSystem(), request_address}; + message = std::make_unique(ios, request); + } +} + void BackUpBTInfoSection(const SysConf* sysconf); void RestoreBTInfoSection(SysConf* sysconf); diff --git a/Source/Core/Core/IOS/USB/Bluetooth/BTEmu.cpp b/Source/Core/Core/IOS/USB/Bluetooth/BTEmu.cpp index 8e89cc927f..000883ec3f 100644 --- a/Source/Core/Core/IOS/USB/Bluetooth/BTEmu.cpp +++ b/Source/Core/Core/IOS/USB/Bluetooth/BTEmu.cpp @@ -79,18 +79,6 @@ BluetoothEmuDevice::BluetoothEmuDevice(EmulationKernel& ios, const std::string& BluetoothEmuDevice::~BluetoothEmuDevice() = default; -template -static void DoStateForMessage(EmulationKernel& ios, PointerWrap& p, std::unique_ptr& message) -{ - u32 request_address = (message != nullptr) ? message->ios_request.address : 0; - p.Do(request_address); - if (request_address != 0) - { - IOCtlVRequest request{ios.GetSystem(), request_address}; - message = std::make_unique(ios, request); - } -} - void BluetoothEmuDevice::DoState(PointerWrap& p) { bool passthrough_bluetooth = false; From 350ec54779f9f52312f6bdd358fc63bd1d716786 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Wed, 11 Jun 2025 07:08:04 -0500 Subject: [PATCH 2/6] BTReal: Improvements: Separate LibUSB logic into LibUSBBluetoothAdapter class. Submit transfers on thread with proper timing. Throttle before ACL input for reduced input latency. Immediately send IPC replies for outgoing data. Continuously submit libusb transfers to fill HCI/ACL input queues. Simplify endpoint handling and state saving. Other cleanups. --- Source/Core/Core/CMakeLists.txt | 2 + Source/Core/Core/IOS/USB/Bluetooth/BTReal.cpp | 874 ++++++------------ Source/Core/Core/IOS/USB/Bluetooth/BTReal.h | 85 +- .../USB/Bluetooth/LibUSBBluetoothAdapter.cpp | 596 ++++++++++++ .../USB/Bluetooth/LibUSBBluetoothAdapter.h | 157 ++++ Source/Core/DolphinLib.props | 2 + .../Config/WiimoteControllersWidget.cpp | 19 +- .../Config/WiimoteControllersWidget.h | 4 +- 8 files changed, 1083 insertions(+), 656 deletions(-) create mode 100644 Source/Core/Core/IOS/USB/Bluetooth/LibUSBBluetoothAdapter.cpp create mode 100644 Source/Core/Core/IOS/USB/Bluetooth/LibUSBBluetoothAdapter.h diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index f7b43c463e..79aab432cd 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -695,6 +695,8 @@ if(TARGET LibUSB::LibUSB) IOS/USB/LibusbDevice.h IOS/USB/Bluetooth/BTReal.cpp IOS/USB/Bluetooth/BTReal.h + IOS/USB/Bluetooth/LibUSBBluetoothAdapter.cpp + IOS/USB/Bluetooth/LibUSBBluetoothAdapter.h ) endif() diff --git a/Source/Core/Core/IOS/USB/Bluetooth/BTReal.cpp b/Source/Core/Core/IOS/USB/Bluetooth/BTReal.cpp index 7940c37fc0..9ba818c854 100644 --- a/Source/Core/Core/IOS/USB/Bluetooth/BTReal.cpp +++ b/Source/Core/Core/IOS/USB/Bluetooth/BTReal.cpp @@ -4,53 +4,49 @@ #include "Core/IOS/USB/Bluetooth/BTReal.h" #include -#include +#include #include -#include -#include #include -#include -#include +#include #include #include -#include #include -#include #include "Common/ChunkFile.h" -#include "Common/EnumUtils.h" -#include "Common/Logging/Log.h" -#include "Common/MsgHandler.h" #include "Common/Network.h" #include "Common/StringUtil.h" #include "Common/Swap.h" + #include "Core/Config/MainSettings.h" #include "Core/Core.h" #include "Core/HW/Memmap.h" #include "Core/IOS/Device.h" #include "Core/IOS/USB/Host.h" #include "Core/System.h" + #include "VideoCommon/OnScreenDisplay.h" +namespace +{ +template +struct HCICommandPayload +{ + hci_cmd_hdr_t header{Opcode, sizeof(CommandType)}; + CommandType command{}; +}; + +template +requires(std::is_trivially_copyable_v) +constexpr auto AsU8Span(const T& obj) +{ + return std::span{reinterpret_cast(std::addressof(obj)), sizeof(obj)}; +} + +} // namespace + namespace IOS::HLE { -constexpr u8 REQUEST_TYPE = static_cast(LIBUSB_ENDPOINT_OUT) | - static_cast(LIBUSB_REQUEST_TYPE_CLASS) | - static_cast(LIBUSB_RECIPIENT_INTERFACE); - -static bool IsBluetoothDevice(const libusb_device_descriptor& descriptor) -{ - constexpr u8 SUBCLASS = 0x01; - constexpr u8 PROTOCOL_BLUETOOTH = 0x01; - // Some devices misreport their class, so we avoid relying solely on descriptor checks and allow - // users to specify their own VID/PID. - return BluetoothRealDevice::IsConfiguredBluetoothDevice(descriptor.idVendor, - descriptor.idProduct) || - (descriptor.bDeviceClass == LIBUSB_CLASS_WIRELESS && - descriptor.bDeviceSubClass == SUBCLASS && - descriptor.bDeviceProtocol == PROTOCOL_BLUETOOTH); -} BluetoothRealDevice::BluetoothRealDevice(EmulationKernel& ios, const std::string& device_name) : BluetoothBaseDevice(ios, device_name) @@ -60,271 +56,232 @@ BluetoothRealDevice::BluetoothRealDevice(EmulationKernel& ios, const std::string BluetoothRealDevice::~BluetoothRealDevice() { - if (m_handle != nullptr) - { - SendHCIResetCommand(); - WaitForHCICommandComplete(HCI_CMD_RESET); - const int ret = libusb_release_interface(m_handle, 0); - if (ret != LIBUSB_SUCCESS) - WARN_LOG_FMT(IOS_WIIMOTE, "libusb_release_interface failed: {}", LibusbUtils::ErrorWrap(ret)); - libusb_close(m_handle); - libusb_unref_device(m_device); - } SaveLinkKeys(); } -bool BluetoothRealDevice::HasConfiguredBluetoothDevice() -{ - const int configured_vid = Config::Get(Config::MAIN_BLUETOOTH_PASSTHROUGH_VID); - const int configured_pid = Config::Get(Config::MAIN_BLUETOOTH_PASSTHROUGH_PID); - return configured_vid != -1 && configured_pid != -1; -} - -bool BluetoothRealDevice::IsConfiguredBluetoothDevice(u16 vid, u16 pid) -{ - const int configured_vid = Config::Get(Config::MAIN_BLUETOOTH_PASSTHROUGH_VID); - const int configured_pid = Config::Get(Config::MAIN_BLUETOOTH_PASSTHROUGH_PID); - return configured_vid == vid && configured_pid == pid; -} - std::optional BluetoothRealDevice::Open(const OpenRequest& request) { - if (!m_context.IsValid()) - return IPCReply(IPC_EACCES); - - m_last_open_error.clear(); - const int ret = m_context.GetDeviceList([this](libusb_device* device) { - libusb_device_descriptor device_descriptor; - libusb_get_device_descriptor(device, &device_descriptor); - auto [make_config_descriptor_ret, config_descriptor] = - LibusbUtils::MakeConfigDescriptor(device); - if (make_config_descriptor_ret != LIBUSB_SUCCESS || !config_descriptor) - { - ERROR_LOG_FMT(IOS_WIIMOTE, "Failed to get config descriptor for device {:04x}:{:04x}: {}", - device_descriptor.idVendor, device_descriptor.idProduct, - LibusbUtils::ErrorWrap(make_config_descriptor_ret)); - return true; - } - - if (HasConfiguredBluetoothDevice() && - !IsConfiguredBluetoothDevice(device_descriptor.idVendor, device_descriptor.idProduct)) - { - return true; - } - if (IsBluetoothDevice(device_descriptor) && OpenDevice(device_descriptor, device)) - { - unsigned char manufacturer[50] = {}, product[50] = {}, serial_number[50] = {}; - const int manufacturer_ret = libusb_get_string_descriptor_ascii( - m_handle, device_descriptor.iManufacturer, manufacturer, sizeof(manufacturer)); - if (manufacturer_ret < LIBUSB_SUCCESS) - { - WARN_LOG_FMT(IOS_WIIMOTE, - "Failed to get string for manufacturer descriptor {:02x} for device " - "{:04x}:{:04x} (rev {:x}): {}", - device_descriptor.iManufacturer, device_descriptor.idVendor, - device_descriptor.idProduct, device_descriptor.bcdDevice, - LibusbUtils::ErrorWrap(manufacturer_ret)); - manufacturer[0] = '?'; - manufacturer[1] = '\0'; - } - const int product_ret = libusb_get_string_descriptor_ascii( - m_handle, device_descriptor.iProduct, product, sizeof(product)); - if (product_ret < LIBUSB_SUCCESS) - { - WARN_LOG_FMT(IOS_WIIMOTE, - "Failed to get string for product descriptor {:02x} for device " - "{:04x}:{:04x} (rev {:x}): {}", - device_descriptor.iProduct, device_descriptor.idVendor, - device_descriptor.idProduct, device_descriptor.bcdDevice, - LibusbUtils::ErrorWrap(product_ret)); - product[0] = '?'; - product[1] = '\0'; - } - const int serial_ret = libusb_get_string_descriptor_ascii( - m_handle, device_descriptor.iSerialNumber, serial_number, sizeof(serial_number)); - if (serial_ret < LIBUSB_SUCCESS) - { - WARN_LOG_FMT(IOS_WIIMOTE, - "Failed to get string for serial number descriptor {:02x} for device " - "{:04x}:{:04x} (rev {:x}): {}", - device_descriptor.iSerialNumber, device_descriptor.idVendor, - device_descriptor.idProduct, device_descriptor.bcdDevice, - LibusbUtils::ErrorWrap(serial_ret)); - serial_number[0] = '?'; - serial_number[1] = '\0'; - } - NOTICE_LOG_FMT(IOS_WIIMOTE, "Using device {:04x}:{:04x} (rev {:x}) for Bluetooth: {} {} {}", - device_descriptor.idVendor, device_descriptor.idProduct, - device_descriptor.bcdDevice, reinterpret_cast(manufacturer), - reinterpret_cast(product), reinterpret_cast(serial_number)); - m_is_wii_bt_module = - device_descriptor.idVendor == 0x57e && device_descriptor.idProduct == 0x305; - return false; - } - return true; - }); - if (ret != LIBUSB_SUCCESS) - { - m_last_open_error = - Common::FmtFormatT("GetDeviceList failed: {0}", LibusbUtils::ErrorWrap(ret)); - } - - if (m_handle == nullptr) - { - if (m_last_open_error.empty()) - { - CriticalAlertFmtT( - "Could not find any usable Bluetooth USB adapter for Bluetooth Passthrough.\n\n" - "The emulated console will now stop."); - } - else - { - CriticalAlertFmtT( - "Could not find any usable Bluetooth USB adapter for Bluetooth Passthrough.\n" - "The following error occurred when Dolphin tried to use an adapter:\n{0}\n\n" - "The emulated console will now stop.", - m_last_open_error); - } - Core::QueueHostJob(&Core::Stop); - return IPCReply(IPC_ENOENT); - } + m_lib_usb_bt_adapter = std::make_unique(); return Device::Open(request); } std::optional BluetoothRealDevice::Close(u32 fd) { - if (m_handle) - { - const int ret = libusb_release_interface(m_handle, 0); - if (ret != LIBUSB_SUCCESS) - WARN_LOG_FMT(IOS_WIIMOTE, "libusb_release_interface failed: {}", LibusbUtils::ErrorWrap(ret)); - libusb_close(m_handle); - libusb_unref_device(m_device); - m_handle = nullptr; - } + m_hci_endpoint.reset(); + m_acl_endpoint.reset(); + + m_fake_replies = {}; + + // FYI: LibUSBBluetoothAdapter destruction will attempt to wait for command completion. + SendHCIResetCommand(); + + m_lib_usb_bt_adapter.reset(); return Device::Close(fd); } std::optional BluetoothRealDevice::IOCtlV(const IOCtlVRequest& request) { - if (!m_is_wii_bt_module && m_need_reset_keys.TestAndClear()) - { - // Do this now before transferring any more data, so that this is fully transparent to games - SendHCIDeleteLinkKeyCommand(); - WaitForHCICommandComplete(HCI_CMD_DELETE_STORED_LINK_KEY); - if (SendHCIStoreLinkKeyCommand()) - WaitForHCICommandComplete(HCI_CMD_WRITE_STORED_LINK_KEY); - } - switch (request.request) { // HCI commands to the Bluetooth adapter case USB::IOCTLV_USBV0_CTRLMSG: { - auto& system = GetSystem(); - auto& memory = system.GetMemory(); + auto& memory = GetSystem().GetMemory(); - std::lock_guard lk(m_transfers_mutex); auto cmd = std::make_unique(GetEmulationKernel(), request); const u16 opcode = Common::swap16(memory.Read_U16(cmd->data_address)); - if (opcode == HCI_CMD_READ_BUFFER_SIZE) + if (!m_lib_usb_bt_adapter->IsWiiBTModule() && (opcode == 0xFC4C || opcode == 0xFC4F)) { - m_fake_read_buffer_size_reply.Set(); + m_fake_replies.emplace( + std::bind_front(&BluetoothRealDevice::FakeVendorCommandReply, this, opcode)); + } + else + { + if (opcode == HCI_CMD_DELETE_STORED_LINK_KEY) + { + INFO_LOG_FMT(IOS_WIIMOTE, "HCI_CMD_DELETE_STORED_LINK_KEY"); + + // Delete link key(s) from our own link key storage when the game tells the adapter to + hci_delete_stored_link_key_cp delete_cmd; + memory.CopyFromEmu(&delete_cmd, cmd->data_address, sizeof(delete_cmd)); + + if (delete_cmd.delete_all != 0) + m_link_keys.clear(); + else + m_link_keys.erase(delete_cmd.bdaddr); + } + else if (opcode == HCI_CMD_SNIFF_MODE) + { + // FYI: This is what causes Wii Remotes to operate at 200hz instead of 100hz. + DEBUG_LOG_FMT(IOS_WIIMOTE, "HCI_CMD_SNIFF_MODE"); + } + + const auto payload = memory.GetSpanForAddress(cmd->data_address).first(cmd->length); + m_lib_usb_bt_adapter->ScheduleControlTransfer(cmd->request_type, cmd->request, cmd->value, + cmd->index, payload, GetTargetTime()); + } + return IPCReply{cmd->length}; + } + // ACL data (incoming or outgoing) + case USB::IOCTLV_USBV0_BLKMSG: + { + auto cmd = std::make_unique(GetEmulationKernel(), request); + if (cmd->endpoint == ACL_DATA_IN) + { + m_acl_endpoint = std::move(cmd); + TryToFillACLEndpoint(); return std::nullopt; } - if (!m_is_wii_bt_module && (opcode == 0xFC4C || opcode == 0xFC4F)) - { - m_fake_vendor_command_reply.Set(); - m_fake_vendor_command_reply_opcode = opcode; - return std::nullopt; - } - if (opcode == HCI_CMD_DELETE_STORED_LINK_KEY) - { - // Delete link key(s) from our own link key storage when the game tells the adapter to - hci_delete_stored_link_key_cp delete_cmd; - memory.CopyFromEmu(&delete_cmd, cmd->data_address, sizeof(delete_cmd)); - if (delete_cmd.delete_all) - m_link_keys.clear(); - else - m_link_keys.erase(delete_cmd.bdaddr); - } - auto buffer = std::make_unique(cmd->length + LIBUSB_CONTROL_SETUP_SIZE); - libusb_fill_control_setup(buffer.get(), cmd->request_type, cmd->request, cmd->value, cmd->index, - cmd->length); - memory.CopyFromEmu(buffer.get() + LIBUSB_CONTROL_SETUP_SIZE, cmd->data_address, cmd->length); - libusb_transfer* transfer = libusb_alloc_transfer(0); - transfer->flags |= LIBUSB_TRANSFER_FREE_TRANSFER; - libusb_fill_control_transfer(transfer, m_handle, buffer.get(), nullptr, this, 0); - transfer->callback = [](libusb_transfer* tr) { - static_cast(tr->user_data)->HandleCtrlTransfer(tr); - }; - PendingTransfer pending_transfer{std::move(cmd), std::move(buffer)}; - m_current_transfers.emplace(transfer, std::move(pending_transfer)); - const int ret = libusb_submit_transfer(transfer); - if (ret != LIBUSB_SUCCESS) - WARN_LOG_FMT(IOS_WIIMOTE, "libusb_submit_transfer failed: {}", LibusbUtils::ErrorWrap(ret)); + + auto& memory = GetSystem().GetMemory(); + const auto payload = memory.GetSpanForAddress(cmd->data_address).first(cmd->length); + m_lib_usb_bt_adapter->ScheduleBulkTransfer(cmd->endpoint, payload, GetTargetTime()); break; } - // ACL data (incoming or outgoing) and incoming HCI events (respectively) - case USB::IOCTLV_USBV0_BLKMSG: + // Incoming HCI events case USB::IOCTLV_USBV0_INTRMSG: { - std::lock_guard lk(m_transfers_mutex); auto cmd = std::make_unique(GetEmulationKernel(), request); - if (request.request == USB::IOCTLV_USBV0_INTRMSG) + if (cmd->endpoint == HCI_EVENT) { - if (m_sync_button_state == SyncButtonState::Pressed) - { - Core::DisplayMessage("Scanning for Wii Remotes", 2000); - FakeSyncButtonPressedEvent(*cmd); - return std::nullopt; - } - if (m_sync_button_state == SyncButtonState::LongPressed) - { - Core::DisplayMessage("Reset saved Wii Remote pairings", 2000); - FakeSyncButtonHeldEvent(*cmd); - return std::nullopt; - } - if (m_fake_read_buffer_size_reply.TestAndClear()) - { - FakeReadBufferSizeReply(*cmd); - return std::nullopt; - } - if (m_fake_vendor_command_reply.TestAndClear()) - { - FakeVendorCommandReply(*cmd); - return std::nullopt; - } + m_hci_endpoint = std::move(cmd); + TryToFillHCIEndpoint(); + return std::nullopt; } - auto buffer = cmd->MakeBuffer(cmd->length); - libusb_transfer* transfer = libusb_alloc_transfer(0); - transfer->buffer = buffer.get(); - transfer->callback = [](libusb_transfer* tr) { - static_cast(tr->user_data)->HandleBulkOrIntrTransfer(tr); - }; - transfer->dev_handle = m_handle; - transfer->endpoint = cmd->endpoint; - transfer->flags |= LIBUSB_TRANSFER_FREE_TRANSFER; - transfer->length = cmd->length; - transfer->timeout = TIMEOUT; - transfer->type = request.request == USB::IOCTLV_USBV0_BLKMSG ? LIBUSB_TRANSFER_TYPE_BULK : - LIBUSB_TRANSFER_TYPE_INTERRUPT; - transfer->user_data = this; - PendingTransfer pending_transfer{std::move(cmd), std::move(buffer)}; - m_current_transfers.emplace(transfer, std::move(pending_transfer)); - const int ret = libusb_submit_transfer(transfer); - if (ret != LIBUSB_SUCCESS) - WARN_LOG_FMT(IOS_WIIMOTE, "libusb_submit_transfer failed: {}", LibusbUtils::ErrorWrap(ret)); + + ERROR_LOG_FMT(IOS_WIIMOTE, "IOCTLV_USBV0_INTRMSG: Unknown endpoint: 0x{:02x}", cmd->endpoint); + } + default: + ERROR_LOG_FMT(IOS_WIIMOTE, "IOCtlV: Unknown request: 0x{:08x}", request.request); break; } - } - // Replies are generated inside of the message handlers (and asynchronously). - return std::nullopt; + + return IPCReply{IPC_SUCCESS}; +} + +void BluetoothRealDevice::Update() +{ + TryToFillHCIEndpoint(); + TryToFillACLEndpoint(); +} + +void BluetoothRealDevice::TryToFillHCIEndpoint() +{ + if (!m_hci_endpoint) + return; + + if (m_sync_button_state == SyncButtonState::Pressed) + { + Core::DisplayMessage("Scanning for Wii Remotes", 2000); + FakeSyncButtonPressedEvent(*m_hci_endpoint); + } + else if (m_sync_button_state == SyncButtonState::LongPressed) + { + Core::DisplayMessage("Reset saved Wii Remote pairings", 2000); + FakeSyncButtonHeldEvent(*m_hci_endpoint); + } + else if (!m_fake_replies.empty()) + { + std::invoke(m_fake_replies.front(), *m_hci_endpoint); + m_fake_replies.pop(); + } + else + { + const auto buffer = ProcessHCIEvent(m_lib_usb_bt_adapter->ReceiveHCIEvent()); + if (buffer.empty()) + return; + + assert(buffer.size() <= m_hci_endpoint->length); + + m_hci_endpoint->FillBuffer(buffer.data(), buffer.size()); + GetEmulationKernel().EnqueueIPCReply(m_hci_endpoint->ios_request, s32(buffer.size())); + } + + m_hci_endpoint.reset(); +} + +auto BluetoothRealDevice::ProcessHCIEvent(BufferType buffer) -> BufferType +{ + if (buffer.empty()) + return buffer; + + const auto event = buffer[0]; + if (event == HCI_EVENT_LINK_KEY_NOTIFICATION) + { + INFO_LOG_FMT(IOS_WIIMOTE, "HCI_EVENT_LINK_KEY_NOTIFICATION"); + + hci_link_key_notification_ep notification; + std::memcpy(¬ification, buffer.data() + sizeof(hci_event_hdr_t), sizeof(notification)); + std::ranges::copy(notification.key, std::begin(m_link_keys[notification.bdaddr])); + } + + if (m_lib_usb_bt_adapter->IsWiiBTModule()) + return buffer; + + // Handle some quirks for non-Wii BT adapters below. + + if (event == HCI_EVENT_COMMAND_COMPL) + { + hci_command_compl_ep ev; + std::memcpy(&ev, buffer.data() + sizeof(hci_event_hdr_t), sizeof(ev)); + + if (ev.opcode == HCI_CMD_READ_BUFFER_SIZE) + { + // Due to how the widcomm stack which Nintendo uses is coded, we must never + // let the stack think the controller is buffering more than 10 data packets + // - it will cause a u8 underflow and royally screw things up. + // Therefore, the reply to this command has to be faked to avoid random, weird issues + // (including Wiimote disconnects and "event mismatch" warning messages). + DEBUG_LOG_FMT(IOS_WIIMOTE, "Adjusting HCI_CMD_READ_BUFFER_SIZE results."); + + hci_read_buffer_size_rp reply; + reply.status = 0x00; + reply.max_acl_size = ACL_PKT_SIZE; + reply.num_acl_pkts = ACL_PKT_NUM; + reply.max_sco_size = SCO_PKT_SIZE; + reply.num_sco_pkts = SCO_PKT_NUM; + + std::memcpy(buffer.data() + sizeof(hci_event_hdr_t) + sizeof(ev), &reply, sizeof(reply)); + } + else if (ev.opcode == HCI_CMD_RESET) + { + SendHCIDeleteLinkKeyCommand(); + SendHCIStoreLinkKeyCommand(); + } + } + + return buffer; +} + +void BluetoothRealDevice::TryToFillACLEndpoint() +{ + if (!m_acl_endpoint) + return; + + // Throttle to minimize input latency. + auto& core_timing = GetSystem().GetCoreTiming(); + core_timing.Throttle(core_timing.GetTicks()); + + const auto buffer = m_lib_usb_bt_adapter->ReceiveACLData(); + if (buffer.empty()) + return; + + assert(buffer.size() <= m_acl_endpoint->length); + + m_acl_endpoint->FillBuffer(buffer.data(), buffer.size()); + GetEmulationKernel().EnqueueIPCReply(m_acl_endpoint->ios_request, s32(buffer.size())); + + m_acl_endpoint.reset(); +} + +TimePoint BluetoothRealDevice::GetTargetTime() const +{ + auto& core_timing = GetSystem().GetCoreTiming(); + return core_timing.GetTargetHostTime(core_timing.GetTicks()); } -static bool s_has_shown_savestate_warning = false; void BluetoothRealDevice::DoState(PointerWrap& p) { bool passthrough_bluetooth = true; @@ -337,37 +294,19 @@ void BluetoothRealDevice::DoState(PointerWrap& p) } Device::DoState(p); - // Prevent the transfer callbacks from messing with m_current_transfers after we have started - // writing a savestate. We cannot use a scoped lock here because DoState is called twice and - // we would lose the lock between the two calls. - if (p.IsMeasureMode() || p.IsVerifyMode()) - m_transfers_mutex.lock(); - std::vector addresses_to_discard; - if (!p.IsReadMode()) - { - // Save addresses of transfer commands to discard on savestate load. - for (const auto& transfer : m_current_transfers) - addresses_to_discard.push_back(transfer.second.command->ios_request.address); - } - p.Do(addresses_to_discard); + DoStateForMessage(GetEmulationKernel(), p, m_hci_endpoint); + DoStateForMessage(GetEmulationKernel(), p, m_acl_endpoint); + if (p.IsReadMode()) { - // On load, discard any pending transfer to make sure the emulated software is not stuck - // waiting for the previous request to complete. This is usually not an issue as long as - // the Bluetooth state is the same (same Wii Remote connections). - auto& system = GetSystem(); - for (const auto& address_to_discard : addresses_to_discard) - GetEmulationKernel().EnqueueIPCReply(Request{system, address_to_discard}, 0); - - // Prevent the callbacks from replying to a request that has already been discarded. - m_current_transfers.clear(); - OSD::AddMessage("If the savestate does not load correctly, disconnect all Wii Remotes " "and reload it.", OSD::Duration::NORMAL); } + static bool s_has_shown_savestate_warning = false; + if (!s_has_shown_savestate_warning && p.IsWriteMode()) { OSD::AddMessage("Savestates may not work with Bluetooth passthrough in all cases.\n" @@ -376,10 +315,6 @@ void BluetoothRealDevice::DoState(PointerWrap& p) OSD::Duration::VERY_LONG); s_has_shown_savestate_warning = true; } - - // We have finished the savestate now, so the transfers mutex can be unlocked. - if (p.IsWriteMode()) - m_transfers_mutex.unlock(); } void BluetoothRealDevice::UpdateSyncButtonState(const bool is_held) @@ -410,55 +345,21 @@ void BluetoothRealDevice::TriggerSyncButtonHeldEvent() m_sync_button_state = SyncButtonState::LongPressed; } -void BluetoothRealDevice::WaitForHCICommandComplete(const u16 opcode) -{ - int actual_length; - SHCIEventCommand packet; - std::vector buffer(1024); - // Only try 100 transfers at most, to avoid being stuck in an infinite loop - for (int tries = 0; tries < 100; ++tries) - { - const int ret = libusb_interrupt_transfer(m_handle, HCI_EVENT, buffer.data(), - static_cast(buffer.size()), &actual_length, 20); - if (ret != LIBUSB_SUCCESS || actual_length < static_cast(sizeof(packet))) - continue; - std::memcpy(&packet, buffer.data(), sizeof(packet)); - if (packet.EventType == HCI_EVENT_COMMAND_COMPL && packet.Opcode == opcode) - break; - } -} - void BluetoothRealDevice::SendHCIResetCommand() { - u8 packet[3] = {}; - const u16 payload[] = {HCI_CMD_RESET}; - memcpy(packet, payload, sizeof(payload)); - const int ret = - libusb_control_transfer(m_handle, REQUEST_TYPE, 0, 0, 0, packet, sizeof(packet), TIMEOUT); - if (ret < LIBUSB_SUCCESS) - WARN_LOG_FMT(IOS_WIIMOTE, "libusb_control_transfer failed: {}", LibusbUtils::ErrorWrap(ret)); - else - INFO_LOG_FMT(IOS_WIIMOTE, "Sent a reset command to adapter"); + INFO_LOG_FMT(IOS_WIIMOTE, "SendHCIResetCommand"); + m_lib_usb_bt_adapter->SendControlTransfer(AsU8Span(hci_cmd_hdr_t{HCI_CMD_RESET, 0})); } void BluetoothRealDevice::SendHCIDeleteLinkKeyCommand() { - struct Payload - { - hci_cmd_hdr_t header; - hci_delete_stored_link_key_cp command; - }; - Payload payload; - payload.header.opcode = HCI_CMD_DELETE_STORED_LINK_KEY; - payload.header.length = sizeof(payload.command); - payload.command.bdaddr = {}; - payload.command.delete_all = true; + INFO_LOG_FMT(IOS_WIIMOTE, "SendHCIDeleteLinkKeyCommand"); - const int ret = - libusb_control_transfer(m_handle, REQUEST_TYPE, 0, 0, 0, reinterpret_cast(&payload), - static_cast(sizeof(payload)), TIMEOUT); - if (ret < LIBUSB_SUCCESS) - WARN_LOG_FMT(IOS_WIIMOTE, "libusb_control_transfer failed: {}", LibusbUtils::ErrorWrap(ret)); + HCICommandPayload payload; + payload.command.bdaddr = {}; + payload.command.delete_all = 0x01; + + m_lib_usb_bt_adapter->SendControlTransfer(AsU8Span(payload)); } bool BluetoothRealDevice::SendHCIStoreLinkKeyCommand() @@ -466,111 +367,78 @@ bool BluetoothRealDevice::SendHCIStoreLinkKeyCommand() if (m_link_keys.empty()) return false; - // The HCI command field is limited to uint8_t, and libusb to uint16_t. - const u8 payload_size = - static_cast(sizeof(hci_write_stored_link_key_cp)) + - (sizeof(bdaddr_t) + sizeof(linkkey_t)) * static_cast(m_link_keys.size()); - std::vector packet(sizeof(hci_cmd_hdr_t) + payload_size); + // Range: 0x01 to 0x0B per Bluetooth spec. + static constexpr std::size_t MAX_LINK_KEYS = 0x0B; + const auto num_link_keys = u8(std::min(m_link_keys.size(), MAX_LINK_KEYS)); - hci_cmd_hdr_t header{}; - header.opcode = HCI_CMD_WRITE_STORED_LINK_KEY; - header.length = payload_size; - std::memcpy(packet.data(), &header, sizeof(header)); + INFO_LOG_FMT(IOS_WIIMOTE, "SendHCIStoreLinkKeyCommand num_link_keys: {}", num_link_keys); - hci_write_stored_link_key_cp command{}; - command.num_keys_write = static_cast(m_link_keys.size()); - std::memcpy(packet.data() + sizeof(hci_cmd_hdr_t), &command, sizeof(command)); - - // This is really ugly, but necessary because of the HCI command structure: - // u8 num_keys; - // u8 bdaddr[6]; - // u8 key[16]; - // where the two last items are repeated num_keys times. - auto iterator = packet.begin() + sizeof(hci_cmd_hdr_t) + sizeof(hci_write_stored_link_key_cp); - for (const auto& entry : m_link_keys) + struct Payload { - std::ranges::copy(entry.first, iterator); - iterator += entry.first.size(); - std::ranges::copy(entry.second, iterator); - iterator += entry.second.size(); - } + hci_cmd_hdr_t header{HCI_CMD_WRITE_STORED_LINK_KEY}; + hci_write_stored_link_key_cp command{}; + struct LinkKey + { + bdaddr_t bdaddr; + linkkey_t linkkey; + }; + std::array link_keys{}; + } payload; + static_assert(sizeof(Payload) == 4 + (6 + 16) * MAX_LINK_KEYS); - const int ret = libusb_control_transfer(m_handle, REQUEST_TYPE, 0, 0, 0, packet.data(), - static_cast(packet.size()), TIMEOUT); - if (ret < LIBUSB_SUCCESS) - WARN_LOG_FMT(IOS_WIIMOTE, "libusb_control_transfer failed: {}", LibusbUtils::ErrorWrap(ret)); + const u8 payload_size = + sizeof(payload.header) + sizeof(payload.command) + (sizeof(Payload::LinkKey) * num_link_keys); + + payload.header.length = payload_size - sizeof(payload.header); + payload.command.num_keys_write = num_link_keys; + + int index = 0; + for (auto& [bdaddr, linkkey] : m_link_keys | std::views::take(num_link_keys)) + payload.link_keys[index++] = {bdaddr, linkkey}; + + m_lib_usb_bt_adapter->SendControlTransfer(AsU8Span(payload).first(payload_size)); return true; } -void BluetoothRealDevice::FakeVendorCommandReply(USB::V0IntrMessage& ctrl) +void BluetoothRealDevice::FakeVendorCommandReply(u16 opcode, USB::V0IntrMessage& ctrl) { - auto& system = GetSystem(); - auto& memory = system.GetMemory(); + DEBUG_LOG_FMT(IOS_WIIMOTE, "FakeVendorCommandReply"); - SHCIEventCommand hci_event; - memory.CopyFromEmu(&hci_event, ctrl.data_address, sizeof(hci_event)); - hci_event.EventType = HCI_EVENT_COMMAND_COMPL; - hci_event.PayloadLength = sizeof(SHCIEventCommand) - 2; - hci_event.PacketIndicator = 0x01; - hci_event.Opcode = m_fake_vendor_command_reply_opcode; - memory.CopyToEmu(ctrl.data_address, &hci_event, sizeof(hci_event)); - GetEmulationKernel().EnqueueIPCReply(ctrl.ios_request, static_cast(sizeof(hci_event))); + struct Payload + { + hci_event_hdr_t header{HCI_EVENT_COMMAND_COMPL}; + hci_command_compl_ep command{}; + } payload; + + payload.header.length = sizeof(payload) - sizeof(payload.header); + + payload.command.num_cmd_pkts = 0x01; + payload.command.opcode = opcode; + + assert(sizeof(payload) <= ctrl.length); + + GetSystem().GetMemory().CopyToEmu(ctrl.data_address, &payload, sizeof(payload)); + GetEmulationKernel().EnqueueIPCReply(ctrl.ios_request, s32(sizeof(payload))); } -// Due to how the widcomm stack which Nintendo uses is coded, we must never -// let the stack think the controller is buffering more than 10 data packets -// - it will cause a u8 underflow and royally screw things up. -// Therefore, the reply to this command has to be faked to avoid random, weird issues -// (including Wiimote disconnects and "event mismatch" warning messages). -void BluetoothRealDevice::FakeReadBufferSizeReply(USB::V0IntrMessage& ctrl) +void BluetoothRealDevice::FakeSyncButtonEvent(USB::V0IntrMessage& ctrl, std::span payload) { - auto& system = GetSystem(); - auto& memory = system.GetMemory(); + const hci_event_hdr_t hci_event{HCI_EVENT_VENDOR, u8(payload.size())}; + + auto& memory = GetSystem().GetMemory(); - SHCIEventCommand hci_event; - memory.CopyFromEmu(&hci_event, ctrl.data_address, sizeof(hci_event)); - hci_event.EventType = HCI_EVENT_COMMAND_COMPL; - hci_event.PayloadLength = sizeof(SHCIEventCommand) - 2 + sizeof(hci_read_buffer_size_rp); - hci_event.PacketIndicator = 0x01; - hci_event.Opcode = HCI_CMD_READ_BUFFER_SIZE; memory.CopyToEmu(ctrl.data_address, &hci_event, sizeof(hci_event)); - - hci_read_buffer_size_rp reply; - reply.status = 0x00; - reply.max_acl_size = ACL_PKT_SIZE; - reply.num_acl_pkts = ACL_PKT_NUM; - reply.max_sco_size = SCO_PKT_SIZE; - reply.num_sco_pkts = SCO_PKT_NUM; - memory.CopyToEmu(ctrl.data_address + sizeof(hci_event), &reply, sizeof(reply)); - GetEmulationKernel().EnqueueIPCReply(ctrl.ios_request, - static_cast(sizeof(hci_event) + sizeof(reply))); + memory.CopyToEmu(ctrl.data_address + sizeof(hci_event), payload.data(), payload.size()); + GetEmulationKernel().EnqueueIPCReply(ctrl.ios_request, s32(sizeof(hci_event) + payload.size())); } -void BluetoothRealDevice::FakeSyncButtonEvent(USB::V0IntrMessage& ctrl, const u8* payload, - const u8 size) -{ - auto& system = GetSystem(); - auto& memory = system.GetMemory(); - - hci_event_hdr_t hci_event; - memory.CopyFromEmu(&hci_event, ctrl.data_address, sizeof(hci_event)); - hci_event.event = HCI_EVENT_VENDOR; - hci_event.length = size; - memory.CopyToEmu(ctrl.data_address, &hci_event, sizeof(hci_event)); - memory.CopyToEmu(ctrl.data_address + sizeof(hci_event), payload, size); - GetEmulationKernel().EnqueueIPCReply(ctrl.ios_request, - static_cast(sizeof(hci_event) + size)); -} - -// When the red sync button is pressed, a HCI event is generated: -// > HCI Event: Vendor (0xff) plen 1 -// 08 +// When the red sync button is pressed, a HCI event is generated. // This causes the emulated software to perform a BT inquiry and connect to found Wiimotes. void BluetoothRealDevice::FakeSyncButtonPressedEvent(USB::V0IntrMessage& ctrl) { NOTICE_LOG_FMT(IOS_WIIMOTE, "Faking 'sync button pressed' (0x08) event packet"); constexpr u8 payload[1] = {0x08}; - FakeSyncButtonEvent(ctrl, payload, sizeof(payload)); + FakeSyncButtonEvent(ctrl, payload); m_sync_button_state = SyncButtonState::Ignored; } @@ -579,7 +447,7 @@ void BluetoothRealDevice::FakeSyncButtonHeldEvent(USB::V0IntrMessage& ctrl) { NOTICE_LOG_FMT(IOS_WIIMOTE, "Faking 'sync button held' (0x09) event packet"); constexpr u8 payload[1] = {0x09}; - FakeSyncButtonEvent(ctrl, payload, sizeof(payload)); + FakeSyncButtonEvent(ctrl, payload); m_sync_button_state = SyncButtonState::Ignored; } @@ -588,13 +456,13 @@ void BluetoothRealDevice::LoadLinkKeys() std::string entries = Config::Get(Config::MAIN_BLUETOOTH_PASSTHROUGH_LINK_KEYS); if (entries.empty()) return; - for (const auto& pair : SplitString(entries, ',')) + for (std::string_view pair : SplitString(entries, ',')) { const auto index = pair.find('='); if (index == std::string::npos) continue; - const std::string address_string = pair.substr(0, index); + const auto address_string = pair.substr(0, index); std::optional address = Common::StringToMacAddress(address_string); if (!address) { @@ -604,18 +472,14 @@ void BluetoothRealDevice::LoadLinkKeys() continue; } - auto& mac = address.value(); + auto& mac = *address; std::ranges::reverse(mac); - const std::string& key_string = pair.substr(index + 1); + const auto key_string = pair.substr(index + 1); linkkey_t key{}; size_t pos = 0; - for (size_t i = 0; i < key_string.length(); i = i + 2) - { - int value; - std::istringstream(key_string.substr(i, 2)) >> std::hex >> value; - key[pos++] = value; - } + for (size_t i = 0; i < key_string.length() && pos != key.size(); i += 2) + TryParse(std::string(key_string.substr(i, 2)), &key[pos++], 16); m_link_keys[mac] = key; } @@ -623,179 +487,25 @@ void BluetoothRealDevice::LoadLinkKeys() void BluetoothRealDevice::SaveLinkKeys() { - std::ostringstream oss; - for (const auto& entry : m_link_keys) + std::string config_string; + for (const auto& [bdaddr, linkkey] : m_link_keys) { bdaddr_t address; // Reverse the address so that it is stored in the correct order in the config file - std::ranges::reverse_copy(entry.first, address.begin()); - oss << Common::MacAddressToString(address); - oss << '='; - oss << std::hex; - for (u8 data : entry.second) - { - // We cast to u16 here in order to have it displayed as two nibbles. - oss << std::setfill('0') << std::setw(2) << static_cast(data); - } - oss << std::dec << ','; + std::ranges::reverse_copy(bdaddr, address.begin()); + + config_string += Common::MacAddressToString(address); + config_string += '='; + for (u8 c : linkkey) + config_string += fmt::format("{:02x}", c); + + config_string += ','; } - std::string config_string = oss.str(); + if (!config_string.empty()) config_string.pop_back(); + Config::SetBase(Config::MAIN_BLUETOOTH_PASSTHROUGH_LINK_KEYS, config_string); } -bool BluetoothRealDevice::OpenDevice(const libusb_device_descriptor& device_descriptor, - libusb_device* device) -{ - m_device = libusb_ref_device(device); - const int ret = libusb_open(m_device, &m_handle); - if (ret != LIBUSB_SUCCESS) - { - m_last_open_error = Common::FmtFormatT("Failed to open Bluetooth device {:04x}:{:04x}: {}", - device_descriptor.idVendor, device_descriptor.idProduct, - LibusbUtils::ErrorWrap(ret)); - return false; - } - -// Detaching always fails as a regular user on FreeBSD -// https://lists.freebsd.org/pipermail/freebsd-usb/2016-March/014161.html -#ifndef __FreeBSD__ - int result = libusb_set_auto_detach_kernel_driver(m_handle, 1); - if (result != LIBUSB_SUCCESS) - { - result = libusb_detach_kernel_driver(m_handle, INTERFACE); - if (result != LIBUSB_SUCCESS && result != LIBUSB_ERROR_NOT_FOUND && - result != LIBUSB_ERROR_NOT_SUPPORTED) - { - m_last_open_error = Common::FmtFormatT( - "Failed to detach kernel driver for BT passthrough: {0}", LibusbUtils::ErrorWrap(result)); - return false; - } - } -#endif - if (const int result2 = libusb_claim_interface(m_handle, INTERFACE); result2 != LIBUSB_SUCCESS) - { - m_last_open_error = Common::FmtFormatT("Failed to claim interface for BT passthrough: {0}", - LibusbUtils::ErrorWrap(result2)); - return false; - } - - return true; -} - -std::vector BluetoothRealDevice::ListDevices() -{ - std::vector device_list; - LibusbUtils::Context context; - - if (!context.IsValid()) - return {}; - - int result = context.GetDeviceList([&device_list](libusb_device* device) { - libusb_device_descriptor desc; - - auto [config_ret, config] = LibusbUtils::MakeConfigDescriptor(device, 0); - if (config_ret != LIBUSB_SUCCESS) - return true; - - if (libusb_get_device_descriptor(device, &desc) != LIBUSB_SUCCESS) - return true; - - if (IsBluetoothDevice(desc)) - { - const std::string device_name = - USBHost::GetDeviceNameFromVIDPID(desc.idVendor, desc.idProduct); - device_list.push_back({desc.idVendor, desc.idProduct, device_name}); - } - return true; - }); - - if (result < 0) - { - ERROR_LOG_FMT(IOS_USB, "Failed to get device list: {}", LibusbUtils::ErrorWrap(result)); - return device_list; - } - - return device_list; -} - -// The callbacks are called from libusb code on a separate thread. -void BluetoothRealDevice::HandleCtrlTransfer(libusb_transfer* tr) -{ - std::lock_guard lk(m_transfers_mutex); - if (!m_current_transfers.contains(tr)) - return; - - if (tr->status != LIBUSB_TRANSFER_COMPLETED && tr->status != LIBUSB_TRANSFER_NO_DEVICE) - { - ERROR_LOG_FMT(IOS_WIIMOTE, "libusb command transfer failed, status: {:#04x}", - Common::ToUnderlying(tr->status)); - if (!m_showed_failed_transfer.IsSet()) - { - Core::DisplayMessage("Failed to send a command to the Bluetooth adapter.", 10000); - Core::DisplayMessage("It may not be compatible with passthrough mode.", 10000); - m_showed_failed_transfer.Set(); - } - } - else - { - m_showed_failed_transfer.Clear(); - } - const auto& command = m_current_transfers.at(tr).command; - command->FillBuffer(libusb_control_transfer_get_data(tr), tr->actual_length); - GetEmulationKernel().EnqueueIPCReply(command->ios_request, tr->actual_length, 0, - CoreTiming::FromThread::ANY); - m_current_transfers.erase(tr); -} - -void BluetoothRealDevice::HandleBulkOrIntrTransfer(libusb_transfer* tr) -{ - std::lock_guard lk(m_transfers_mutex); - if (!m_current_transfers.contains(tr)) - return; - - if (tr->status != LIBUSB_TRANSFER_COMPLETED && tr->status != LIBUSB_TRANSFER_TIMED_OUT && - tr->status != LIBUSB_TRANSFER_NO_DEVICE) - { - ERROR_LOG_FMT(IOS_WIIMOTE, "libusb transfer failed, status: {:#04x}", - Common::ToUnderlying(tr->status)); - if (!m_showed_failed_transfer.IsSet()) - { - Core::DisplayMessage("Failed to transfer to or from to the Bluetooth adapter.", 10000); - Core::DisplayMessage("It may not be compatible with passthrough mode.", 10000); - m_showed_failed_transfer.Set(); - } - } - else - { - m_showed_failed_transfer.Clear(); - } - - if (tr->status == LIBUSB_TRANSFER_COMPLETED && tr->endpoint == HCI_EVENT) - { - const u8 event = tr->buffer[0]; - if (event == HCI_EVENT_LINK_KEY_NOTIFICATION) - { - hci_link_key_notification_ep notification; - std::memcpy(¬ification, tr->buffer + sizeof(hci_event_hdr_t), sizeof(notification)); - linkkey_t key; - std::ranges::copy(notification.key, std::begin(key)); - m_link_keys[notification.bdaddr] = key; - } - else if (event == HCI_EVENT_COMMAND_COMPL) - { - hci_command_compl_ep complete_event; - std::memcpy(&complete_event, tr->buffer + sizeof(hci_event_hdr_t), sizeof(complete_event)); - if (complete_event.opcode == HCI_CMD_RESET) - m_need_reset_keys.Set(); - } - } - - const auto& command = m_current_transfers.at(tr).command; - command->FillBuffer(tr->buffer, tr->actual_length); - GetEmulationKernel().EnqueueIPCReply(command->ios_request, tr->actual_length, 0, - CoreTiming::FromThread::ANY); - m_current_transfers.erase(tr); -} } // namespace IOS::HLE diff --git a/Source/Core/Core/IOS/USB/Bluetooth/BTReal.h b/Source/Core/Core/IOS/USB/Bluetooth/BTReal.h index b40ace416e..4ffc8fd969 100644 --- a/Source/Core/Core/IOS/USB/Bluetooth/BTReal.h +++ b/Source/Core/Core/IOS/USB/Bluetooth/BTReal.h @@ -6,25 +6,22 @@ #if defined(__LIBUSB__) #include #include +#include #include #include -#include +#include #include #include "Common/CommonTypes.h" -#include "Common/Flag.h" #include "Common/Timer.h" + #include "Core/IOS/IOS.h" #include "Core/IOS/USB/Bluetooth/BTBase.h" +#include "Core/IOS/USB/Bluetooth/LibUSBBluetoothAdapter.h" #include "Core/IOS/USB/Bluetooth/hci.h" #include "Core/IOS/USB/USBV0.h" -#include "Core/LibusbUtils.h" class PointerWrap; -struct libusb_device; -struct libusb_device_handle; -struct libusb_device_descriptor; -struct libusb_transfer; namespace IOS::HLE { @@ -49,87 +46,55 @@ public: std::optional Open(const OpenRequest& request) override; std::optional Close(u32 fd) override; std::optional IOCtlV(const IOCtlVRequest& request) override; + void Update() override; void DoState(PointerWrap& p) override; void UpdateSyncButtonState(bool is_held) override; void TriggerSyncButtonPressedEvent() override; void TriggerSyncButtonHeldEvent() override; - void HandleCtrlTransfer(libusb_transfer* finished_transfer); - void HandleBulkOrIntrTransfer(libusb_transfer* finished_transfer); - - static bool IsConfiguredBluetoothDevice(u16 vid, u16 pid); - static bool HasConfiguredBluetoothDevice(); - - struct BluetoothDeviceInfo - { - u16 vid; - u16 pid; - std::string name; - }; - - static std::vector ListDevices(); - private: - static constexpr u8 INTERFACE = 0x00; - // Arbitrarily chosen value that allows emulated software to send commands often enough - // so that the sync button event is triggered at least every 200ms. - // Ideally this should be equal to 0, so we don't trigger unnecessary libusb transfers. - static constexpr u32 TIMEOUT = 200; + using BufferType = LibUSBBluetoothAdapter::BufferType; + + std::unique_ptr m_lib_usb_bt_adapter; + static constexpr u32 SYNC_BUTTON_HOLD_MS_TO_RESET = 10000; std::atomic m_sync_button_state{SyncButtonState::Unpressed}; Common::Timer m_sync_button_held_timer; - std::string m_last_open_error; - - LibusbUtils::Context m_context; - libusb_device* m_device = nullptr; - libusb_device_handle* m_handle = nullptr; - - std::mutex m_transfers_mutex; - struct PendingTransfer - { - PendingTransfer(std::unique_ptr command_, std::unique_ptr buffer_) - : command(std::move(command_)), buffer(std::move(buffer_)) - { - } - std::unique_ptr command; - std::unique_ptr buffer; - }; - std::map m_current_transfers; - - // Set when we received a command to which we need to fake a reply - Common::Flag m_fake_read_buffer_size_reply; - Common::Flag m_fake_vendor_command_reply; - u16 m_fake_vendor_command_reply_opcode; + // Enqueued when we observe a command to which we need to fake a reply. + std::queue> m_fake_replies; // This stores the address of paired devices and associated link keys. // It is needed because some adapters forget all stored link keys when they are reset, // which breaks pairings because the Wii relies on the Bluetooth module to remember them. std::map m_link_keys; - Common::Flag m_need_reset_keys; - // This flag is set when a libusb transfer failed (for reasons other than timing out) - // and we showed an OSD message about it. - Common::Flag m_showed_failed_transfer; + // Endpoints provided by the emulated software. + std::unique_ptr m_hci_endpoint; + std::unique_ptr m_acl_endpoint; - bool m_is_wii_bt_module = false; + // Used for proper Bluetooth packet timing, especially for Wii remote speaker data. + TimePoint GetTargetTime() const; + + void TryToFillHCIEndpoint(); + void TryToFillACLEndpoint(); + + [[nodiscard]] BufferType ProcessHCIEvent(BufferType buffer); - void WaitForHCICommandComplete(u16 opcode); void SendHCIResetCommand(); void SendHCIDeleteLinkKeyCommand(); bool SendHCIStoreLinkKeyCommand(); - void FakeVendorCommandReply(USB::V0IntrMessage& ctrl); - void FakeReadBufferSizeReply(USB::V0IntrMessage& ctrl); - void FakeSyncButtonEvent(USB::V0IntrMessage& ctrl, const u8* payload, u8 size); + + void FakeVendorCommandReply(u16 opcode, USB::V0IntrMessage& ctrl); + + void FakeSyncButtonEvent(USB::V0IntrMessage& ctrl, std::span payload); void FakeSyncButtonPressedEvent(USB::V0IntrMessage& ctrl); void FakeSyncButtonHeldEvent(USB::V0IntrMessage& ctrl); void LoadLinkKeys(); void SaveLinkKeys(); - - bool OpenDevice(const libusb_device_descriptor& device_descriptor, libusb_device* device); }; } // namespace IOS::HLE diff --git a/Source/Core/Core/IOS/USB/Bluetooth/LibUSBBluetoothAdapter.cpp b/Source/Core/Core/IOS/USB/Bluetooth/LibUSBBluetoothAdapter.cpp new file mode 100644 index 0000000000..87ce4d8235 --- /dev/null +++ b/Source/Core/Core/IOS/USB/Bluetooth/LibUSBBluetoothAdapter.cpp @@ -0,0 +1,596 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/IOS/USB/Bluetooth/LibUSBBluetoothAdapter.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "Common/MsgHandler.h" + +#include "Core/Config/MainSettings.h" +#include "Core/Core.h" +#include "Core/IOS/USB/Bluetooth/hci.h" +#include "Core/IOS/USB/Host.h" + +namespace +{ +constexpr std::size_t BUFFER_SIZE = 1024; + +constexpr auto HCI_COMMAND_TIMEOUT = std::chrono::milliseconds{1000}; + +constexpr u8 REQUEST_TYPE = + u8(u8(LIBUSB_ENDPOINT_OUT) | u8(LIBUSB_REQUEST_TYPE_CLASS)) | u8(LIBUSB_RECIPIENT_INTERFACE); + +constexpr u8 HCI_EVENT = 0x81; +constexpr u8 ACL_DATA_IN = 0x82; + +template +constexpr libusb_transfer_cb_fn LibUSBMemFunCallback() +{ + return [](libusb_transfer* tr) { + std::invoke(MemFun, static_cast*>(tr->user_data), tr); + }; +} + +bool IsBluetoothDevice(const libusb_device_descriptor& descriptor) +{ + constexpr u8 SUBCLASS = 0x01; + constexpr u8 PROTOCOL_BLUETOOTH = 0x01; + + const bool is_bluetooth_protocol = descriptor.bDeviceClass == LIBUSB_CLASS_WIRELESS && + descriptor.bDeviceSubClass == SUBCLASS && + descriptor.bDeviceProtocol == PROTOCOL_BLUETOOTH; + + // Some devices misreport their class, so we avoid relying solely on descriptor checks and allow + // users to specify their own VID/PID. + return is_bluetooth_protocol || LibUSBBluetoothAdapter::IsConfiguredBluetoothDevice( + descriptor.idVendor, descriptor.idProduct); +} + +} // namespace + +bool LibUSBBluetoothAdapter::IsWiiBTModule() const +{ + return m_is_wii_bt_module; +} + +bool LibUSBBluetoothAdapter::HasConfiguredBluetoothDevice() +{ + const int configured_vid = Config::Get(Config::MAIN_BLUETOOTH_PASSTHROUGH_VID); + const int configured_pid = Config::Get(Config::MAIN_BLUETOOTH_PASSTHROUGH_PID); + return configured_vid != -1 && configured_pid != -1; +} + +bool LibUSBBluetoothAdapter::IsConfiguredBluetoothDevice(u16 vid, u16 pid) +{ + const int configured_vid = Config::Get(Config::MAIN_BLUETOOTH_PASSTHROUGH_VID); + const int configured_pid = Config::Get(Config::MAIN_BLUETOOTH_PASSTHROUGH_PID); + return configured_vid == vid && configured_pid == pid; +} + +LibUSBBluetoothAdapter::LibUSBBluetoothAdapter() +{ + if (!m_context.IsValid()) + return; + + m_last_open_error.clear(); + + const bool has_configured_bt = HasConfiguredBluetoothDevice(); + + const int ret = m_context.GetDeviceList([&](libusb_device* device) { + libusb_device_descriptor device_descriptor; + libusb_get_device_descriptor(device, &device_descriptor); + auto [make_config_descriptor_ret, config_descriptor] = + LibusbUtils::MakeConfigDescriptor(device); + if (make_config_descriptor_ret != LIBUSB_SUCCESS || !config_descriptor) + { + ERROR_LOG_FMT(IOS_WIIMOTE, "Failed to get config descriptor for device {:04x}:{:04x}: {}", + device_descriptor.idVendor, device_descriptor.idProduct, + LibusbUtils::ErrorWrap(make_config_descriptor_ret)); + return true; + } + + if (has_configured_bt && + !IsConfiguredBluetoothDevice(device_descriptor.idVendor, device_descriptor.idProduct)) + { + return true; + } + + if (IsBluetoothDevice(device_descriptor) && OpenDevice(device_descriptor, device)) + { + unsigned char manufacturer[50] = {}, product[50] = {}, serial_number[50] = {}; + const int manufacturer_ret = libusb_get_string_descriptor_ascii( + m_handle, device_descriptor.iManufacturer, manufacturer, sizeof(manufacturer)); + if (manufacturer_ret < LIBUSB_SUCCESS) + { + WARN_LOG_FMT(IOS_WIIMOTE, + "Failed to get string for manufacturer descriptor {:02x} for device " + "{:04x}:{:04x} (rev {:x}): {}", + device_descriptor.iManufacturer, device_descriptor.idVendor, + device_descriptor.idProduct, device_descriptor.bcdDevice, + LibusbUtils::ErrorWrap(manufacturer_ret)); + manufacturer[0] = '?'; + manufacturer[1] = '\0'; + } + const int product_ret = libusb_get_string_descriptor_ascii( + m_handle, device_descriptor.iProduct, product, sizeof(product)); + if (product_ret < LIBUSB_SUCCESS) + { + WARN_LOG_FMT(IOS_WIIMOTE, + "Failed to get string for product descriptor {:02x} for device " + "{:04x}:{:04x} (rev {:x}): {}", + device_descriptor.iProduct, device_descriptor.idVendor, + device_descriptor.idProduct, device_descriptor.bcdDevice, + LibusbUtils::ErrorWrap(product_ret)); + product[0] = '?'; + product[1] = '\0'; + } + const int serial_ret = libusb_get_string_descriptor_ascii( + m_handle, device_descriptor.iSerialNumber, serial_number, sizeof(serial_number)); + if (serial_ret < LIBUSB_SUCCESS) + { + WARN_LOG_FMT(IOS_WIIMOTE, + "Failed to get string for serial number descriptor {:02x} for device " + "{:04x}:{:04x} (rev {:x}): {}", + device_descriptor.iSerialNumber, device_descriptor.idVendor, + device_descriptor.idProduct, device_descriptor.bcdDevice, + LibusbUtils::ErrorWrap(serial_ret)); + serial_number[0] = '?'; + serial_number[1] = '\0'; + } + NOTICE_LOG_FMT(IOS_WIIMOTE, "Using device {:04x}:{:04x} (rev {:x}) for Bluetooth: {} {} {}", + device_descriptor.idVendor, device_descriptor.idProduct, + device_descriptor.bcdDevice, reinterpret_cast(manufacturer), + reinterpret_cast(product), reinterpret_cast(serial_number)); + m_is_wii_bt_module = + device_descriptor.idVendor == 0x57e && device_descriptor.idProduct == 0x305; + return false; + } + return true; + }); + if (ret != LIBUSB_SUCCESS) + { + m_last_open_error = + Common::FmtFormatT("GetDeviceList failed: {0}", LibusbUtils::ErrorWrap(ret)); + } + + if (m_handle == nullptr) + { + if (m_last_open_error.empty()) + { + CriticalAlertFmtT( + "Could not find any usable Bluetooth USB adapter for Bluetooth Passthrough.\n\n" + "The emulated console will now stop."); + } + else + { + CriticalAlertFmtT( + "Could not find any usable Bluetooth USB adapter for Bluetooth Passthrough.\n" + "The following error occurred when Dolphin tried to use an adapter:\n{0}\n\n" + "The emulated console will now stop.", + m_last_open_error); + } + Core::QueueHostJob(&Core::Stop); + return; + } + + StartInputTransfers(); + + m_output_worker.Reset("Bluetooth Output", + std::bind_front(&LibUSBBluetoothAdapter::SubmitTimedTransfer, this)); +} + +LibUSBBluetoothAdapter::~LibUSBBluetoothAdapter() +{ + if (m_handle == nullptr) + return; + + // Wait for completion (or time out) of all HCI commands. + while (!m_pending_hci_transfers.empty() && !m_unacknowledged_commands.empty()) + { + (void)ReceiveHCIEvent(); + Common::YieldCPU(); + } + + m_output_worker.Shutdown(); + + // Stop the repeating input transfers. + if (std::lock_guard lg{m_transfers_mutex}; true) + m_run_input_transfers = false; + + // Cancel all LibUSB transfers. + std::ranges::for_each(m_transfer_buffers | std::ranges::views::keys, libusb_cancel_transfer); + + // Wait for transfer callbacks and clean up all the buffers. + while (!m_transfer_buffers.empty()) + { + m_transfers_to_free.WaitForData(); + CleanCompletedTransfers(); + } + + const int ret = libusb_release_interface(m_handle, 0); + if (ret != LIBUSB_SUCCESS) + WARN_LOG_FMT(IOS_WIIMOTE, "libusb_release_interface failed: {}", LibusbUtils::ErrorWrap(ret)); + + libusb_close(std::exchange(m_handle, nullptr)); +} + +void LibUSBBluetoothAdapter::Update() +{ + // Remove timed out commands. + const auto expired_time = Clock::now() - HCI_COMMAND_TIMEOUT; + while (!m_unacknowledged_commands.empty() && + m_unacknowledged_commands.front().submit_time < expired_time) + { + WARN_LOG_FMT(IOS_WIIMOTE, "HCI command 0x{:04x} timed out.", + m_unacknowledged_commands.front().opcode); + m_unacknowledged_commands.pop_front(); + } + + // Allow sending commands if none are pending acknowledgement. + if (m_unacknowledged_commands.empty()) + m_num_hci_command_packets = 1; + + // Push queued commands when the controller is ready for them. + if (!m_pending_hci_transfers.empty() && IsControllerReadyForCommand()) + { + DEBUG_LOG_FMT(IOS_WIIMOTE, "Submitting queued HCI command."); + + PushHCICommand(m_pending_hci_transfers.front()); + m_pending_hci_transfers.pop(); + } + + CleanCompletedTransfers(); +} + +auto LibUSBBluetoothAdapter::ReceiveHCIEvent() -> BufferType +{ + if (m_hci_event_queue.Empty()) + { + Update(); + return {}; + } + + auto buffer = std::move(m_hci_event_queue.Front()); + m_hci_event_queue.Pop(); + + // We only care about "COMMAND_COMPL" and "COMMAND_STATUS" events here. + // The controller will reply with one of those for every command. + // We track which commands are still "in flight" to properly obey Num_HCI_Command_Packets. + const auto event = buffer[0]; + if (event == HCI_EVENT_COMMAND_COMPL) + { + AcknowledgeCommand(buffer); + } + else if (event == HCI_EVENT_COMMAND_STATUS) + { + AcknowledgeCommand(buffer); + } + + Update(); + return buffer; +} + +auto LibUSBBluetoothAdapter::ReceiveACLData() -> BufferType +{ + BufferType buffer; + m_acl_data_queue.Pop(buffer); + return buffer; +} + +bool LibUSBBluetoothAdapter::IsControllerReadyForCommand() const +{ + return m_num_hci_command_packets > m_unacknowledged_commands.size(); +} + +template +void LibUSBBluetoothAdapter::AcknowledgeCommand(std::span buffer) +{ + if (buffer.size() < sizeof(hci_event_hdr_t) + sizeof(EventType)) [[unlikely]] + { + WARN_LOG_FMT(IOS_WIIMOTE, "Undersized HCI event"); + return; + } + + EventType ev; + std::memcpy(&ev, buffer.data() + sizeof(hci_event_hdr_t), sizeof(ev)); + + const auto it = + std::ranges::find(m_unacknowledged_commands, ev.opcode, &OutstandingCommand::opcode); + if (it != m_unacknowledged_commands.end()) + { + DEBUG_LOG_FMT(IOS_WIIMOTE, "HCI command acknowledged: 0x{:04x}", ev.opcode); + m_unacknowledged_commands.erase(it); + } + else if (ev.opcode != 0x0000) + { + WARN_LOG_FMT(IOS_WIIMOTE, "Unexpected opcode acknowledgement: 0x{:04x}", ev.opcode); + } + + m_num_hci_command_packets = ev.num_cmd_pkts; +} + +libusb_transfer* LibUSBBluetoothAdapter::AllocateTransfer(std::size_t buffer_size) +{ + auto& item = + *m_transfer_buffers.emplace(libusb_alloc_transfer(0), Common::UniqueBuffer{buffer_size}) + .first; + + auto* const transfer = item.first; + transfer->buffer = item.second.get(); + + return transfer; +} + +void LibUSBBluetoothAdapter::FreeTransfer(libusb_transfer* transfer) +{ + libusb_free_transfer(transfer); + m_transfer_buffers.erase(transfer); +} + +void LibUSBBluetoothAdapter::CleanCompletedTransfers() +{ + libusb_transfer* transfer{}; + while (m_transfers_to_free.Pop(transfer)) + FreeTransfer(transfer); +} + +void LibUSBBluetoothAdapter::PushHCICommand(TimedTransfer transfer) +{ + hci_cmd_hdr_t cmd; + std::memcpy(&cmd, libusb_control_transfer_get_data(transfer.transfer), sizeof(cmd)); + + // Push the time forward so our timeouts work properly if command was queued and delayed. + const auto submit_time = std::max(transfer.target_time, Clock::now()); + + m_unacknowledged_commands.emplace_back(OutstandingCommand{cmd.opcode, submit_time}); + + // This function is only invoked when this value is at least 1. + assert(m_num_hci_command_packets >= 1); + --m_num_hci_command_packets; + + m_output_worker.EmplaceItem(transfer); +} + +void LibUSBBluetoothAdapter::SubmitTimedTransfer(TimedTransfer transfer) +{ + if (Config::Get(Config::MAIN_PRECISION_FRAME_TIMING)) + m_precision_timer.SleepUntil(transfer.target_time); + else + std::this_thread::sleep_until(transfer.target_time); + + SubmitTransfer(transfer.transfer); +} + +void LibUSBBluetoothAdapter::SubmitTransfer(libusb_transfer* transfer) +{ + const int ret = libusb_submit_transfer(transfer); + + if (ret == LIBUSB_SUCCESS) + return; + + ERROR_LOG_FMT(IOS_WIIMOTE, "libusb_submit_transfer: {}", LibusbUtils::ErrorWrap(ret)); + + // Failed transers will not invoke callbacks so we must mark the buffer to be free'd here. + std::lock_guard lk{m_transfers_mutex}; + m_transfers_to_free.Emplace(transfer); +} + +void LibUSBBluetoothAdapter::ScheduleBulkTransfer(u8 endpoint, std::span data, + TimePoint target_time) +{ + constexpr auto callback = LibUSBMemFunCallback<&LibUSBBluetoothAdapter::HandleOutputTransfer>(); + + auto* const transfer = AllocateTransfer(data.size()); + libusb_fill_bulk_transfer(transfer, m_handle, endpoint, transfer->buffer, int(data.size()), + callback, this, 0); + + std::ranges::copy(data, transfer->buffer); + + m_output_worker.EmplaceItem(transfer, target_time); +} + +void LibUSBBluetoothAdapter::ScheduleControlTransfer(u8 type, u8 request, u8 value, u8 index, + std::span data, + TimePoint target_time) +{ + constexpr auto callback = LibUSBMemFunCallback<&LibUSBBluetoothAdapter::HandleOutputTransfer>(); + + auto* const transfer = AllocateTransfer(data.size() + LIBUSB_CONTROL_SETUP_SIZE); + + libusb_fill_control_setup(transfer->buffer, type, request, value, index, u16(data.size())); + libusb_fill_control_transfer(transfer, m_handle, transfer->buffer, callback, this, 0); + + std::ranges::copy(data, transfer->buffer + LIBUSB_CONTROL_SETUP_SIZE); + + const TimedTransfer timed_transfer{transfer, target_time}; + + if (IsControllerReadyForCommand()) + { + PushHCICommand(timed_transfer); + } + else + { + DEBUG_LOG_FMT(IOS_WIIMOTE, "Queueing HCI command while controller is busy."); + m_pending_hci_transfers.emplace(timed_transfer); + } +} + +void LibUSBBluetoothAdapter::SendControlTransfer(std::span data) +{ + ScheduleControlTransfer(REQUEST_TYPE, 0, 0, 0, data, Clock::now()); +} + +void LibUSBBluetoothAdapter::StartInputTransfers() +{ + constexpr auto callback = LibUSBMemFunCallback<&LibUSBBluetoothAdapter::HandleInputTransfer>(); + + // Incoming HCI events. + { + auto* const transfer = AllocateTransfer(BUFFER_SIZE); + libusb_fill_interrupt_transfer(transfer, m_handle, HCI_EVENT, transfer->buffer, BUFFER_SIZE, + callback, this, 0); + SubmitTransfer(transfer); + } + + // Incoming ACL data. + { + auto* const transfer = AllocateTransfer(BUFFER_SIZE); + libusb_fill_bulk_transfer(transfer, m_handle, ACL_DATA_IN, transfer->buffer, BUFFER_SIZE, + callback, this, 0); + SubmitTransfer(transfer); + } +} + +bool LibUSBBluetoothAdapter::OpenDevice(const libusb_device_descriptor& device_descriptor, + libusb_device* device) +{ + const int ret = libusb_open(device, &m_handle); + if (ret != LIBUSB_SUCCESS) + { + m_last_open_error = Common::FmtFormatT("Failed to open Bluetooth device {:04x}:{:04x}: {}", + device_descriptor.idVendor, device_descriptor.idProduct, + LibusbUtils::ErrorWrap(ret)); + return false; + } + +// Detaching always fails as a regular user on FreeBSD +// https://lists.freebsd.org/pipermail/freebsd-usb/2016-March/014161.html +#ifndef __FreeBSD__ + int result = libusb_set_auto_detach_kernel_driver(m_handle, 1); + if (result != LIBUSB_SUCCESS) + { + result = libusb_detach_kernel_driver(m_handle, INTERFACE); + if (result != LIBUSB_SUCCESS && result != LIBUSB_ERROR_NOT_FOUND && + result != LIBUSB_ERROR_NOT_SUPPORTED) + { + m_last_open_error = Common::FmtFormatT( + "Failed to detach kernel driver for BT passthrough: {0}", LibusbUtils::ErrorWrap(result)); + return false; + } + } +#endif + if (const int result2 = libusb_claim_interface(m_handle, INTERFACE); result2 != LIBUSB_SUCCESS) + { + m_last_open_error = Common::FmtFormatT("Failed to claim interface for BT passthrough: {0}", + LibusbUtils::ErrorWrap(result2)); + return false; + } + + return true; +} + +std::vector LibUSBBluetoothAdapter::ListDevices() +{ + std::vector device_list; + LibusbUtils::Context context; + + if (!context.IsValid()) + return {}; + + int result = context.GetDeviceList([&device_list](libusb_device* device) { + auto [config_ret, config] = LibusbUtils::MakeConfigDescriptor(device, 0); + if (config_ret != LIBUSB_SUCCESS) + return true; + + libusb_device_descriptor desc; + if (libusb_get_device_descriptor(device, &desc) != LIBUSB_SUCCESS) + return true; + + if (IsBluetoothDevice(desc)) + { + const std::string device_name = + IOS::HLE::USBHost::GetDeviceNameFromVIDPID(desc.idVendor, desc.idProduct); + device_list.push_back({desc.idVendor, desc.idProduct, device_name}); + } + return true; + }); + + if (result < 0) + { + ERROR_LOG_FMT(IOS_USB, "Failed to get device list: {}", LibusbUtils::ErrorWrap(result)); + return device_list; + } + + return device_list; +} + +void LibUSBBluetoothAdapter::HandleOutputTransfer(libusb_transfer* tr) +{ + HandleTransferError(tr); + + std::lock_guard lg{m_transfers_mutex}; + m_transfers_to_free.Emplace(tr); +} + +void LibUSBBluetoothAdapter::HandleInputTransfer(libusb_transfer* tr) +{ + if (tr->status == LIBUSB_TRANSFER_COMPLETED) + { + const bool is_hci_event = tr->endpoint == HCI_EVENT; + auto& queue = is_hci_event ? m_hci_event_queue : m_acl_data_queue; + + if (queue.Size() < MAX_INPUT_QUEUE_SIZE) + { + if (tr->actual_length != 0) + { + BufferType buffer(tr->actual_length); + std::copy_n(tr->buffer, tr->actual_length, buffer.data()); + queue.Emplace(std::move(buffer)); + } + + m_showed_long_queue_drop = false; + } + else if (!std::exchange(m_showed_long_queue_drop, true)) + { + // This will happen when pausing the emulator. + WARN_LOG_FMT(IOS_WIIMOTE, "{} queue is too long. Packets will be dropped.", + (is_hci_event ? "HCI" : "ACL")); + } + } + + HandleTransferError(tr); + + std::lock_guard lg{m_transfers_mutex}; + + if (m_run_input_transfers) + { + // Continuously repeat this input transfer so long as we're still running. + const auto ret = libusb_submit_transfer(tr); + if (ret == LIBUSB_SUCCESS) + return; + + ERROR_LOG_FMT(IOS_WIIMOTE, "HandleInputTransfer libusb_submit_transfer: {}", + LibusbUtils::ErrorWrap(ret)); + } + + m_transfers_to_free.Emplace(tr); +} + +void LibUSBBluetoothAdapter::HandleTransferError(libusb_transfer* transfer) +{ + if (transfer->status != LIBUSB_TRANSFER_COMPLETED && + transfer->status != LIBUSB_TRANSFER_CANCELLED) + { + ERROR_LOG_FMT(IOS_WIIMOTE, "libusb transfer failed: {}", + LibusbUtils::ErrorWrap(transfer->status)); + if (!std::exchange(m_showed_failed_transfer, true)) + { + Core::DisplayMessage("Failed to send a command to the Bluetooth adapter.", 10000); + Core::DisplayMessage("It may not be compatible with passthrough mode.", 10000); + } + } + else + { + m_showed_failed_transfer = false; + } +} diff --git a/Source/Core/Core/IOS/USB/Bluetooth/LibUSBBluetoothAdapter.h b/Source/Core/Core/IOS/USB/Bluetooth/LibUSBBluetoothAdapter.h new file mode 100644 index 0000000000..6e0f8bc1de --- /dev/null +++ b/Source/Core/Core/IOS/USB/Bluetooth/LibUSBBluetoothAdapter.h @@ -0,0 +1,157 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include "Common/Buffer.h" +#include "Common/Timer.h" +#include "Common/WorkQueueThread.h" + +#include "Core/LibusbUtils.h" + +struct libusb_device_handle; +struct libusb_device_descriptor; +struct libusb_transfer; + +class LibUSBBluetoothAdapter +{ +public: + using BufferType = Common::UniqueBuffer; + + struct BluetoothDeviceInfo + { + u16 vid; + u16 pid; + std::string name; + }; + + static std::vector ListDevices(); + static bool IsConfiguredBluetoothDevice(u16 vid, u16 pid); + static bool HasConfiguredBluetoothDevice(); + + // Public interface is intended to be used by a single thread. + + LibUSBBluetoothAdapter(); + ~LibUSBBluetoothAdapter(); + + // Return a packet at the front of the queue, or an empty buffer when nothing is queued. + // ReceiveHCIEvent is expected to be called regularly to do some internal processing. + [[nodiscard]] BufferType ReceiveHCIEvent(); + [[nodiscard]] BufferType ReceiveACLData(); + + // Schedule a transfer to submit at a specific time. + void ScheduleBulkTransfer(u8 endpoint, std::span data, TimePoint target_time); + void ScheduleControlTransfer(u8 type, u8 request, u8 value, u8 index, std::span data, + TimePoint target_time); + + // Schedule a transfer to be submitted as soon as possible. + void SendControlTransfer(std::span data); + + bool IsWiiBTModule() const; + +private: + // Inputs will be dropped when queue is full. + static constexpr std::size_t MAX_INPUT_QUEUE_SIZE = 100; + + static constexpr u8 INTERFACE = 0x00; + + std::string m_last_open_error; + + LibusbUtils::Context m_context; + libusb_device_handle* m_handle = nullptr; + + // Protects run-flag and Push'ing to the completed transfer queue. + std::mutex m_transfers_mutex; + + bool m_run_input_transfers = true; + + // callback/worker threads push transfers here that are ready for clean up. + // This avoids locking around m_transfer_buffers. + Common::WaitableSPSCQueue m_transfers_to_free; + + // Incoming packets Push'd from libusb callback thread. + Common::SPSCQueue m_hci_event_queue; + Common::SPSCQueue m_acl_data_queue; + + // All transfers are alloc'd and kept here until complete. + std::map> m_transfer_buffers; + + struct TimedTransfer + { + libusb_transfer* transfer{}; + TimePoint target_time{}; + }; + + // Outgoing data is submitted on a worker thread. + // This allows proper packet timing without throttling the core thread. + Common::WorkQueueThreadSP m_output_worker; + + // Used by the output worker. + Common::PrecisionTimer m_precision_timer; + + // Set to reduce OSD and log spam. + bool m_showed_failed_transfer = false; + bool m_showed_long_queue_drop = false; + + // Some responses need to be fabricated if we aren't using the Nintendo BT module. + bool m_is_wii_bt_module = false; + + // Bluetooth spec's Num_HCI_Command_Packets. + // This is the number of hci commands that the host is allowed to send. + // We track this to send commands only when the controller is ready. + u8 m_num_hci_command_packets = 1; + + // HCI commands are queued when the controller isn't ready for them. + // They will be sent after COMMAND_COMPL or COMMAND_STATUS signal readiness. + std::queue m_pending_hci_transfers; + + struct OutstandingCommand + { + u16 opcode{}; + TimePoint submit_time{}; + }; + + // Sent HCI commands that have yet to receive a COMMAND_COMPL or COMMAND_STATUS response. + // Container size will be small, around 2. + std::deque m_unacknowledged_commands; + + bool IsControllerReadyForCommand() const; + + // Give the transfer to the worker and track the command appropriately. + // This should only be used when IsControllerReadyForCommand is true. + void PushHCICommand(TimedTransfer); + + template + void AcknowledgeCommand(std::span buffer); + + libusb_transfer* AllocateTransfer(std::size_t buffer_size); + void FreeTransfer(libusb_transfer*); + void CleanCompletedTransfers(); + + // Do some HCI logic and clean up completed transfers. + void Update(); + + // Sleep until the target time and then submit the transfer. + // Used by worker thread. + void SubmitTimedTransfer(TimedTransfer); + + // Immediately submit transfer with libusb. + void SubmitTransfer(libusb_transfer*); + + // Start repetitive transfers to fill the hci-event and acl-data queues. + void StartInputTransfers(); + + // LibUSB callbacks. Invoked from the LibUSB context thread. + void HandleOutputTransfer(libusb_transfer*); + void HandleInputTransfer(libusb_transfer*); + + // Error logging. + void HandleTransferError(libusb_transfer*); + + bool OpenDevice(const libusb_device_descriptor& device_descriptor, libusb_device* device); +}; diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index 02a4894609..efcd9f5a92 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -402,6 +402,7 @@ + @@ -1079,6 +1080,7 @@ + diff --git a/Source/Core/DolphinQt/Config/WiimoteControllersWidget.cpp b/Source/Core/DolphinQt/Config/WiimoteControllersWidget.cpp index 1cc1271517..ef6989ad9b 100644 --- a/Source/Core/DolphinQt/Config/WiimoteControllersWidget.cpp +++ b/Source/Core/DolphinQt/Config/WiimoteControllersWidget.cpp @@ -15,9 +15,6 @@ #include #include -#include -#include - #include "Common/Config/Config.h" #include "Common/WorkQueueThread.h" @@ -28,7 +25,7 @@ #include "Core/HW/Wiimote.h" #include "Core/HW/WiimoteReal/WiimoteReal.h" #include "Core/IOS/IOS.h" -#include "Core/IOS/USB/Bluetooth/BTReal.h" +#include "Core/IOS/USB/Bluetooth/LibUSBBluetoothAdapter.h" #include "Core/NetPlayProto.h" #include "Core/System.h" #include "Core/WiiUtils.h" @@ -40,8 +37,6 @@ #include "DolphinQt/QtUtils/SignalBlocking.h" #include "DolphinQt/Settings.h" -#include "UICommon/UICommon.h" - WiimoteControllersWidget::WiimoteControllersWidget(QWidget* parent) : QWidget(parent) { CreateLayout(); @@ -80,7 +75,7 @@ void WiimoteControllersWidget::StartBluetoothAdapterRefresh() const auto scan_func = [this]() { INFO_LOG_FMT(COMMON, "Refreshing Bluetooth adapter list..."); - auto device_list = IOS::HLE::BluetoothRealDevice::ListDevices(); + auto device_list = LibUSBBluetoothAdapter::ListDevices(); INFO_LOG_FMT(COMMON, "{} Bluetooth adapters available.", device_list.size()); const auto refresh_complete_func = [this, devices = std::move(device_list)]() { OnBluetoothAdapterRefreshComplete(devices); @@ -92,7 +87,7 @@ void WiimoteControllersWidget::StartBluetoothAdapterRefresh() } void WiimoteControllersWidget::OnBluetoothAdapterRefreshComplete( - const std::vector& devices) + const std::vector& devices) { const int configured_vid = Config::Get(Config::MAIN_BLUETOOTH_PASSTHROUGH_VID); const int configured_pid = Config::Get(Config::MAIN_BLUETOOTH_PASSTHROUGH_PID); @@ -114,7 +109,7 @@ void WiimoteControllersWidget::OnBluetoothAdapterRefreshComplete( m_bluetooth_adapters->addItem(device_info, QVariant::fromValue(device)); if (!found_configured_device && - IOS::HLE::BluetoothRealDevice::IsConfiguredBluetoothDevice(device.vid, device.pid)) + LibUSBBluetoothAdapter::IsConfiguredBluetoothDevice(device.vid, device.pid)) { found_configured_device = true; m_bluetooth_adapters->setCurrentIndex(m_bluetooth_adapters->count() - 1); @@ -126,7 +121,7 @@ void WiimoteControllersWidget::OnBluetoothAdapterRefreshComplete( const QString name = QLatin1Char{'['} + tr("disconnected") + QLatin1Char(']'); const std::string name_str = name.toStdString(); - IOS::HLE::BluetoothRealDevice::BluetoothDeviceInfo disconnected_device; + LibUSBBluetoothAdapter::BluetoothDeviceInfo disconnected_device; disconnected_device.vid = configured_vid; disconnected_device.pid = configured_pid; disconnected_device.name = name_str; @@ -314,8 +309,8 @@ void WiimoteControllersWidget::OnBluetoothPassthroughDeviceChanged(int index) return; } - auto device_info = m_bluetooth_adapters->itemData(index) - .value(); + auto device_info = + m_bluetooth_adapters->itemData(index).value(); Config::SetBaseOrCurrent(Config::MAIN_BLUETOOTH_PASSTHROUGH_PID, device_info.pid); Config::SetBaseOrCurrent(Config::MAIN_BLUETOOTH_PASSTHROUGH_VID, device_info.vid); diff --git a/Source/Core/DolphinQt/Config/WiimoteControllersWidget.h b/Source/Core/DolphinQt/Config/WiimoteControllersWidget.h index b174c1b786..e8507d9866 100644 --- a/Source/Core/DolphinQt/Config/WiimoteControllersWidget.h +++ b/Source/Core/DolphinQt/Config/WiimoteControllersWidget.h @@ -8,7 +8,7 @@ #include #include "Common/WorkQueueThread.h" -#include "Core/IOS/USB/Bluetooth/BTReal.h" +#include "Core/IOS/USB/Bluetooth/LibUSBBluetoothAdapter.h" class QCheckBox; class QComboBox; @@ -39,7 +39,7 @@ private: void OnBluetoothPassthroughSyncPressed(); void OnBluetoothPassthroughResetPressed(); void OnBluetoothAdapterRefreshComplete( - const std::vector& devices); + const std::vector& devices); void OnWiimoteRefreshPressed(); void OnWiimoteConfigure(size_t index); void StartBluetoothAdapterRefresh(); From 0e25979449e0386c485e0997c5315d7edb84c991 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Thu, 12 Jun 2025 02:10:59 -0500 Subject: [PATCH 3/6] BTReal: Attempt to configure HCI_SERVICE_TYPE_GUARANTEED on all connections for improved performance with certain adapters. --- Source/Core/Core/IOS/USB/Bluetooth/BTReal.cpp | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/Source/Core/Core/IOS/USB/Bluetooth/BTReal.cpp b/Source/Core/Core/IOS/USB/Bluetooth/BTReal.cpp index 9ba818c854..1647c3db3a 100644 --- a/Source/Core/Core/IOS/USB/Bluetooth/BTReal.cpp +++ b/Source/Core/Core/IOS/USB/Bluetooth/BTReal.cpp @@ -251,6 +251,37 @@ auto BluetoothRealDevice::ProcessHCIEvent(BufferType buffer) -> BufferType SendHCIStoreLinkKeyCommand(); } } + else if (event == HCI_EVENT_CON_COMPL) + { + // Some devices (e.g. Sena UD100 0a12:0001) default to HCI_SERVICE_TYPE_BEST_EFFORT. + // This can cause less than 200hz input and drop-outs in some games (e.g. Brawl). + + // We configure HCI_SERVICE_TYPE_GUARANTEED for each new connection. + // This solves dropped input issues at least for the mentioned Sena adapter. + + INFO_LOG_FMT(IOS_WIIMOTE, "Sending HCI_CMD_QOS_SETUP"); + + HCICommandPayload payload; + + // Copy the connection handle. + std::memcpy(&payload.command.con_handle, buffer.data() + 3, sizeof(payload.command.con_handle)); + + payload.command.service_type = HCI_SERVICE_TYPE_GUARANTEED; + payload.command.token_rate = 0xffffffff; + payload.command.peak_bandwidth = 0xffffffff; + payload.command.latency = 10000; + payload.command.delay_variation = 0xffffffff; + + m_lib_usb_bt_adapter->SendControlTransfer(AsU8Span(payload)); + } + else if (event == HCI_EVENT_QOS_SETUP_COMPL) + { + const auto service_type = buffer[6]; + if (service_type != HCI_SERVICE_TYPE_GUARANTEED) + WARN_LOG_FMT(IOS_WIIMOTE, "Got HCI_EVENT_QOS_SETUP_COMPL service_type: {}", service_type); + else + INFO_LOG_FMT(IOS_WIIMOTE, "Got HCI_EVENT_QOS_SETUP_COMPL HCI_SERVICE_TYPE_GUARANTEED"); + } return buffer; } From 25583658d2d2d60ecf37f69b72a6d4e381561e9a Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Thu, 12 Jun 2025 02:26:37 -0500 Subject: [PATCH 4/6] State: Increase STATE_VERSION. --- Source/Core/Core/State.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Core/Core/State.cpp b/Source/Core/Core/State.cpp index 49ff84dd30..bbfb92ebe5 100644 --- a/Source/Core/Core/State.cpp +++ b/Source/Core/Core/State.cpp @@ -95,7 +95,7 @@ static size_t s_state_writes_in_queue; static std::condition_variable s_state_write_queue_is_empty; // Don't forget to increase this after doing changes on the savestate system -constexpr u32 STATE_VERSION = 174; // Last changed in PR 13342 +constexpr u32 STATE_VERSION = 175; // Last changed in PR 13751 // Increase this if the StateExtendedHeader definition changes constexpr u32 EXTENDED_HEADER_VERSION = 1; // Last changed in PR 12217 From 7fe4a6e4f374a1b109e6cf97a7c289e7b828931d Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Tue, 17 Jun 2025 22:57:01 -0500 Subject: [PATCH 5/6] LibUSBBluetoothAdapter: Change the request type of our generated HCI commands from LIBUSB_RECIPIENT_INTERFACE to LIBUSB_RECIPIENT_DEVICE. This changes the value from 0x21 to 0x20 which now matches the value that Wii software generates. --- Source/Core/Core/IOS/USB/Bluetooth/LibUSBBluetoothAdapter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Core/Core/IOS/USB/Bluetooth/LibUSBBluetoothAdapter.cpp b/Source/Core/Core/IOS/USB/Bluetooth/LibUSBBluetoothAdapter.cpp index 87ce4d8235..2264d4d956 100644 --- a/Source/Core/Core/IOS/USB/Bluetooth/LibUSBBluetoothAdapter.cpp +++ b/Source/Core/Core/IOS/USB/Bluetooth/LibUSBBluetoothAdapter.cpp @@ -28,7 +28,7 @@ constexpr std::size_t BUFFER_SIZE = 1024; constexpr auto HCI_COMMAND_TIMEOUT = std::chrono::milliseconds{1000}; constexpr u8 REQUEST_TYPE = - u8(u8(LIBUSB_ENDPOINT_OUT) | u8(LIBUSB_REQUEST_TYPE_CLASS)) | u8(LIBUSB_RECIPIENT_INTERFACE); + u8(u8(LIBUSB_ENDPOINT_OUT) | u8(LIBUSB_REQUEST_TYPE_CLASS)) | u8(LIBUSB_RECIPIENT_DEVICE); constexpr u8 HCI_EVENT = 0x81; constexpr u8 ACL_DATA_IN = 0x82; From 936887838a9439ef96b1864aa13c529b839df6f4 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Sun, 22 Jun 2025 22:05:35 -0500 Subject: [PATCH 6/6] BTReal: Don't falsely increase a controller's ACL packet number buffer size. WARN_LOG if the size is smaller than that of the original BT module. --- Source/Core/Core/IOS/USB/Bluetooth/BTReal.cpp | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/Source/Core/Core/IOS/USB/Bluetooth/BTReal.cpp b/Source/Core/Core/IOS/USB/Bluetooth/BTReal.cpp index 1647c3db3a..0fc76cff55 100644 --- a/Source/Core/Core/IOS/USB/Bluetooth/BTReal.cpp +++ b/Source/Core/Core/IOS/USB/Bluetooth/BTReal.cpp @@ -229,17 +229,34 @@ auto BluetoothRealDevice::ProcessHCIEvent(BufferType buffer) -> BufferType if (ev.opcode == HCI_CMD_READ_BUFFER_SIZE) { - // Due to how the widcomm stack which Nintendo uses is coded, we must never - // let the stack think the controller is buffering more than 10 data packets - // - it will cause a u8 underflow and royally screw things up. - // Therefore, the reply to this command has to be faked to avoid random, weird issues - // (including Wiimote disconnects and "event mismatch" warning messages). - DEBUG_LOG_FMT(IOS_WIIMOTE, "Adjusting HCI_CMD_READ_BUFFER_SIZE results."); - hci_read_buffer_size_rp reply; - reply.status = 0x00; + std::memcpy(&reply, buffer.data() + sizeof(hci_event_hdr_t) + sizeof(ev), sizeof(reply)); + + if (reply.status != 0x00) + { + ERROR_LOG_FMT(IOS_WIIMOTE, "HCI_CMD_READ_BUFFER_SIZE status: 0x{:02x}.", reply.status); + + reply.status = 0x00; + reply.num_acl_pkts = ACL_PKT_NUM; + } + else if (reply.num_acl_pkts > ACL_PKT_NUM) + { + // Due to how the widcomm stack which Nintendo uses is coded, we must never + // let the stack think the controller is buffering more than 10 data packets + // - it will cause a u8 underflow and royally screw things up. + // Therefore, the reply to this command has to be faked to avoid random, weird issues + // (including Wiimote disconnects and "event mismatch" warning messages). + reply.num_acl_pkts = ACL_PKT_NUM; + } + else if (reply.num_acl_pkts < ACL_PKT_NUM) + { + // The controller buffers fewer ACL packets than the original BT module. + WARN_LOG_FMT(IOS_WIIMOTE, "HCI_CMD_READ_BUFFER_SIZE num_acl_pkts({}) < ACL_PKT_NUM({})", + reply.num_acl_pkts, ACL_PKT_NUM); + } + + // Force the other the parameters to match that of the original BT module. reply.max_acl_size = ACL_PKT_SIZE; - reply.num_acl_pkts = ACL_PKT_NUM; reply.max_sco_size = SCO_PKT_SIZE; reply.num_sco_pkts = SCO_PKT_NUM;