diff --git a/Source/Core/Common/Keyboard.h b/Source/Core/Common/Keyboard.h index 8a3b6ca08e..7c7ef31c88 100644 --- a/Source/Core/Common/Keyboard.h +++ b/Source/Core/Common/Keyboard.h @@ -69,13 +69,16 @@ enum LayoutEnum } using HIDPressedKeys = std::array; +#pragma pack(push, 1) struct HIDPressedState { u8 modifiers = 0; + u8 oem = 0; HIDPressedKeys pressed_keys{}; auto operator<=>(const HIDPressedState&) const = default; }; +#pragma pack(pop) class KeyboardContext { diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index 6e7ff1a0bc..63e08f1c24 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -427,6 +427,8 @@ add_library(core IOS/USB/Bluetooth/WiimoteHIDAttr.h IOS/USB/Common.cpp IOS/USB/Common.h + IOS/USB/Emulated/HIDKeyboard.cpp + IOS/USB/Emulated/HIDKeyboard.h IOS/USB/Emulated/Infinity.cpp IOS/USB/Emulated/Infinity.h IOS/USB/Emulated/Microphone.cpp diff --git a/Source/Core/Core/IOS/USB/Common.h b/Source/Core/Core/IOS/USB/Common.h index fbd1de202d..b3ebdec6d5 100644 --- a/Source/Core/Core/IOS/USB/Common.h +++ b/Source/Core/Core/IOS/USB/Common.h @@ -24,11 +24,34 @@ enum StandardDeviceRequestCodes REQUEST_SET_INTERFACE = 11, }; +// See USB HID specification under "Class-Specific Requests": +// - https://www.usb.org/sites/default/files/documents/hid1_11.pdf +namespace HIDRequestCodes +{ +enum +{ + GET_REPORT = 1, + GET_IDLE = 2, + GET_PROTOCOL = 3, + // 0x04~0x08 - Reserved + SET_REPORT = 9, + SET_IDLE = 10, + SET_PROTOCOL = 11, +}; +} + +enum class HIDProtocol : u16 +{ + Boot = 0, + Report = 1, +}; + enum ControlRequestTypes { DIR_HOST2DEVICE = 0, DIR_DEVICE2HOST = 1, TYPE_STANDARD = 0, + TYPE_CLASS = 1, TYPE_VENDOR = 2, REC_DEVICE = 0, REC_INTERFACE = 1, diff --git a/Source/Core/Core/IOS/USB/Emulated/HIDKeyboard.cpp b/Source/Core/Core/IOS/USB/Emulated/HIDKeyboard.cpp new file mode 100644 index 0000000000..9e134d977f --- /dev/null +++ b/Source/Core/Core/IOS/USB/Emulated/HIDKeyboard.cpp @@ -0,0 +1,331 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/IOS/USB/Emulated/HIDKeyboard.h" + +#include "Core/Config/MainSettings.h" +#include "Core/HW/Memmap.h" +#include "Core/System.h" +#include "InputCommon/ControlReference/ControlReference.h" + +namespace IOS::HLE::USB +{ +HIDKeyboard::HIDKeyboard() +{ + m_id = u64(m_vid) << 32 | u64(m_pid) << 16 | u64(8) << 8 | u64(1); +} + +HIDKeyboard::~HIDKeyboard() +{ + if (!m_device_attached) + return; + CancelPendingTransfers(); +} + +DeviceDescriptor HIDKeyboard::GetDeviceDescriptor() const +{ + return m_device_descriptor; +} + +std::vector HIDKeyboard::GetConfigurations() const +{ + return m_config_descriptor; +} + +std::vector HIDKeyboard::GetInterfaces(u8 config) const +{ + return m_interface_descriptor; +} + +std::vector HIDKeyboard::GetEndpoints(u8 config, u8 interface, u8 alt) const +{ + if (const auto it{m_endpoint_descriptor.find(interface)}; it != m_endpoint_descriptor.end()) + { + return it->second; + } + return {}; +} + +bool HIDKeyboard::Attach() +{ + if (m_device_attached) + return true; + + INFO_LOG_FMT(IOS_USB, "[{:04x}:{:04x}] Opening emulated keyboard", m_vid, m_pid); + m_keyboard_context = Common::KeyboardContext::GetInstance(); + m_worker.Reset("HID Keyboard", [this](auto transfer) { HandlePendingTransfer(transfer); }); + m_device_attached = true; + return true; +} + +bool HIDKeyboard::AttachAndChangeInterface(const u8 interface) +{ + if (!Attach()) + return false; + + if (interface != m_active_interface) + return ChangeInterface(interface) == 0; + + return true; +} + +int HIDKeyboard::CancelTransfer(const u8 endpoint) +{ + if (endpoint != KEYBOARD_ENDPOINT) + { + ERROR_LOG_FMT( + IOS_USB, + "[{:04x}:{:04x} {}] Cancelling transfers for invalid endpoint {:#x} (expected: {:#x})", + m_vid, m_pid, m_active_interface, endpoint, KEYBOARD_ENDPOINT); + return IPC_SUCCESS; + } + + INFO_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Cancelling transfers (endpoint {:#x})", m_vid, m_pid, + m_active_interface, endpoint); + CancelPendingTransfers(); + + return IPC_SUCCESS; +} + +int HIDKeyboard::ChangeInterface(const u8 interface) +{ + DEBUG_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Changing interface to {}", m_vid, m_pid, + m_active_interface, interface); + m_active_interface = interface; + return 0; +} + +int HIDKeyboard::GetNumberOfAltSettings(u8 interface) +{ + return 0; +} + +int HIDKeyboard::SetAltSetting(u8 alt_setting) +{ + return 0; +} + +int HIDKeyboard::SubmitTransfer(std::unique_ptr cmd) +{ + DEBUG_LOG_FMT(IOS_USB, + "[{:04x}:{:04x} {}] Control: bRequestType={:02x} bRequest={:02x} wValue={:04x}" + " wIndex={:04x} wLength={:04x}", + m_vid, m_pid, m_active_interface, cmd->request_type, cmd->request, cmd->value, + cmd->index, cmd->length); + + auto& ios = cmd->GetEmulationKernel(); + + switch (cmd->request_type << 8 | cmd->request) + { + case USBHDR(DIR_DEVICE2HOST, TYPE_STANDARD, REC_INTERFACE, REQUEST_GET_INTERFACE): + { + constexpr u8 data{1}; + cmd->FillBuffer(&data, sizeof(data)); + cmd->ScheduleTransferCompletion(1, 100); + break; + } + case USBHDR(DIR_HOST2DEVICE, TYPE_STANDARD, REC_INTERFACE, REQUEST_SET_INTERFACE): + { + INFO_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] REQUEST_SET_INTERFACE index={:04x} value={:04x}", + m_vid, m_pid, m_active_interface, cmd->index, cmd->value); + if (static_cast(cmd->index) != m_active_interface) + { + const int ret = ChangeInterface(static_cast(cmd->index)); + if (ret < 0) + { + ERROR_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Failed to change interface to {}", m_vid, m_pid, + m_active_interface, cmd->index); + return ret; + } + } + const int ret = SetAltSetting(static_cast(cmd->value)); + if (ret == 0) + ios.EnqueueIPCReply(cmd->ios_request, cmd->length); + return ret; + } + case USBHDR(DIR_HOST2DEVICE, TYPE_CLASS, REC_INTERFACE, HIDRequestCodes::SET_REPORT): + { + // According to the HID specification: + // - A device might choose to ignore input Set_Report requests as meaningless. + // - Alternatively these reports could be used to reset the origin of a control + // (that is, current position should report zero). + // - The effect of sent reports will also depend on whether the recipient controls + // are absolute or relative. + const u8 report_type = cmd->value >> 8; + const u8 report_id = cmd->value & 0xFF; + auto& memory = ios.GetSystem().GetMemory(); + + // The data seems to report LED status for keys such as: + // - NUM LOCK, CAPS LOCK + const u8* data = memory.GetPointerForRange(cmd->data_address, cmd->length); + INFO_LOG_FMT(IOS_USB, "SET_REPORT ignored (report_type={:02x}, report_id={:02x}, index={})\n{}", + report_type, report_id, cmd->index, HexDump(data, cmd->length)); + ios.EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS); + break; + } + case USBHDR(DIR_HOST2DEVICE, TYPE_CLASS, REC_INTERFACE, HIDRequestCodes::SET_IDLE): + { + const u8 duration = cmd->value >> 8; + const u8 report_id = cmd->value & 0xFF; + + if (duration == 0) + { + INFO_LOG_FMT(IOS_USB, "SET_IDLE duration to indefinite (report_id={:02x}, index={})", + report_id, cmd->index); + const std::lock_guard lock(m_pending_lock); + m_idle_duration.reset(); + } + else + { + const auto idle_duration = 4 * duration; // 4 millisecond resolution (see HID specification) + INFO_LOG_FMT(IOS_USB, "SET_IDLE duration to {} milliseconds (report_id={:02x}, index={})", + idle_duration, report_id, cmd->index); + const std::lock_guard lock(m_pending_lock); + m_idle_duration = std::chrono::milliseconds(idle_duration); + } + + ios.EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS); + break; + } + case USBHDR(DIR_HOST2DEVICE, TYPE_CLASS, REC_INTERFACE, HIDRequestCodes::SET_PROTOCOL): + { + INFO_LOG_FMT(IOS_USB, "SET_PROTOCOL: value={}, index={}", cmd->value, cmd->index); + const HIDProtocol protocol = static_cast(cmd->value); + switch (protocol) + { + case HIDProtocol::Boot: + case HIDProtocol::Report: + m_current_protocol = protocol; + break; + default: + WARN_LOG_FMT(IOS_USB, "SET_PROTOCOL: Unknown protocol {} for interface {}", cmd->value, + cmd->index); + } + ios.EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS); + break; + } + default: + WARN_LOG_FMT(IOS_USB, "Unknown command, req={:02x}, type={:02x}", cmd->request, + cmd->request_type); + ios.EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS); + } + + return IPC_SUCCESS; +} + +int HIDKeyboard::SubmitTransfer(std::unique_ptr cmd) +{ + DEBUG_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Bulk: length={:04x} endpoint={:02x}", m_vid, m_pid, + m_active_interface, cmd->length, cmd->endpoint); + cmd->GetEmulationKernel().EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS); + return IPC_SUCCESS; +} + +int HIDKeyboard::SubmitTransfer(std::unique_ptr cmd) +{ + // We can't use cmd->ScheduleTransferCompletion here as it might provoke + // invalid memory access with scheduled transfers when CancelTransfer is called. + auto transfer = std::make_shared(std::move(cmd)); + { + const std::lock_guard lock(m_pending_lock); + m_pending_tranfers.insert(transfer); + } + m_worker.EmplaceItem(transfer); + + return IPC_SUCCESS; +} + +int HIDKeyboard::SubmitTransfer(std::unique_ptr cmd) +{ + DEBUG_LOG_FMT(IOS_USB, + "[{:04x}:{:04x} {}] Isochronous: length={:04x} endpoint={:02x} num_packets={:02x}", + m_vid, m_pid, m_active_interface, cmd->length, cmd->endpoint, cmd->num_packets); + cmd->ScheduleTransferCompletion(IPC_SUCCESS, 2000); + return IPC_SUCCESS; +} + +void HIDKeyboard::HandlePendingTransfer(std::shared_ptr transfer) +{ + static constexpr auto SLEEP_DURATION = POLLING_RATE / 2; + + std::unique_lock lock(m_pending_lock); + if (transfer->IsCanceled()) + return; + + Common::HIDPressedState state; + if (ControlReference::GetInputGate()) + state = m_keyboard_context->GetPressedState(); + + while (state == m_last_state && transfer->Idle(m_idle_duration)) + { + lock.unlock(); + std::this_thread::sleep_for(SLEEP_DURATION); + lock.lock(); + + if (transfer->IsCanceled()) + return; + + if (ControlReference::GetInputGate()) + state = m_keyboard_context->GetPressedState(); + } + + transfer->Do(state); + m_pending_tranfers.erase(transfer); + m_last_state = std::move(state); +} + +void HIDKeyboard::CancelPendingTransfers() +{ + { + const std::lock_guard lock(m_pending_lock); + for (auto& transfer : m_pending_tranfers) + transfer->Cancel(); + m_pending_tranfers.clear(); + } + m_worker.Cancel(); +} + +HIDKeyboard::PendingTransfer::PendingTransfer(std::unique_ptr msg) +{ + m_time = std::chrono::steady_clock::now(); + m_msg = std::move(msg); +} + +HIDKeyboard::PendingTransfer::~PendingTransfer() +{ + if (!m_pending) + return; + // Value based on LibusbDevice's HandleTransfer implementation + m_msg->ScheduleTransferCompletion(-5, 0); +} + +bool HIDKeyboard::PendingTransfer::Idle( + std::optional idle_duration) const +{ + if (!idle_duration.has_value()) + { + // Based on the HID specification for Set_Idle Request: + // - inhibit reporting forever, + // - only reporting when a change is detected in the report data + return true; + } + return (std::chrono::steady_clock::now() - m_time) < *idle_duration; +} + +bool HIDKeyboard::PendingTransfer::IsCanceled() const +{ + return m_is_canceled; +} + +void HIDKeyboard::PendingTransfer::Do(const Common::HIDPressedState& state) +{ + m_msg->FillBuffer(reinterpret_cast(&state), sizeof(state)); + m_msg->ScheduleTransferCompletion(IPC_SUCCESS, 0); + m_pending = false; +} + +void HIDKeyboard::PendingTransfer::Cancel() +{ + m_is_canceled = true; +} +} // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/Emulated/HIDKeyboard.h b/Source/Core/Core/IOS/USB/Emulated/HIDKeyboard.h new file mode 100644 index 0000000000..149e6a8487 --- /dev/null +++ b/Source/Core/Core/IOS/USB/Emulated/HIDKeyboard.h @@ -0,0 +1,96 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "Common/CommonTypes.h" +#include "Common/Keyboard.h" +#include "Common/WorkQueueThread.h" +#include "Core/IOS/USB/Common.h" + +namespace IOS::HLE::USB +{ +class HIDKeyboard final : public Device +{ +public: + HIDKeyboard(); + ~HIDKeyboard() override; + + DeviceDescriptor GetDeviceDescriptor() const override; + std::vector GetConfigurations() const override; + std::vector GetInterfaces(u8 config) const override; + std::vector GetEndpoints(u8 config, u8 interface, u8 alt) const override; + bool Attach() override; + bool AttachAndChangeInterface(u8 interface) override; + int CancelTransfer(u8 endpoint) override; + int ChangeInterface(u8 interface) override; + int GetNumberOfAltSettings(u8 interface) override; + int SetAltSetting(u8 alt_setting) override; + int SubmitTransfer(std::unique_ptr message) override; + int SubmitTransfer(std::unique_ptr message) override; + int SubmitTransfer(std::unique_ptr message) override; + int SubmitTransfer(std::unique_ptr message) override; + + static constexpr auto POLLING_RATE = std::chrono::milliseconds(8); // 125 Hz + +private: + class PendingTransfer + { + public: + PendingTransfer(std::unique_ptr msg); + ~PendingTransfer(); + + bool Idle(std::optional idle_duration) const; + bool IsCanceled() const; + void Do(const Common::HIDPressedState& state); + void Cancel(); + + private: + std::unique_ptr m_msg; + std::chrono::steady_clock::time_point m_time; + bool m_is_canceled = false; + bool m_pending = true; + }; + + void HandlePendingTransfer(std::shared_ptr transfer); + void CancelPendingTransfers(); + + Common::WorkQueueThreadSP> m_worker; + std::mutex m_pending_lock; + std::set> m_pending_tranfers; + std::optional m_idle_duration; + + HIDProtocol m_current_protocol = HIDProtocol::Report; + Common::HIDPressedState m_last_state; + std::shared_ptr m_keyboard_context; + + // Apple Extended Keyboard [Mitsumi] + // - Model A1058 / USB 1.1 + const u16 m_vid = 0x05ac; + const u16 m_pid = 0x020c; + u8 m_active_interface = 0; + bool m_device_attached = false; + const DeviceDescriptor m_device_descriptor{0x12, 0x01, 0x0110, 0x00, 0x00, 0x00, 0x08, + 0x05AC, 0x020C, 0x0395, 0x01, 0x03, 0x00, 0x01}; + const std::vector m_config_descriptor{ + {0x09, 0x02, 0x003B, 0x02, 0x01, 0x00, 0xA0, 0x19}}; + static constexpr u8 INTERFACE_0 = 0; + static constexpr u8 INTERFACE_1 = 1; + static constexpr u8 KEYBOARD_ENDPOINT = 0x81; + static constexpr u8 HUB_ENDPOINT = 0x82; + const std::vector m_interface_descriptor{ + {0x09, 0x04, INTERFACE_0, 0x00, 0x01, 0x03, 0x01, 0x01, 0x00}, + {0x09, 0x04, INTERFACE_1, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00}}; + const std::map> m_endpoint_descriptor{ + {INTERFACE_0, {{0x07, 0x05, KEYBOARD_ENDPOINT, 0x03, 0x0008, 0x0A}}}, + {INTERFACE_1, {{0x07, 0x05, HUB_ENDPOINT, 0x03, 0x0004, 0x0A}}}, + }; +}; +} // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/USBScanner.cpp b/Source/Core/Core/IOS/USB/USBScanner.cpp index aa213ce412..efc45e62f0 100644 --- a/Source/Core/Core/IOS/USB/USBScanner.cpp +++ b/Source/Core/Core/IOS/USB/USBScanner.cpp @@ -21,6 +21,7 @@ #include "Core/Config/MainSettings.h" #include "Core/Core.h" #include "Core/IOS/USB/Common.h" +#include "Core/IOS/USB/Emulated/HIDKeyboard.h" #include "Core/IOS/USB/Emulated/Infinity.h" #include "Core/IOS/USB/Emulated/Skylanders/Skylander.h" #include "Core/IOS/USB/Emulated/WiiSpeak.h" @@ -189,6 +190,11 @@ void USBScanner::AddEmulatedDevices(DeviceMap* new_devices) auto wii_speak = std::make_unique(); AddDevice(std::move(wii_speak), new_devices); } + if (Config::Get(Config::MAIN_WII_KEYBOARD) && !NetPlay::IsNetPlayRunning()) + { + auto keyboard = std::make_unique(); + AddDevice(std::move(keyboard), new_devices); + } } void USBScanner::WakeupSantrollerDevice(libusb_device* device) diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index dfc6c74d88..9159daef8d 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -411,6 +411,7 @@ + @@ -1089,6 +1090,7 @@ +