IOS/USB: Add "Connect USB Keyboard" support for HID

This commit is contained in:
Sepalani 2025-05-08 23:44:29 +04:00
parent 3513e5c906
commit fc2ae3b880
7 changed files with 448 additions and 0 deletions

View File

@ -69,13 +69,16 @@ enum
}
using HIDPressedKeys = std::array<u8, 6>;
#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
{

View File

@ -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

View File

@ -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,

View File

@ -0,0 +1,316 @@
// 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<ConfigDescriptor> HIDKeyboard::GetConfigurations() const
{
return m_config_descriptor;
}
std::vector<InterfaceDescriptor> HIDKeyboard::GetInterfaces(u8 config) const
{
return m_interface_descriptor;
}
std::vector<EndpointDescriptor> 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<CtrlMessage> 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<u8>(cmd->index) != m_active_interface)
{
const int ret = ChangeInterface(static_cast<u8>(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<u8>(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):
{
WARN_LOG_FMT(IOS_USB, "SET_IDLE not implemented (value={:04x}, index={})", cmd->value,
cmd->index);
// TODO: Handle idle duration and implement NAK
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<HIDProtocol>(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<BulkMessage> 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<IntrMessage> cmd)
{
static auto start_time = std::chrono::steady_clock::now();
const auto current_time = std::chrono::steady_clock::now();
const bool should_poll =
(current_time - start_time) >= POLLING_RATE && ControlReference::GetInputGate();
const Common::HIDPressedState state =
should_poll ? m_keyboard_context->GetPressedState() : m_last_state;
// We can't use cmd->ScheduleTransferCompletion here as it might provoke
// invalid memory access with scheduled transfers when CancelTransfer is called.
EnqueueTransfer(std::move(cmd), state);
if (should_poll)
{
m_last_state = std::move(state);
start_time = std::chrono::steady_clock::now();
}
return IPC_SUCCESS;
}
int HIDKeyboard::SubmitTransfer(std::unique_ptr<IsoMessage> 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::EnqueueTransfer(std::unique_ptr<IntrMessage> msg,
const Common::HIDPressedState& state)
{
msg->FillBuffer(reinterpret_cast<const u8*>(&state), sizeof(state));
auto transfer = std::make_shared<PendingTransfer>(std::move(msg));
m_worker.EmplaceItem(transfer);
{
std::lock_guard lock(m_pending_lock);
m_pending_tranfers.insert(transfer);
}
}
void HIDKeyboard::HandlePendingTransfer(std::shared_ptr<PendingTransfer> transfer)
{
std::unique_lock lock(m_pending_lock);
if (transfer->IsCanceled())
return;
while (!transfer->IsReady())
{
lock.unlock();
std::this_thread::sleep_for(POLLING_RATE / 2);
lock.lock();
if (transfer->IsCanceled())
return;
}
transfer->Do();
m_pending_tranfers.erase(transfer);
}
void HIDKeyboard::CancelPendingTransfers()
{
m_worker.Cancel();
{
std::lock_guard lock(m_pending_lock);
for (auto& transfer : m_pending_tranfers)
transfer->Cancel();
m_pending_tranfers.clear();
}
}
HIDKeyboard::PendingTransfer::PendingTransfer(std::unique_ptr<IntrMessage> 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::IsReady() const
{
return (std::chrono::steady_clock::now() - m_time) >= POLLING_RATE;
}
bool HIDKeyboard::PendingTransfer::IsCanceled() const
{
return m_is_canceled;
}
void HIDKeyboard::PendingTransfer::Do()
{
m_msg->ScheduleTransferCompletion(IPC_SUCCESS, 0);
m_pending = false;
}
void HIDKeyboard::PendingTransfer::Cancel()
{
m_is_canceled = true;
}
} // namespace IOS::HLE::USB

View File

@ -0,0 +1,96 @@
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <chrono>
#include <map>
#include <memory>
#include <mutex>
#include <set>
#include <vector>
#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<ConfigDescriptor> GetConfigurations() const override;
std::vector<InterfaceDescriptor> GetInterfaces(u8 config) const override;
std::vector<EndpointDescriptor> 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<CtrlMessage> message) override;
int SubmitTransfer(std::unique_ptr<BulkMessage> message) override;
int SubmitTransfer(std::unique_ptr<IntrMessage> message) override;
int SubmitTransfer(std::unique_ptr<IsoMessage> message) override;
static constexpr auto POLLING_RATE = std::chrono::milliseconds(8); // 125 Hz
private:
class PendingTransfer
{
public:
PendingTransfer(std::unique_ptr<IntrMessage> msg);
~PendingTransfer();
bool IsReady() const;
bool IsCanceled() const;
void Do();
void Cancel();
private:
std::unique_ptr<IntrMessage> m_msg;
std::chrono::steady_clock::time_point m_time;
bool m_is_canceled = false;
bool m_pending = true;
};
void EnqueueTransfer(std::unique_ptr<IntrMessage> msg, const Common::HIDPressedState& state);
void HandlePendingTransfer(std::shared_ptr<PendingTransfer> transfer);
void CancelPendingTransfers();
Common::WorkQueueThreadSP<std::shared_ptr<PendingTransfer>> m_worker;
std::mutex m_pending_lock;
std::set<std::shared_ptr<PendingTransfer>> m_pending_tranfers;
HIDProtocol m_current_protocol = HIDProtocol::Report;
Common::HIDPressedState m_last_state;
std::shared_ptr<Common::KeyboardContext> 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<ConfigDescriptor> 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<InterfaceDescriptor> 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<u8, std::vector<EndpointDescriptor>> 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

View File

@ -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"
@ -187,6 +188,11 @@ void USBScanner::AddEmulatedDevices(DeviceMap* new_devices)
auto wii_speak = std::make_unique<USB::WiiSpeak>();
AddDevice(std::move(wii_speak), new_devices);
}
if (Config::Get(Config::MAIN_WII_KEYBOARD) && !NetPlay::IsNetPlayRunning())
{
auto keyboard = std::make_unique<USB::HIDKeyboard>();
AddDevice(std::move(keyboard), new_devices);
}
}
void USBScanner::WakeupSantrollerDevice(libusb_device* device)

View File

@ -407,6 +407,7 @@
<ClInclude Include="Core\IOS\USB\Bluetooth\WiimoteDevice.h" />
<ClInclude Include="Core\IOS\USB\Bluetooth\WiimoteHIDAttr.h" />
<ClInclude Include="Core\IOS\USB\Common.h" />
<ClInclude Include="Core\IOS\USB\Emulated\HIDKeyboard.h" />
<ClInclude Include="Core\IOS\USB\Emulated\Infinity.h" />
<ClInclude Include="Core\IOS\USB\Emulated\Microphone.h" />
<ClInclude Include="Core\IOS\USB\Emulated\Skylanders\Skylander.h" />
@ -1078,6 +1079,7 @@
<ClCompile Include="Core\IOS\USB\Bluetooth\WiimoteDevice.cpp" />
<ClCompile Include="Core\IOS\USB\Bluetooth\WiimoteHIDAttr.cpp" />
<ClCompile Include="Core\IOS\USB\Common.cpp" />
<ClCompile Include="Core\IOS\USB\Emulated\HIDKeyboard.cpp" />
<ClCompile Include="Core\IOS\USB\Emulated\Infinity.cpp" />
<ClCompile Include="Core\IOS\USB\Emulated\Microphone.cpp" />
<ClCompile Include="Core\IOS\USB\Emulated\Skylanders\Skylander.cpp" />