Merge pull request #14237 from JulienBernard3383279/gcc-adapter-hotswap

GC adapter fixes (hotplugging, claim retries, Dolphin shutdown)
This commit is contained in:
Jordan Woyak
2026-03-18 16:56:30 -05:00
committed by GitHub
2 changed files with 83 additions and 48 deletions
+70 -35
View File
@@ -12,8 +12,10 @@
#endif #endif
#include <array> #include <array>
#include <chrono>
#include <mutex> #include <mutex>
#include <optional> #include <optional>
using namespace std::chrono_literals;
#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION #if GCADAPTER_USE_LIBUSB_IMPLEMENTATION
#include <libusb.h> #include <libusb.h>
@@ -137,6 +139,9 @@ static Common::Flag s_adapter_detect_thread_running;
static Common::Event s_hotplug_event; static Common::Event s_hotplug_event;
static Common::Flag s_read_thread_has_init;
static Common::Flag s_adapter_reads_failing;
#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION #if GCADAPTER_USE_LIBUSB_IMPLEMENTATION
static std::function<void(void)> s_detect_callback; static std::function<void(void)> s_detect_callback;
@@ -197,6 +202,11 @@ static void ReadThreadFunc()
s_write_adapter_thread_running.Set(true); s_write_adapter_thread_running.Set(true);
s_write_adapter_thread = std::thread(WriteThreadFunc); s_write_adapter_thread = std::thread(WriteThreadFunc);
std::chrono::steady_clock::time_point last_successful_read_time =
std::chrono::steady_clock::now();
s_adapter_reads_failing.Clear();
s_read_thread_has_init.Set();
// Reset rumble once on initial reading // Reset rumble once on initial reading
ResetRumble(); ResetRumble();
@@ -205,34 +215,47 @@ static void ReadThreadFunc()
auto poll_rate_measurement_start_time = Clock::now(); auto poll_rate_measurement_start_time = Clock::now();
int poll_rate_measurement_count = 0; int poll_rate_measurement_count = 0;
while (s_read_adapter_thread_running.IsSet()) bool last_read_failed = false;
while (s_read_adapter_thread_running.IsSet() && !s_adapter_reads_failing.IsSet())
{ {
#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION #if GCADAPTER_USE_LIBUSB_IMPLEMENTATION
std::array<u8, CONTROLLER_INPUT_PAYLOAD_EXPECTED_SIZE> input_buffer; std::array<u8, CONTROLLER_INPUT_PAYLOAD_EXPECTED_SIZE> input_buffer;
int payload_size = 0; int payload_size = 0;
int error = libusb_interrupt_transfer(s_handle, s_endpoint_in, input_buffer.data(), int transfer_return_code =
int(input_buffer.size()), &payload_size, USB_TIMEOUT_MS); libusb_interrupt_transfer(s_handle, s_endpoint_in, input_buffer.data(),
if (error != LIBUSB_SUCCESS) int(input_buffer.size()), &payload_size, USB_TIMEOUT_MS);
if (transfer_return_code == LIBUSB_SUCCESS)
{ {
ERROR_LOG_FMT(CONTROLLERINTERFACE, "Read: libusb_interrupt_transfer failed: {}", ProcessInputPayload(input_buffer.data(), payload_size);
LibusbUtils::ErrorWrap(error)); last_successful_read_time = std::chrono::steady_clock::now();
last_read_failed = false;
} }
if (error == LIBUSB_ERROR_IO) else
{ {
// s_read_adapter_thread_running is cleared by the joiner, not the stopper. if (last_read_failed)
{
INFO_LOG_FMT(CONTROLLERINTERFACE, "Read: libusb_interrupt_transfer failed: {}",
LibusbUtils::ErrorWrap(transfer_return_code));
// Reset the device, which may trigger a replug. // Prevents busy-looping when transfers return instantly e.g. on disconnect
error = libusb_reset_device(s_handle); Common::SleepCurrentThread(1);
ERROR_LOG_FMT(CONTROLLERINTERFACE, "Read: libusb_reset_device: {}", }
LibusbUtils::ErrorWrap(error)); else
{
ERROR_LOG_FMT(CONTROLLERINTERFACE, "Read: libusb_interrupt_transfer newly failed: {}",
LibusbUtils::ErrorWrap(transfer_return_code));
}
last_read_failed = true;
// If error is nonzero, try fixing it next loop iteration. We can't easily return if (std::chrono::steady_clock::now() - last_successful_read_time > 500ms)
// and cleanup program state without getting another thread to call Reset(). {
ERROR_LOG_FMT(CONTROLLERINTERFACE, "Read: continuously failing transfers, resetting.");
s_adapter_reads_failing.Set();
}
} }
ProcessInputPayload(input_buffer.data(), payload_size);
#elif GCADAPTER_USE_ANDROID_IMPLEMENTATION #elif GCADAPTER_USE_ANDROID_IMPLEMENTATION
const int payload_size = env->CallStaticIntMethod(s_adapter_class, input_func); const int payload_size = env->CallStaticIntMethod(s_adapter_class, input_func);
jbyte* const java_data = env->GetByteArrayElements(*java_controller_payload, nullptr); jbyte* const java_data = env->GetByteArrayElements(*java_controller_payload, nullptr);
@@ -260,8 +283,6 @@ static void ReadThreadFunc()
poll_rate_measurement_start_time = now; poll_rate_measurement_start_time = now;
poll_rate_measurement_count = 0; poll_rate_measurement_count = 0;
} }
Common::YieldCPU();
} }
// Terminate the write thread on leaving // Terminate the write thread on leaving
@@ -387,6 +408,8 @@ static void ScanThreadFunc()
#if LIBUSB_API_HAS_HOTPLUG #if LIBUSB_API_HAS_HOTPLUG
#ifndef __FreeBSD__ #ifndef __FreeBSD__
s_libusb_hotplug_enabled = libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG) != 0; s_libusb_hotplug_enabled = libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG) != 0;
// As of v1.0.23-rc1 (2026/02) libusb has hotplug event capability on Linux & MacOS, but not on
// Windows
#endif #endif
if (s_libusb_hotplug_enabled) if (s_libusb_hotplug_enabled)
{ {
@@ -418,9 +441,12 @@ static void ScanThreadFunc()
} }
if (s_libusb_hotplug_enabled) if (s_libusb_hotplug_enabled)
s_hotplug_event.Wait(); s_hotplug_event.WaitFor(1000ms);
else else
Common::SleepCurrentThread(500); Common::SleepCurrentThread(500);
if (s_read_thread_has_init.IsSet() && s_adapter_reads_failing.IsSet())
Reset();
} }
#elif GCADAPTER_USE_ANDROID_IMPLEMENTATION #elif GCADAPTER_USE_ANDROID_IMPLEMENTATION
JNIEnv* const env = IDCache::GetEnvForThread(); JNIEnv* const env = IDCache::GetEnvForThread();
@@ -588,8 +614,13 @@ static bool CheckDeviceAccess(libusb_device* device)
return false; return false;
} }
NOTICE_LOG_FMT(CONTROLLERINTERFACE, "Found GC Adapter with Vendor: {:X} Product: {:X} Devnum: {}", const u8 bus = libusb_get_bus_number(device);
desc.idVendor, desc.idProduct, 1); const u8 port = libusb_get_device_address(device);
NOTICE_LOG_FMT(CONTROLLERINTERFACE,
"Found GC Adapter [Bus:{:03d}, Address:{:03d}, VID:"
"{:04X}, PID:{:04X}]",
bus, port, desc.idVendor, desc.idProduct);
// In case of failure, capture the libusb error code into the adapter status // In case of failure, capture the libusb error code into the adapter status
Common::ScopeGuard status_guard([&ret] { Common::ScopeGuard status_guard([&ret] {
@@ -597,20 +628,13 @@ static bool CheckDeviceAccess(libusb_device* device)
s_status = AdapterStatus::Error; s_status = AdapterStatus::Error;
}); });
const u8 bus = libusb_get_bus_number(device);
const u8 port = libusb_get_device_address(device);
ret = libusb_open(device, &s_handle); ret = libusb_open(device, &s_handle);
if (ret != LIBUSB_SUCCESS) if (ret != LIBUSB_SUCCESS)
{ {
if (ret == LIBUSB_ERROR_ACCESS) ERROR_LOG_FMT(CONTROLLERINTERFACE,
{ "libusb_open failed to open GC Adapter [Bus:{:03d}, Address:{:03d}, VID:"
ERROR_LOG_FMT(CONTROLLERINTERFACE, "{:04X}, PID:{:04X}] with error {}",
"Dolphin does not have access to this device: Bus {:03d} Device {:03d}: ID " bus, port, desc.idVendor, desc.idProduct, LibusbUtils::ErrorWrap(ret));
"{:04X}:{:04X}.",
bus, port, desc.idVendor, desc.idProduct);
}
ERROR_LOG_FMT(CONTROLLERINTERFACE, "libusb_open failed to open device: {}",
LibusbUtils::ErrorWrap(ret));
return false; return false;
} }
@@ -633,9 +657,18 @@ static bool CheckDeviceAccess(libusb_device* device)
} }
else if (ret != 0) // 0: kernel driver is not active, but otherwise no error. else if (ret != 0) // 0: kernel driver is not active, but otherwise no error.
{ {
// Neither 0 nor 1 means an error occurred. // Neither 0 nor 1 means an error occured.
ERROR_LOG_FMT(CONTROLLERINTERFACE, "libusb_kernel_driver_active failed: {}", if (ret == LIBUSB_ERROR_NOT_SUPPORTED)
LibusbUtils::ErrorWrap(ret)); {
// Expected outside Linux
INFO_LOG_FMT(CONTROLLERINTERFACE, "libusb_kernel_driver_active failed: {}",
LibusbUtils::ErrorWrap(ret));
}
else
{
ERROR_LOG_FMT(CONTROLLERINTERFACE, "libusb_kernel_driver_active failed: {}",
LibusbUtils::ErrorWrap(ret));
}
} }
// This call makes Nyko-brand (and perhaps other) adapters work. // This call makes Nyko-brand (and perhaps other) adapters work.
@@ -763,6 +796,8 @@ static void Reset()
s_read_adapter_thread.join(); s_read_adapter_thread.join();
// The read thread will close the write thread // The read thread will close the write thread
s_read_thread_has_init.Clear();
s_port_states.fill({}); s_port_states.fill({});
#if GCADAPTER_USE_LIBUSB_IMPLEMENTATION #if GCADAPTER_USE_LIBUSB_IMPLEMENTATION
+13 -13
View File
@@ -37,23 +37,23 @@ enum SwitchButton
struct GCPadStatus struct GCPadStatus
{ {
u16 button = 0; // Or-ed PAD_BUTTON_* and PAD_TRIGGER_* bits
u8 stickX = 0; // 0 <= stickX <= 255
u8 stickY = 0; // 0 <= stickY <= 255
u8 substickX = 0; // 0 <= substickX <= 255
u8 substickY = 0; // 0 <= substickY <= 255
u8 triggerLeft = 0; // 0 <= triggerLeft <= 255
u8 triggerRight = 0; // 0 <= triggerRight <= 255
u8 analogA = 0; // 0 <= analogA <= 255
u8 analogB = 0; // 0 <= analogB <= 255
// Triforce
u8 switches = 0;
bool isConnected = true;
static const u8 MAIN_STICK_CENTER_X = 0x80; static const u8 MAIN_STICK_CENTER_X = 0x80;
static const u8 MAIN_STICK_CENTER_Y = 0x80; static const u8 MAIN_STICK_CENTER_Y = 0x80;
static const u8 MAIN_STICK_RADIUS = 0x7f; static const u8 MAIN_STICK_RADIUS = 0x7f;
static const u8 C_STICK_CENTER_X = 0x80; static const u8 C_STICK_CENTER_X = 0x80;
static const u8 C_STICK_CENTER_Y = 0x80; static const u8 C_STICK_CENTER_Y = 0x80;
static const u8 C_STICK_RADIUS = 0x7f; static const u8 C_STICK_RADIUS = 0x7f;
u16 button = 0; // Or-ed PAD_BUTTON_* and PAD_TRIGGER_* bits
u8 stickX = MAIN_STICK_CENTER_X; // 0 <= stickX <= 255
u8 stickY = MAIN_STICK_CENTER_Y; // 0 <= stickY <= 255
u8 substickX = C_STICK_CENTER_X; // 0 <= substickX <= 255
u8 substickY = C_STICK_CENTER_Y; // 0 <= substickY <= 255
u8 triggerLeft = 0; // 0 <= triggerLeft <= 255
u8 triggerRight = 0; // 0 <= triggerRight <= 255
u8 analogA = 0; // 0 <= analogA <= 255
u8 analogB = 0; // 0 <= analogB <= 255
// Triforce
u8 switches = 0;
bool isConnected = true;
}; };