Prototype adapter XInput feasibility

This commit is contained in:
Joey Yakimowich-Payne 2026-09-02 07:53:18 -06:00
commit 17b3d7399c
13 changed files with 953 additions and 13 deletions

View file

@ -24,6 +24,8 @@ if (EXISTS ${picoVscode})
endif()
# ====================================================================================
option(SWITCH_PICO_LOG "Enable UART debug logging" OFF)
option(SWITCH_PICO_ADAPTER_FEASIBILITY
"Build the automatic Switch/XInput feasibility prototype" OFF)
set(SWITCH_PICO_INPUT_BACKEND "UART" CACHE STRING "Controller input backend")
set_property(CACHE SWITCH_PICO_INPUT_BACKEND PROPERTY STRINGS UART BLUEPAD32)
if(NOT SWITCH_PICO_INPUT_BACKEND STREQUAL "UART"
@ -38,6 +40,11 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32"
message(FATAL_ERROR
"SWITCH_PICO_INPUT_BACKEND=BLUEPAD32 requires PICO_BOARD=pico2_w")
endif()
if(SWITCH_PICO_ADAPTER_FEASIBILITY
AND NOT SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
message(FATAL_ERROR
"SWITCH_PICO_ADAPTER_FEASIBILITY requires the BLUEPAD32 backend")
endif()
# Pull in Raspberry Pi Pico SDK (must be before project)
include(pico_sdk_import.cmake)
@ -95,6 +102,15 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
bootsel_pairing_button.cpp
usb_pairing_management.cpp
)
if(SWITCH_PICO_ADAPTER_FEASIBILITY)
target_sources(switch-pico PRIVATE
adapter_host_probe.cpp
xinput_feasibility_driver.cpp
)
target_compile_definitions(switch-pico PRIVATE
SWITCH_PICO_ADAPTER_FEASIBILITY=1
)
endif()
target_compile_definitions(switch-pico PRIVATE
SWITCH_PICO_BLUEPAD32=1
SWITCH_PICO_HID_INSTANCE_COUNT=4
@ -132,6 +148,9 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
pico_multicore
pico_flash
)
if(SWITCH_PICO_ADAPTER_FEASIBILITY)
target_link_libraries(switch-pico hardware_watchdog)
endif()
endif()
if (SWITCH_PICO_LOG)

87
adapter_host_probe.cpp Normal file
View file

@ -0,0 +1,87 @@
#include "adapter_host_probe.h"
#include <stddef.h>
#ifdef SWITCH_PICO_LOG
#include <stdio.h>
#define PROBE_LOG(...) printf(__VA_ARGS__)
#else
#define PROBE_LOG(...) ((void)0)
#endif
#include "adapter_host_probe_state.h"
#include "hardware/structs/watchdog.h"
#include "hardware/watchdog.h"
#include "pico/time.h"
#include "xinput_feasibility_descriptors.h"
namespace {
constexpr uint32_t kXInputBootMagic = 0x58494e50; // "XINP"
constexpr uint8_t kModeScratchRegister = 0;
AdapterUsbMode g_mode = AdapterUsbMode::kSwitchProbe;
AdapterHostProbeState g_probe;
uint32_t now_ms() {
return static_cast<uint32_t>(to_ms_since_boot(get_absolute_time()));
}
} // namespace
void adapter_host_probe_init() {
if (watchdog_hw->scratch[kModeScratchRegister] == kXInputBootMagic) {
watchdog_hw->scratch[kModeScratchRegister] = 0;
g_mode = AdapterUsbMode::kXInput;
} else {
g_mode = AdapterUsbMode::kSwitchProbe;
}
g_probe = {};
PROBE_LOG("[HOST PROBE] boot mode=%s\n",
g_mode == AdapterUsbMode::kXInput ? "XInput" : "Switch probe");
}
AdapterUsbMode adapter_host_probe_mode() { return g_mode; }
void adapter_host_probe_note_string_descriptor(uint8_t index) {
if (g_mode == AdapterUsbMode::kSwitchProbe && index == 0xee) {
g_probe.note_ms_os_string();
PROBE_LOG("[HOST PROBE] Microsoft OS string requested\n");
}
}
bool adapter_host_probe_vendor_control(uint8_t rhport, uint8_t stage,
tusb_control_request_t const *request) {
if (stage != CONTROL_STAGE_SETUP || request == nullptr ||
request->bmRequestType_bit.direction != TUSB_DIR_IN ||
request->bmRequestType_bit.type != TUSB_REQ_TYPE_VENDOR ||
request->bmRequestType_bit.recipient != TUSB_REQ_RCPT_DEVICE ||
request->bRequest != XInputFeasibility::kMsVendorRequest ||
request->wIndex != XInputFeasibility::kMsCompatIdIndex) {
return false;
}
if (g_mode == AdapterUsbMode::kSwitchProbe) {
g_probe.note_ms_compat_id_request(now_ms());
PROBE_LOG("[HOST PROBE] Microsoft compatible-ID request confirmed\n");
return tud_control_xfer(
rhport, request,
const_cast<uint8_t *>(
XInputFeasibility::kProbeMsCompatIdDescriptor),
sizeof(XInputFeasibility::kProbeMsCompatIdDescriptor));
}
return tud_control_xfer(
rhport, request,
const_cast<uint8_t *>(XInputFeasibility::kMsCompatIdDescriptor),
sizeof(XInputFeasibility::kMsCompatIdDescriptor));
}
void adapter_host_probe_task() {
if (g_mode != AdapterUsbMode::kSwitchProbe ||
!g_probe.should_reboot(now_ms())) {
return;
}
watchdog_hw->scratch[kModeScratchRegister] = kXInputBootMagic;
PROBE_LOG("[HOST PROBE] rebooting once into XInput\n");
watchdog_reboot(0, 0, 10);
}

17
adapter_host_probe.h Normal file
View file

@ -0,0 +1,17 @@
#pragma once
#include <stdint.h>
#include "tusb.h"
enum class AdapterUsbMode : uint8_t {
kSwitchProbe,
kXInput,
};
void adapter_host_probe_init();
AdapterUsbMode adapter_host_probe_mode();
void adapter_host_probe_note_string_descriptor(uint8_t index);
bool adapter_host_probe_vendor_control(uint8_t rhport, uint8_t stage,
tusb_control_request_t const *request);
void adapter_host_probe_task();

View file

@ -0,0 +1,30 @@
#pragma once
#include <stdint.h>
class AdapterHostProbeState {
public:
static constexpr uint32_t kRebootDelayMs = 100;
void note_ms_os_string() { saw_ms_os_string_ = true; }
void note_ms_compat_id_request(uint32_t now_ms) {
if (!saw_ms_os_string_) {
return;
}
confirmed_windows_ = true;
reboot_deadline_ms_ = now_ms + kRebootDelayMs;
}
bool windows_confirmed() const { return confirmed_windows_; }
bool should_reboot(uint32_t now_ms) const {
return confirmed_windows_ &&
static_cast<int32_t>(now_ms - reboot_deadline_ms_) >= 0;
}
private:
uint32_t reboot_deadline_ms_ = 0;
bool saw_ms_os_string_ = false;
bool confirmed_windows_ = false;
};

View file

@ -8,6 +8,10 @@
#else
#include "bluepad32_input_backend.h"
#include "bootsel_pairing_button.h"
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
#include "adapter_host_probe.h"
#include "xinput_feasibility_driver.h"
#endif
#endif
#ifdef SWITCH_PICO_LOG
@ -177,11 +181,26 @@ static void log_usb_state() {
#ifdef SWITCH_PICO_BLUEPAD32
for (uint8_t instance = 0;
instance < BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; ++instance) {
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
const bool ready =
adapter_host_probe_mode() == AdapterUsbMode::kXInput
? xinput_feasibility_is_ready(instance)
: switch_pro_is_ready(instance);
#else
const bool ready = switch_pro_is_ready(instance);
#endif
if (ready != g_last_ready[instance]) {
g_last_ready[instance] = ready;
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
LOG_PRINTF("[%s %u] driver %s\n",
adapter_host_probe_mode() == AdapterUsbMode::kXInput
? "XINPUT"
: "SWITCH",
instance, ready ? "ready" : "not ready");
#else
LOG_PRINTF("[SWITCH %u] driver %s\n", instance,
ready ? "ready (handshake OK)" : "not ready");
#endif
}
}
#else
@ -200,6 +219,9 @@ int main() {
#ifdef SWITCH_PICO_BLUEPAD32
bluepad32_input_backend_init();
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
adapter_host_probe_init();
#endif
#else
init_uart_input();
#endif
@ -208,10 +230,27 @@ int main() {
#ifdef SWITCH_PICO_BLUEPAD32
for (uint8_t instance = 0;
instance < BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; ++instance) {
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
if (adapter_host_probe_mode() == AdapterUsbMode::kXInput) {
xinput_feasibility_init(instance);
xinput_feasibility_set_rumble_callback(
instance, on_rumble_from_switch);
g_user_states[instance] = neutral_input();
xinput_feasibility_set_input(instance,
g_user_states[instance]);
} else {
switch_pro_init(instance);
switch_pro_set_rumble_callback(instance,
on_rumble_from_switch);
g_user_states[instance] = neutral_input();
switch_pro_set_input(instance, g_user_states[instance]);
}
#else
switch_pro_init(instance);
switch_pro_set_rumble_callback(instance, on_rumble_from_switch);
g_user_states[instance] = neutral_input();
switch_pro_set_input(instance, g_user_states[instance]);
#endif
}
#else
switch_pro_init(SWITCH_HID_INSTANCE);
@ -223,7 +262,14 @@ int main() {
#ifdef SWITCH_PICO_BLUEPAD32
bluepad32_input_backend_start();
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
LOG_PRINTF("[BOOT] adapter feasibility mode=%s\n",
adapter_host_probe_mode() == AdapterUsbMode::kXInput
? "XInput"
: "Switch probe");
#else
LOG_PRINTF("[BOOT] switch-pico starting (Bluepad32 wireless @ 115200)\n");
#endif
#else
LOG_PRINTF("[BOOT] switch-pico starting (UART0 log @ 115200)\n");
LOG_PRINTF("[INFO] UART1 pins TX=%d RX=%d baud=%d\n",
@ -232,6 +278,9 @@ int main() {
while (true) {
tud_task(); // USB device tasks
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
adapter_host_probe_task();
#endif
#ifdef SWITCH_PICO_BLUEPAD32
switch (bootsel_pairing_button_task()) {
case BootselPairingButtonEvent::kOpenPairing:
@ -247,10 +296,25 @@ int main() {
instance < BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; ++instance) {
bluepad32_input_backend_snapshot(instance,
&g_user_states[instance]);
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
bool sent = false;
if (adapter_host_probe_mode() == AdapterUsbMode::kXInput) {
xinput_feasibility_set_input(instance,
g_user_states[instance]);
sent = xinput_feasibility_task(instance);
} else {
switch_pro_set_input(instance, g_user_states[instance]);
sent = switch_pro_task(instance);
}
if (sent) {
bluepad32_input_backend_report_sent(instance);
}
#else
switch_pro_set_input(instance, g_user_states[instance]);
if (switch_pro_task(instance)) {
bluepad32_input_backend_report_sent(instance);
}
#endif
}
#else
bool new_data = poll_uart_frames(); // Pull controller state from UART1

View file

@ -8,6 +8,10 @@
#include "pico/rand.h"
#include "pico/time.h"
#include "tusb.h"
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
#include "adapter_host_probe.h"
#include "xinput_feasibility_descriptors.h"
#endif
#ifdef SWITCH_PICO_LOG
#define LOG_PRINTF(...) printf(__VA_ARGS__)
@ -1082,11 +1086,21 @@ uint8_t const* tud_hid_descriptor_report_cb(uint8_t instance) {
}
uint8_t const* tud_descriptor_device_cb(void) {
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
if (adapter_host_probe_mode() == AdapterUsbMode::kXInput) {
return XInputFeasibility::kDeviceDescriptor;
}
#endif
return switch_pro_device_descriptor;
}
uint8_t const* tud_descriptor_configuration_cb(uint8_t index) {
(void)index;
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
if (adapter_host_probe_mode() == AdapterUsbMode::kXInput) {
return XInputFeasibility::kConfigurationDescriptor;
}
#endif
return switch_pro_configuration_descriptor;
}
@ -1114,28 +1128,62 @@ void tud_umount_cb(void) {
static uint16_t desc_str[32];
uint16_t const * tud_descriptor_string_cb(uint8_t index, uint16_t langid) {
uint16_t const* tud_descriptor_string_cb(uint8_t index, uint16_t langid) {
(void)langid;
uint8_t chr_count;
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
adapter_host_probe_note_string_descriptor(index);
if (index == 0xee) {
static constexpr char kSignature[] = "MSFT100";
for (uint8_t i = 0; i < sizeof(kSignature) - 1; ++i) {
desc_str[1 + i] = kSignature[i];
}
desc_str[8] = XInputFeasibility::kMsVendorRequest;
desc_str[0] = static_cast<uint16_t>((0x03 << 8) | 18);
return desc_str;
}
#endif
if ( index == 0 ) {
uint8_t chr_count = 0;
if (index == 0) {
memcpy(&desc_str[1], switch_pro_string_language, 2);
chr_count = 1;
} else {
if ( index >= sizeof(switch_pro_string_descriptors)/sizeof(switch_pro_string_descriptors[0]) ) return nullptr;
const uint8_t* str = nullptr;
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
static const uint8_t kManufacturer[] = "Switch Pico";
static const uint8_t kProduct[] = "XInput Feasibility";
static const uint8_t kSerial[] = "XINPUT-PROTOTYPE";
static const uint8_t* const kXInputStrings[] = {
nullptr, kManufacturer, kProduct, kSerial};
if (adapter_host_probe_mode() == AdapterUsbMode::kXInput) {
if (index >= sizeof(kXInputStrings) /
sizeof(kXInputStrings[0])) {
return nullptr;
}
str = kXInputStrings[index];
} else
#endif
{
if (index >= sizeof(switch_pro_string_descriptors) /
sizeof(switch_pro_string_descriptors[0])) {
return nullptr;
}
str = switch_pro_string_descriptors[index];
}
const uint8_t *str = switch_pro_string_descriptors[index];
chr_count = 0;
while ( str[chr_count] ) chr_count++;
if ( chr_count > 31 ) chr_count = 31;
for(uint8_t i=0; i<chr_count; i++) {
desc_str[1+i] = str[i];
while (str[chr_count] != 0) {
++chr_count;
}
if (chr_count > 31) {
chr_count = 31;
}
for (uint8_t i = 0; i < chr_count; ++i) {
desc_str[1 + i] = str[i];
}
}
desc_str[0] = (uint16_t) ((0x03 << 8 ) | (2*chr_count + 2));
desc_str[0] =
static_cast<uint16_t>((0x03 << 8) | (2 * chr_count + 2));
return desc_str;
}

View file

@ -0,0 +1,39 @@
from __future__ import annotations
import shutil
import subprocess
from pathlib import Path
def host_compiler() -> str:
compiler = shutil.which("c++") or shutil.which("g++")
assert compiler is not None, "a host C++ compiler is required"
return compiler
def test_xinput_feasibility_contracts(tmp_path: Path) -> None:
root = Path(__file__).resolve().parents[1]
compiler = host_compiler()
for instance_count in range(1, 5):
executable = tmp_path / f"xinput_feasibility_{instance_count}_test"
result = subprocess.run(
[
compiler,
"-std=c++17",
"-Wall",
"-Wextra",
"-Werror",
"-pedantic",
f"-DSWITCH_PICO_HID_INSTANCE_COUNT={instance_count}",
f"-I{root}",
str(root / "tests" / "xinput_feasibility_test.cpp"),
"-o",
str(executable),
],
check=False,
cwd=root,
text=True,
capture_output=True,
)
assert result.returncode == 0, result.stderr
_ = subprocess.run([str(executable)], check=True, cwd=root)

View file

@ -0,0 +1,186 @@
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <iostream>
#include "adapter_host_probe_state.h"
#include "xinput_feasibility_descriptors.h"
#include "xinput_feasibility_protocol.h"
namespace {
int failures = 0;
void expect(bool condition, const char *message) {
if (!condition) {
std::cerr << "FAIL: " << message << '\n';
++failures;
}
}
uint16_t read_le16(const uint8_t *data) {
return static_cast<uint16_t>(data[0] | (data[1] << 8));
}
uint32_t read_le32(const uint8_t *data) {
return static_cast<uint32_t>(data[0]) |
(static_cast<uint32_t>(data[1]) << 8) |
(static_cast<uint32_t>(data[2]) << 16) |
(static_cast<uint32_t>(data[3]) << 24);
}
void test_device_and_configuration_descriptors() {
using namespace XInputFeasibility;
expect(read_le16(&kDeviceDescriptor[8]) == kPrototypeVendorId,
"prototype VID mismatch");
expect(read_le16(&kDeviceDescriptor[10]) == kPrototypeProductId,
"prototype PID mismatch");
expect(kPrototypeVendorId != 0x045e,
"prototype must not impersonate Microsoft's VID");
expect(read_le16(&kConfigurationDescriptor[2]) ==
sizeof(kConfigurationDescriptor),
"configuration total length mismatch");
expect(kConfigurationDescriptor[4] == SWITCH_PICO_HID_INSTANCE_COUNT,
"configuration interface count mismatch");
std::array<bool, 16> endpoints{};
for (uint8_t instance = 0; instance < SWITCH_PICO_HID_INSTANCE_COUNT;
++instance) {
const size_t offset = 9 + instance * kInterfaceDescriptorSize;
const uint8_t *interface = &kConfigurationDescriptor[offset];
expect(interface[0] == 9 && interface[1] == 4,
"missing interface descriptor");
expect(interface[2] == instance, "interface number mismatch");
expect(interface[5] == 0xff && interface[6] == 0x5d &&
interface[7] == 0x01,
"XInput interface class tuple mismatch");
expect(interface[9] == 0x10 && interface[10] == 0x21,
"XInput capability descriptor missing");
const uint8_t in_endpoint = interface[27];
const uint8_t out_endpoint = interface[34];
expect(in_endpoint == static_cast<uint8_t>(0x81 + instance),
"input endpoint mismatch");
expect(out_endpoint == static_cast<uint8_t>(0x01 + instance),
"output endpoint mismatch");
expect(interface[15] == in_endpoint && interface[21] == out_endpoint,
"capability descriptor endpoint mismatch");
expect(!endpoints[in_endpoint & 0x0f] &&
!endpoints[out_endpoint & 0x0f],
"endpoint number reused");
endpoints[in_endpoint & 0x0f] = true;
}
}
void test_microsoft_compatible_id_descriptor() {
using namespace XInputFeasibility;
expect(read_le32(kMsCompatIdDescriptor) == sizeof(kMsCompatIdDescriptor),
"Microsoft descriptor total length mismatch");
expect(read_le16(&kMsCompatIdDescriptor[4]) == 0x0100,
"Microsoft descriptor version mismatch");
expect(read_le16(&kMsCompatIdDescriptor[6]) == kMsCompatIdIndex,
"Microsoft descriptor index mismatch");
expect(kMsCompatIdDescriptor[8] == SWITCH_PICO_HID_INSTANCE_COUNT,
"Microsoft function count mismatch");
for (uint8_t instance = 0; instance < SWITCH_PICO_HID_INSTANCE_COUNT;
++instance) {
const uint8_t *function = &kMsCompatIdDescriptor[16 + instance * 24];
expect(function[0] == instance,
"Microsoft descriptor interface mismatch");
expect(std::memcmp(&function[2], "XUSB10", 6) == 0,
"XUSB10 compatible ID missing");
}
expect(read_le32(kProbeMsCompatIdDescriptor) == 16 &&
kProbeMsCompatIdDescriptor[8] == 0,
"probe descriptor must expose no compatible functions");
}
void test_input_report_mapping() {
SwitchInputState state{};
state.lx = state.ly = state.rx = state.ry = 32768;
auto report = XInputFeasibility::build_input_report(state);
expect(report.report_id == 0 && report.report_size == 20,
"neutral report header mismatch");
expect(report.buttons == 0 && report.left_trigger == 0 &&
report.right_trigger == 0,
"neutral report controls mismatch");
expect(report.left_x == 0 && report.left_y == 0 && report.right_x == 0 &&
report.right_y == 0,
"neutral axes mismatch");
state.dpad_up = true;
state.button_b = true;
state.button_a = true;
state.button_y = true;
state.button_x = true;
state.button_plus = true;
state.button_minus = true;
state.button_home = true;
state.button_zl = true;
state.button_zr = true;
state.lx = 0;
state.ly = 0;
state.rx = UINT16_MAX;
state.ry = UINT16_MAX;
report = XInputFeasibility::build_input_report(state);
expect((report.buttons & XInputFeasibility::kDpadUp) != 0,
"D-pad mapping missing");
expect((report.buttons & XInputFeasibility::kButtonA) != 0 &&
(report.buttons & XInputFeasibility::kButtonB) != 0 &&
(report.buttons & XInputFeasibility::kButtonX) != 0 &&
(report.buttons & XInputFeasibility::kButtonY) != 0,
"positional face-button mapping mismatch");
expect(report.left_trigger == 0xff && report.right_trigger == 0xff,
"digital trigger mapping mismatch");
expect(report.left_x == INT16_MIN && report.left_y == INT16_MAX &&
report.right_x == INT16_MAX && report.right_y == -INT16_MAX,
"axis endpoint mapping mismatch");
}
void test_rumble_report() {
const uint8_t packet[8] = {0x00, 0x08, 0x00, 0xa5, 0x5a, 0x00, 0x00, 0x00};
SwitchRumbleOutput output{};
expect(
XInputFeasibility::parse_rumble_report(packet, sizeof(packet), &output),
"valid rumble report rejected");
expect(output.low_frequency_magnitude == 0xa5 &&
output.high_frequency_magnitude == 0x5a,
"rumble magnitudes mapped incorrectly");
expect(!XInputFeasibility::parse_rumble_report(packet, 4, &output),
"truncated rumble report accepted");
uint8_t wrong_type[8]{};
expect(!XInputFeasibility::parse_rumble_report(wrong_type,
sizeof(wrong_type), &output),
"wrong rumble report type accepted");
}
void test_host_probe_sequence() {
AdapterHostProbeState state;
state.note_ms_compat_id_request(10);
expect(!state.windows_confirmed(),
"compatible-ID request without signature confirmed Windows");
state.note_ms_os_string();
state.note_ms_compat_id_request(20);
expect(state.windows_confirmed(),
"two-stage Windows signature not confirmed");
expect(!state.should_reboot(119), "probe rebooted before delay");
expect(state.should_reboot(120), "probe did not reboot at deadline");
AdapterHostProbeState wrapped;
wrapped.note_ms_os_string();
wrapped.note_ms_compat_id_request(UINT32_MAX - 50);
expect(!wrapped.should_reboot(48),
"wrapped timer rebooted before deadline");
expect(wrapped.should_reboot(49), "wrapped timer missed deadline");
}
} // namespace
int main() {
test_device_and_configuration_descriptors();
test_microsoft_compatible_id_descriptor();
test_input_report_mapping();
test_rumble_report();
test_host_probe_sequence();
return failures == 0 ? 0 : 1;
}

View file

@ -3,6 +3,9 @@
#include <string.h>
#include "tusb.h"
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
#include "adapter_host_probe.h"
#endif
namespace UsbPairingManagement {
@ -45,6 +48,11 @@ size_t encode_snapshot(const Bluepad32PairingSnapshot& snapshot,
extern "C" bool tud_vendor_control_xfer_cb(
uint8_t rhport, uint8_t stage,
tusb_control_request_t const* request) {
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
if (adapter_host_probe_vendor_control(rhport, stage, request)) {
return true;
}
#endif
if (stage != CONTROL_STAGE_SETUP) {
return true;
}

View file

@ -0,0 +1,123 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#ifndef SWITCH_PICO_HID_INSTANCE_COUNT
#define SWITCH_PICO_HID_INSTANCE_COUNT 1
#endif
#if SWITCH_PICO_HID_INSTANCE_COUNT < 1 || SWITCH_PICO_HID_INSTANCE_COUNT > 4
#error "SWITCH_PICO_HID_INSTANCE_COUNT must be between 1 and 4"
#endif
namespace XInputFeasibility {
constexpr uint16_t kPrototypeVendorId = 0xcafe;
constexpr uint16_t kPrototypeProductId = 0x4010;
constexpr uint8_t kInterfaceDescriptorSize = 39;
constexpr uint16_t kConfigurationDescriptorSize =
9 + SWITCH_PICO_HID_INSTANCE_COUNT * kInterfaceDescriptorSize;
constexpr uint16_t kMsCompatIdDescriptorSize =
16 + SWITCH_PICO_HID_INSTANCE_COUNT * 24;
constexpr uint8_t kMsVendorRequest = 0x20;
constexpr uint16_t kMsCompatIdIndex = 0x0004;
static const uint8_t kDeviceDescriptor[] = {
0x12,
0x01, // Device descriptor
0x00,
0x02, // USB 2.0
0xff,
0xff,
0xff, // Vendor-specific device
0x40, // Endpoint zero packet size
static_cast<uint8_t>(kPrototypeVendorId & 0xff),
static_cast<uint8_t>(kPrototypeVendorId >> 8),
static_cast<uint8_t>(kPrototypeProductId & 0xff),
static_cast<uint8_t>(kPrototypeProductId >> 8),
0x00,
0x01, // Prototype revision 1.00
0x01,
0x02,
0x03, // Manufacturer, product, serial strings
0x01, // One configuration
};
#define XINPUT_FEASIBILITY_INTERFACE(number, endpoint) \
0x09, 0x04, number, 0x00, 0x02, 0xff, 0x5d, 0x01, 0x00, 0x10, 0x21, 0x10, \
0x01, 0x01, 0x24, static_cast<uint8_t>(0x80 | endpoint), 0x14, 0x03, \
0x00, 0x03, 0x13, endpoint, 0x00, 0x03, 0x00, 0x07, 0x05, \
static_cast<uint8_t>(0x80 | endpoint), 0x03, 0x20, 0x00, 0x04, 0x07, \
0x05, endpoint, 0x03, 0x20, 0x00, 0x08
static const uint8_t kConfigurationDescriptor[] = {
0x09,
0x02,
static_cast<uint8_t>(kConfigurationDescriptorSize & 0xff),
static_cast<uint8_t>(kConfigurationDescriptorSize >> 8),
SWITCH_PICO_HID_INSTANCE_COUNT,
0x01,
0x00,
0x80,
0xfa,
XINPUT_FEASIBILITY_INTERFACE(0x00, 0x01),
#if SWITCH_PICO_HID_INSTANCE_COUNT >= 2
XINPUT_FEASIBILITY_INTERFACE(0x01, 0x02),
#endif
#if SWITCH_PICO_HID_INSTANCE_COUNT >= 3
XINPUT_FEASIBILITY_INTERFACE(0x02, 0x03),
#endif
#if SWITCH_PICO_HID_INSTANCE_COUNT >= 4
XINPUT_FEASIBILITY_INTERFACE(0x03, 0x04),
#endif
};
#undef XINPUT_FEASIBILITY_INTERFACE
#define XINPUT_FEASIBILITY_COMPAT_FUNCTION(number) \
number, 0x01, 'X', 'U', 'S', 'B', '1', '0', 0x00, 0x00, 0x00, 0x00, 0x00, \
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
static const uint8_t kMsCompatIdDescriptor[] = {
static_cast<uint8_t>(kMsCompatIdDescriptorSize & 0xff),
static_cast<uint8_t>(kMsCompatIdDescriptorSize >> 8),
0x00,
0x00,
0x00,
0x01, // Microsoft OS descriptor version 1.0
0x04,
0x00, // Extended compatible ID descriptor
SWITCH_PICO_HID_INSTANCE_COUNT,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
XINPUT_FEASIBILITY_COMPAT_FUNCTION(0x00),
#if SWITCH_PICO_HID_INSTANCE_COUNT >= 2
XINPUT_FEASIBILITY_COMPAT_FUNCTION(0x01),
#endif
#if SWITCH_PICO_HID_INSTANCE_COUNT >= 3
XINPUT_FEASIBILITY_COMPAT_FUNCTION(0x02),
#endif
#if SWITCH_PICO_HID_INSTANCE_COUNT >= 4
XINPUT_FEASIBILITY_COMPAT_FUNCTION(0x03),
#endif
};
#undef XINPUT_FEASIBILITY_COMPAT_FUNCTION
static const uint8_t kProbeMsCompatIdDescriptor[] = {
0x10, 0x00, 0x00, 0x00, 0x00, 0x01, 0x04, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static_assert(sizeof(kDeviceDescriptor) == 18);
static_assert(sizeof(kConfigurationDescriptor) == kConfigurationDescriptorSize);
static_assert(sizeof(kMsCompatIdDescriptor) == kMsCompatIdDescriptorSize);
static_assert(sizeof(kProbeMsCompatIdDescriptor) == 16);
} // namespace XInputFeasibility

View file

@ -0,0 +1,219 @@
#include "xinput_feasibility_driver.h"
#include <stddef.h>
#include <string.h>
#include "device/usbd_pvt.h"
#include "tusb.h"
#include "xinput_feasibility_descriptors.h"
#include "xinput_feasibility_protocol.h"
namespace {
constexpr uint8_t kRhport = 0;
constexpr uint8_t kEndpointBufferSize = 32;
struct XInputContext {
SwitchInputState input{};
XInputFeasibility::InputReport input_report{};
uint8_t output_report[kEndpointBufferSize]{};
SwitchRumbleCallback rumble_callback = nullptr;
uint8_t endpoint_in = 0;
uint8_t endpoint_out = 0;
bool configured = false;
};
XInputContext g_contexts[SWITCH_PICO_HID_INSTANCE_COUNT]{};
XInputContext *context_for(uint8_t instance) {
if (instance >= SWITCH_PICO_HID_INSTANCE_COUNT) {
return nullptr;
}
return &g_contexts[instance];
}
XInputContext *context_for_endpoint(uint8_t endpoint) {
for (XInputContext &context : g_contexts) {
if (context.endpoint_in == endpoint ||
context.endpoint_out == endpoint) {
return &context;
}
}
return nullptr;
}
void reset_context(XInputContext &context) {
const SwitchRumbleCallback callback = context.rumble_callback;
context = {};
context.rumble_callback = callback;
}
void driver_init() {
for (XInputContext &context : g_contexts) {
reset_context(context);
}
}
bool driver_deinit() {
driver_init();
return true;
}
void driver_reset(uint8_t rhport) {
(void)rhport;
driver_init();
}
uint16_t driver_open(uint8_t rhport,
tusb_desc_interface_t const *interface_descriptor,
uint16_t max_length) {
if (interface_descriptor == nullptr ||
interface_descriptor->bInterfaceClass != 0xff ||
interface_descriptor->bInterfaceSubClass != 0x5d ||
interface_descriptor->bInterfaceProtocol != 0x01 ||
interface_descriptor->bInterfaceNumber >=
SWITCH_PICO_HID_INSTANCE_COUNT ||
max_length < XInputFeasibility::kInterfaceDescriptorSize) {
return 0;
}
XInputContext &context = g_contexts[interface_descriptor->bInterfaceNumber];
reset_context(context);
uint16_t consumed = sizeof(tusb_desc_interface_t);
uint8_t const *descriptor = tu_desc_next(interface_descriptor);
uint8_t endpoints_found = 0;
while (consumed < XInputFeasibility::kInterfaceDescriptorSize) {
const uint8_t descriptor_length = descriptor[0];
if (descriptor_length == 0 ||
consumed + descriptor_length >
XInputFeasibility::kInterfaceDescriptorSize) {
reset_context(context);
return 0;
}
if (tu_desc_type(descriptor) == TUSB_DESC_ENDPOINT) {
auto const *endpoint =
reinterpret_cast<tusb_desc_endpoint_t const *>(descriptor);
if (!usbd_edpt_open(rhport, endpoint)) {
reset_context(context);
return 0;
}
if (tu_edpt_dir(endpoint->bEndpointAddress) == TUSB_DIR_IN) {
context.endpoint_in = endpoint->bEndpointAddress;
} else {
context.endpoint_out = endpoint->bEndpointAddress;
}
++endpoints_found;
}
consumed = static_cast<uint16_t>(consumed + descriptor_length);
descriptor = tu_desc_next(descriptor);
}
if (endpoints_found != 2 || context.endpoint_in == 0 ||
context.endpoint_out == 0) {
reset_context(context);
return 0;
}
context.configured = true;
if (!usbd_edpt_xfer(rhport, context.endpoint_out, context.output_report,
sizeof(context.output_report))) {
reset_context(context);
return 0;
}
return consumed;
}
bool driver_control(uint8_t rhport, uint8_t stage,
tusb_control_request_t const *request) {
(void)rhport;
(void)stage;
(void)request;
return false;
}
bool driver_transfer(uint8_t rhport, uint8_t endpoint, xfer_result_t result,
uint32_t transferred) {
XInputContext *context = context_for_endpoint(endpoint);
if (context == nullptr || result != XFER_RESULT_SUCCESS) {
return false;
}
if (endpoint == context->endpoint_out) {
SwitchRumbleOutput rumble{};
if (XInputFeasibility::parse_rumble_report(context->output_report,
transferred, &rumble) &&
context->rumble_callback != nullptr) {
const uint8_t instance = static_cast<uint8_t>(context - g_contexts);
context->rumble_callback(instance, rumble);
}
memset(context->output_report, 0, sizeof(context->output_report));
return usbd_edpt_xfer(rhport, context->endpoint_out,
context->output_report,
sizeof(context->output_report));
}
return true;
}
usbd_class_driver_t const kDriver = {
"XINPUT-FEASIBILITY", driver_init, driver_deinit, driver_reset,
driver_open, driver_control, driver_transfer, nullptr,
};
} // namespace
void xinput_feasibility_init(uint8_t instance) {
XInputContext *context = context_for(instance);
if (context != nullptr) {
reset_context(*context);
}
}
void xinput_feasibility_set_rumble_callback(uint8_t instance,
SwitchRumbleCallback callback) {
XInputContext *context = context_for(instance);
if (context != nullptr) {
context->rumble_callback = callback;
}
}
void xinput_feasibility_set_input(uint8_t instance,
const SwitchInputState &state) {
XInputContext *context = context_for(instance);
if (context != nullptr) {
context->input = state;
}
}
bool xinput_feasibility_task(uint8_t instance) {
XInputContext *context = context_for(instance);
if (context == nullptr || !context->configured || !tud_ready() ||
usbd_edpt_busy(kRhport, context->endpoint_in)) {
return false;
}
context->input_report =
XInputFeasibility::build_input_report(context->input);
if (!usbd_edpt_claim(kRhport, context->endpoint_in)) {
return false;
}
if (!usbd_edpt_xfer(kRhport, context->endpoint_in,
reinterpret_cast<uint8_t *>(&context->input_report),
sizeof(context->input_report))) {
usbd_edpt_release(kRhport, context->endpoint_in);
return false;
}
return true;
}
bool xinput_feasibility_is_ready(uint8_t instance) {
XInputContext *context = context_for(instance);
return context != nullptr && context->configured && tud_ready();
}
extern "C" usbd_class_driver_t const *
usbd_app_driver_get_cb(uint8_t *driver_count) {
if (driver_count == nullptr) {
return nullptr;
}
*driver_count = 1;
return &kDriver;
}

View file

@ -0,0 +1,13 @@
#pragma once
#include <stdint.h>
#include "switch_pro_driver.h"
void xinput_feasibility_init(uint8_t instance);
void xinput_feasibility_set_rumble_callback(uint8_t instance,
SwitchRumbleCallback callback);
void xinput_feasibility_set_input(uint8_t instance,
const SwitchInputState &state);
bool xinput_feasibility_task(uint8_t instance);
bool xinput_feasibility_is_ready(uint8_t instance);

View file

@ -0,0 +1,87 @@
#pragma once
#include <stdint.h>
#include "switch_pro_driver.h"
namespace XInputFeasibility {
constexpr uint16_t kDpadUp = 0x0001;
constexpr uint16_t kDpadDown = 0x0002;
constexpr uint16_t kDpadLeft = 0x0004;
constexpr uint16_t kDpadRight = 0x0008;
constexpr uint16_t kStart = 0x0010;
constexpr uint16_t kBack = 0x0020;
constexpr uint16_t kLeftThumb = 0x0040;
constexpr uint16_t kRightThumb = 0x0080;
constexpr uint16_t kLeftShoulder = 0x0100;
constexpr uint16_t kRightShoulder = 0x0200;
constexpr uint16_t kGuide = 0x0400;
constexpr uint16_t kButtonA = 0x1000;
constexpr uint16_t kButtonB = 0x2000;
constexpr uint16_t kButtonX = 0x4000;
constexpr uint16_t kButtonY = 0x8000;
#pragma pack(push, 1)
struct InputReport {
uint8_t report_id;
uint8_t report_size;
uint16_t buttons;
uint8_t left_trigger;
uint8_t right_trigger;
int16_t left_x;
int16_t left_y;
int16_t right_x;
int16_t right_y;
uint8_t reserved[6];
};
#pragma pack(pop)
static_assert(sizeof(InputReport) == 20);
constexpr int16_t horizontal_axis(uint16_t value) {
return static_cast<int16_t>(static_cast<int32_t>(value) - 32768);
}
constexpr int16_t vertical_axis(uint16_t value) {
const int16_t horizontal = horizontal_axis(value);
return horizontal == INT16_MIN ? INT16_MAX
: static_cast<int16_t>(-horizontal);
}
inline InputReport build_input_report(const SwitchInputState &state) {
InputReport report{};
report.report_size = sizeof(report);
report.buttons =
(state.dpad_up ? kDpadUp : 0) | (state.dpad_down ? kDpadDown : 0) |
(state.dpad_left ? kDpadLeft : 0) |
(state.dpad_right ? kDpadRight : 0) | (state.button_plus ? kStart : 0) |
(state.button_minus ? kBack : 0) | (state.button_l3 ? kLeftThumb : 0) |
(state.button_r3 ? kRightThumb : 0) |
(state.button_l ? kLeftShoulder : 0) |
(state.button_r ? kRightShoulder : 0) |
(state.button_home ? kGuide : 0) |
// Switch labels are positional opposites of XInput labels.
(state.button_b ? kButtonA : 0) | (state.button_a ? kButtonB : 0) |
(state.button_y ? kButtonX : 0) | (state.button_x ? kButtonY : 0);
report.left_trigger = state.button_zl ? 0xff : 0x00;
report.right_trigger = state.button_zr ? 0xff : 0x00;
report.left_x = horizontal_axis(state.lx);
report.left_y = vertical_axis(state.ly);
report.right_x = horizontal_axis(state.rx);
report.right_y = vertical_axis(state.ry);
return report;
}
inline bool parse_rumble_report(const uint8_t *data, uint32_t size,
SwitchRumbleOutput *output) {
if (data == nullptr || output == nullptr || size < 5 || data[0] != 0x00 ||
data[1] != 0x08) {
return false;
}
output->low_frequency_magnitude = data[3];
output->high_frequency_magnitude = data[4];
return true;
}
} // namespace XInputFeasibility