diff --git a/.gitignore b/.gitignore index 1c69c0a..96b8348 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,9 @@ Code.pul # Build output /build/ /build-*/ +/.build-output/ +/.build-test/ +/native-build/ /dist/ /out/ [Bb]in/ diff --git a/runtime/cmake/PublicProducts.cmake b/runtime/cmake/PublicProducts.cmake index db26440..111d354 100644 --- a/runtime/cmake/PublicProducts.cmake +++ b/runtime/cmake/PublicProducts.cmake @@ -205,7 +205,7 @@ function(mkw_configure_product target) endif() target_link_libraries(${target} PRIVATE - dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp) + dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp setupapi winusb) set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE) foreach(runtime_dll libc++.dll libunwind.dll) diff --git a/runtime/include/wup028_adapter.h b/runtime/include/wup028_adapter.h new file mode 100644 index 0000000..d0b0a4e --- /dev/null +++ b/runtime/include/wup028_adapter.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include + +struct PADStatus; + +namespace Wup028Adapter { + +enum class ConnectionState : uint8_t { + Searching, + DriverError, + Connected, +}; + +struct AdapterInfo { + ConnectionState state = ConnectionState::Searching; + std::string deviceName; + std::string detail; + float pollRateHz = 0.0f; + uint8_t inputEndpoint = 0; + uint8_t outputEndpoint = 0; + std::array ports{}; + // Raw adapter type/status byte. High nibble 1 is wired, 2 is wireless. + std::array portStatus{}; + std::array portChangeSequence{}; +}; + +// Starts the project-owned WUP-028 worker. Safe to call more than once. +void Initialize(); +void Shutdown(); + +// Returns true while an official adapter is open. Each entry is a native +// GameCube port; an empty port is represented by PAD_ERR_NO_CONTROLLER. +bool Read(std::array& statuses); +// Returns true when the command belongs to an active WUP-028, allowing the +// caller to avoid also sending it to Aurora's unrelated controller backend. +bool SetRumble(uint32_t port, bool enabled); +AdapterInfo GetInfo(); + +} // namespace Wup028Adapter diff --git a/runtime/src/hle/input/pad.cpp b/runtime/src/hle/input/pad.cpp index e3a0539..658a622 100644 --- a/runtime/src/hle/input/pad.cpp +++ b/runtime/src/hle/input/pad.cpp @@ -1,6 +1,7 @@ #include "hle_stubs.h" #include "memory.h" #include "hle/controller_status_contract.h" +#include "wup028_adapter.h" #include #include @@ -33,6 +34,7 @@ void WritePadStatus(uint32_t base, const PADStatus& status) { extern "C" uint32_t PAD__Init_HLE() { + Wup028Adapter::Initialize(); return PADInit() ? 1u : 0u; } PPC_NATIVE_OVERRIDE(801AF2F0, PAD__Init_HLE, uint32_t, (), ()); @@ -44,7 +46,18 @@ extern "C" uint32_t PAD__Read_HLE(uint32_t statusPtr) } PADStatus statuses[PAD_CHANMAX]{}; - const uint32_t rumbleMask = PADRead(statuses); + std::array adapterStatuses{}; + uint32_t rumbleMask = 0; + if (Wup028Adapter::Read(adapterStatuses)) { + std::copy(adapterStatuses.begin(), adapterStatuses.end(), statuses); + for (uint32_t port = 0; port < PAD_CHANMAX; ++port) { + if (statuses[port].err == PAD_ERR_NONE) { + rumbleMask |= PAD_CHAN0_BIT >> port; + } + } + } else { + rumbleMask = PADRead(statuses); + } try { for (uint32_t i = 0; i < PAD_CHANMAX; ++i) { @@ -73,6 +86,8 @@ PPC_NATIVE_OVERRIDE(801AF1E4, PAD__Recalibrate_HLE, uint32_t, (uint32_t mask), ( extern "C" void PAD__ControlMotor_HLE(int32_t chan, uint32_t command) { - PADControlMotor(chan, command); + if (!Wup028Adapter::SetRumble(static_cast(chan), command == PAD_MOTOR_RUMBLE)) { + PADControlMotor(chan, command); + } } PPC_NATIVE_OVERRIDE_VOID(801AF908, PAD__ControlMotor_HLE, (int32_t chan, uint32_t command), (chan, command)); diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 7050948..0c254dc 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -47,6 +47,7 @@ #include "system_bridge.h" #include "ppc_runtime.h" #include "aurora_events.h" +#include "wup028_adapter.h" #include "fiber_manager.h" #include "hle_stubs.h" #include "runtime_config.h" @@ -1252,6 +1253,7 @@ int RuntimeMain(int argc, char** argv) { } aurora_set_frame_worker_wait_callback(ServiceGuestTimingDuringAuroraFrameWait); GxGuestWrite::InstallAuroraHooks(); + Wup028Adapter::Initialize(); UpdateMkwDynamicAspectSurface(auroraInfo.windowSize.native_fb_width, auroraInfo.windowSize.native_fb_height); settings_overlay::InitializeRuntimeSettings(); @@ -1293,6 +1295,7 @@ int RuntimeMain(int argc, char** argv) { // Shutdown fiber system Fiber::GuestFiberManager::Shutdown(); WindowPlacementPersistence::Flush(true); + Wup028Adapter::Shutdown(); aurora_shutdown(); SetRuntimeExitCodeImpl(0); ShutdownProcessTranscript(); @@ -1310,6 +1313,7 @@ int RuntimeMain(int argc, char** argv) { SetRuntimeExitCodeImpl(1); Fiber::GuestFiberManager::Shutdown(); WindowPlacementPersistence::Flush(true); + Wup028Adapter::Shutdown(); aurora_shutdown(); ShutdownProcessTranscript(); return 1; @@ -1321,6 +1325,7 @@ int RuntimeMain(int argc, char** argv) { SetRuntimeExitCodeImpl(1); Fiber::GuestFiberManager::Shutdown(); WindowPlacementPersistence::Flush(true); + Wup028Adapter::Shutdown(); aurora_shutdown(); ShutdownProcessTranscript(); return 1; diff --git a/runtime/src/settings_overlay.cpp b/runtime/src/settings_overlay.cpp index b269604..c66bc4c 100644 --- a/runtime/src/settings_overlay.cpp +++ b/runtime/src/settings_overlay.cpp @@ -1,4 +1,5 @@ #include "settings_overlay.h" +#include "wup028_adapter.h" #include "audio_backend.h" #include "game_graphics_options.h" #include "music_attenuation.h" @@ -319,6 +320,30 @@ void DrawControllerSettings() { } } + ImGui::Separator(); + if (ImGui::BeginMenu("GameCube controller adapter")) { + const auto adapter = Wup028Adapter::GetInfo(); + const char* state = adapter.state == Wup028Adapter::ConnectionState::Connected + ? "Connected" + : adapter.state == Wup028Adapter::ConnectionState::DriverError ? "Driver error" + : "Searching"; + ImGui::Text("Status: %s", state); + if (!adapter.deviceName.empty()) ImGui::Text("Device: %s", adapter.deviceName.c_str()); + ImGui::TextWrapped("%s", adapter.detail.c_str()); + if (adapter.state == Wup028Adapter::ConnectionState::Connected) { + ImGui::Text("Poll rate: %.1f reports/s", adapter.pollRateHz); + ImGui::Text("Endpoints: IN 0x%02X, OUT 0x%02X", adapter.inputEndpoint, adapter.outputEndpoint); + for (size_t port = 0; port < adapter.ports.size(); ++port) { + const uint8_t type = adapter.portStatus[port] & 0x30; + const char* typeName = type == 0x10 ? "wired" : type == 0x20 ? "wireless" : "none"; + ImGui::Text("Adapter port %u: %s (type %s, raw 0x%02X)", + static_cast(port + 1), + adapter.ports[port] ? "Controller connected" : "Empty", typeName, + adapter.portStatus[port]); + } + } + ImGui::EndMenu(); + } ImGui::Separator(); const uint32_t controllerCount = PADCount(); if (controllerCount == 0) { diff --git a/runtime/src/wup028_adapter.cpp b/runtime/src/wup028_adapter.cpp new file mode 100644 index 0000000..c6babba --- /dev/null +++ b/runtime/src/wup028_adapter.cpp @@ -0,0 +1,399 @@ +#include "wup028_adapter.h" + +#include "runtime_log.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Wup028Adapter { +namespace { + +constexpr uint16_t kNintendoVendor = 0x057e; +constexpr uint16_t kAdapterProduct = 0x0337; +constexpr size_t kReportSize = 37; + +std::mutex g_mutex; +std::array g_statuses{}; +std::array g_rumble{}; +std::thread g_worker; +std::atomic_bool g_stop{false}; +std::atomic_bool g_running{false}; +std::atomic_bool g_connected{false}; +AdapterInfo g_info; + +struct Device { + HANDLE file = INVALID_HANDLE_VALUE; + HANDLE event = nullptr; + WINUSB_INTERFACE_HANDLE usb = nullptr; + UCHAR inputPipe = 0; + UCHAR outputPipe = 0; + + ~Device() { Close(); } + void Close() { + if (usb != nullptr) WinUsb_Free(usb); + if (event != nullptr) CloseHandle(event); + if (file != INVALID_HANDLE_VALUE) CloseHandle(file); + usb = nullptr; + event = nullptr; + file = INVALID_HANDLE_VALUE; + } +}; + +bool Transfer(Device& device, bool input, UCHAR pipe, UCHAR* data, ULONG size, ULONG& transferred, + DWORD timeoutMs, bool* timedOut = nullptr) { + if (timedOut != nullptr) *timedOut = false; + ResetEvent(device.event); + OVERLAPPED operation{}; + operation.hEvent = device.event; + const BOOL started = input ? WinUsb_ReadPipe(device.usb, pipe, data, size, &transferred, &operation) + : WinUsb_WritePipe(device.usb, pipe, data, size, &transferred, &operation); + if (started) return true; + if (GetLastError() != ERROR_IO_PENDING) return false; + const DWORD wait = WaitForSingleObject(device.event, timeoutMs); + if (wait == WAIT_OBJECT_0) { + return WinUsb_GetOverlappedResult(device.usb, &operation, &transferred, FALSE); + } + CancelIoEx(device.file, &operation); + WaitForSingleObject(device.event, INFINITE); + WinUsb_GetOverlappedResult(device.usb, &operation, &transferred, FALSE); + if (timedOut != nullptr && wait == WAIT_TIMEOUT) *timedOut = true; + return false; +} + +struct DeviceMatch { + std::wstring path; + std::string name; +}; + +std::string WideToUtf8(const wchar_t* value) { + if (value == nullptr || *value == L'\0') return {}; + const int size = WideCharToMultiByte(CP_UTF8, 0, value, -1, nullptr, 0, nullptr, nullptr); + if (size <= 1) return {}; + std::string result(static_cast(size), '\0'); + WideCharToMultiByte(CP_UTF8, 0, value, -1, result.data(), size, nullptr, nullptr); + result.resize(static_cast(size - 1)); + return result; +} + +DeviceMatch FindAdapter() { + HDEVINFO devices = SetupDiGetClassDevsW(&GUID_DEVINTERFACE_USB_DEVICE, nullptr, nullptr, + DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); + if (devices == INVALID_HANDLE_VALUE) return {}; + + DeviceMatch result; + for (DWORD index = 0;; ++index) { + SP_DEVICE_INTERFACE_DATA iface{sizeof(iface)}; + if (!SetupDiEnumDeviceInterfaces(devices, nullptr, &GUID_DEVINTERFACE_USB_DEVICE, index, &iface)) break; + + DWORD required = 0; + SetupDiGetDeviceInterfaceDetailW(devices, &iface, nullptr, 0, &required, nullptr); + if (required < sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W)) continue; + std::vector storage(required); + auto* detail = reinterpret_cast(storage.data()); + detail->cbSize = sizeof(*detail); + SP_DEVINFO_DATA deviceInfo{sizeof(deviceInfo)}; + if (!SetupDiGetDeviceInterfaceDetailW(devices, &iface, detail, required, nullptr, &deviceInfo)) continue; + + std::wstring lower(detail->DevicePath); + std::transform(lower.begin(), lower.end(), lower.begin(), + [](wchar_t c) { return static_cast(std::towlower(c)); }); + if (lower.find(L"vid_057e") != std::wstring::npos && lower.find(L"pid_0337") != std::wstring::npos) { + result.path = detail->DevicePath; + std::array description{}; + if (SetupDiGetDeviceRegistryPropertyW(devices, &deviceInfo, SPDRP_FRIENDLYNAME, nullptr, + reinterpret_cast(description.data()), + static_cast(description.size() * sizeof(wchar_t)), nullptr) || + SetupDiGetDeviceRegistryPropertyW(devices, &deviceInfo, SPDRP_DEVICEDESC, nullptr, + reinterpret_cast(description.data()), + static_cast(description.size() * sizeof(wchar_t)), nullptr)) { + result.name = WideToUtf8(description.data()); + } + break; + } + } + SetupDiDestroyDeviceInfoList(devices); + return result; +} + +bool Open(Device& device, std::string& name, std::string& error) { + const auto match = FindAdapter(); + if (match.path.empty()) { + error = "No VID 057E / PID 0337 adapter is present"; + return false; + } + name = match.name.empty() ? "WUP-028-compatible adapter" : match.name; + device.file = CreateFileW(match.path.c_str(), GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, nullptr); + if (device.file == INVALID_HANDLE_VALUE || !WinUsb_Initialize(device.file, &device.usb)) { + error = "WinUSB could not open the adapter (Windows error " + std::to_string(GetLastError()) + ")"; + device.Close(); + return false; + } + device.event = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (device.event == nullptr) { + error = "Could not create the WinUSB transfer event"; + device.Close(); + return false; + } + + USB_INTERFACE_DESCRIPTOR descriptor{}; + if (!WinUsb_QueryInterfaceSettings(device.usb, 0, &descriptor)) { + error = "WinUSB could not query the adapter interface"; + device.Close(); + return false; + } + for (UCHAR index = 0; index < descriptor.bNumEndpoints; ++index) { + WINUSB_PIPE_INFORMATION pipe{}; + if (!WinUsb_QueryPipe(device.usb, 0, index, &pipe)) continue; + if ((pipe.PipeType == UsbdPipeTypeInterrupt || pipe.PipeType == UsbdPipeTypeBulk) && + USB_ENDPOINT_DIRECTION_IN(pipe.PipeId) && device.inputPipe == 0) { + device.inputPipe = pipe.PipeId; + } else if ((pipe.PipeType == UsbdPipeTypeInterrupt || pipe.PipeType == UsbdPipeTypeBulk) && + USB_ENDPOINT_DIRECTION_OUT(pipe.PipeId) && device.outputPipe == 0) { + device.outputPipe = pipe.PipeId; + } + } + if (device.inputPipe == 0 || device.outputPipe == 0) { + error = "The adapter has no usable input/output endpoint"; + device.Close(); + return false; + } + + UCHAR command = 0x13; // Enable the adapter's 37-byte input stream. + ULONG written = 0; + if (!Transfer(device, false, device.outputPipe, &command, 1, written, 1000) || written != 1) { + error = "The adapter rejected input initialization on endpoint 0x"; + const char hex[] = "0123456789ABCDEF"; + error += hex[device.outputPipe >> 4]; + error += hex[device.outputPipe & 15]; + device.Close(); + return false; + } + return true; +} + +int8_t Axis(uint8_t raw) { + return static_cast(std::clamp(static_cast(raw) - 128, -128, 127)); +} + +PADStatus DecodePort(const uint8_t* p) { + PADStatus out{}; + // The WUP-028 protocol uses type 1 for wired pads and type 2 for + // WaveBird/wireless receivers. Testing only bit 0x10 drops every type-2 + // controller and makes otherwise valid ports appear empty. + if ((p[0] & 0x30) == 0) { + out.err = PAD_ERR_NO_CONTROLLER; + return out; + } + if (p[1] & 0x01) out.button |= PAD_BUTTON_A; + if (p[1] & 0x02) out.button |= PAD_BUTTON_B; + if (p[1] & 0x04) out.button |= PAD_BUTTON_X; + if (p[1] & 0x08) out.button |= PAD_BUTTON_Y; + if (p[1] & 0x10) out.button |= PAD_BUTTON_LEFT; + if (p[1] & 0x20) out.button |= PAD_BUTTON_RIGHT; + if (p[1] & 0x40) out.button |= PAD_BUTTON_DOWN; + if (p[1] & 0x80) out.button |= PAD_BUTTON_UP; + if (p[2] & 0x01) out.button |= PAD_BUTTON_START; + if (p[2] & 0x02) out.button |= PAD_TRIGGER_Z; + if (p[2] & 0x04) out.button |= PAD_TRIGGER_R; + if (p[2] & 0x08) out.button |= PAD_TRIGGER_L; + out.stickX = Axis(p[3]); + out.stickY = Axis(p[4]); + out.substickX = Axis(p[5]); + out.substickY = Axis(p[6]); + out.triggerL = p[7]; + out.triggerR = p[8]; + out.err = PAD_ERR_NONE; + return out; +} + +bool SendRumble(Device& device, const std::array& motors) { + std::array report{{0x11, motors[0], motors[1], motors[2], motors[3]}}; + ULONG written = 0; + return Transfer(device, false, device.outputPipe, report.data(), report.size(), written, 1000) && + written == report.size(); +} + +bool RefreshInputStream(Device& device) { + UCHAR command = 0x13; + ULONG written = 0; + return Transfer(device, false, device.outputPipe, &command, 1, written, 1000) && written == 1; +} + +void ClearConnectedPorts(const char* reason) { + std::lock_guard lock(g_mutex); + for (size_t port = 0; port < g_info.ports.size(); ++port) { + if (g_info.ports[port]) { + g_info.ports[port] = false; + ++g_info.portChangeSequence[port]; + RT_LOG(RT_TAG_RUNTIME) << "GameCube adapter port " << (port + 1) + << " controller disconnected (" << reason << ")" << std::endl; + } + g_statuses[port] = {}; + g_statuses[port].err = PAD_ERR_NO_CONTROLLER; + g_info.portStatus[port] = 0; + } +} + +void Worker() { + std::string lastError; + while (!g_stop.load(std::memory_order_acquire)) { + Device device; + std::string name; + std::string error; + if (!Open(device, name, error)) { + g_connected.store(false, std::memory_order_release); + { + std::lock_guard lock(g_mutex); + g_info.state = error.starts_with("No VID") ? ConnectionState::Searching : ConnectionState::DriverError; + g_info.deviceName = name; + g_info.detail = error; + g_info.pollRateHz = 0.0f; + g_info.inputEndpoint = 0; + g_info.outputEndpoint = 0; + g_info.ports.fill(false); + g_info.portStatus.fill(0); + } + if (error != lastError && !error.starts_with("No VID")) { + RT_LOG(RT_TAG_RUNTIME) << "GameCube adapter: " << error << std::endl; + } + lastError = error; + std::this_thread::sleep_for(std::chrono::seconds(1)); + continue; + } + RT_LOG(RT_TAG_RUNTIME) << name << " connected (input endpoint 0x" << std::hex + << static_cast(device.inputPipe) << ", output endpoint 0x" + << static_cast(device.outputPipe) << std::dec << ")" << std::endl; + lastError.clear(); + g_connected.store(true, std::memory_order_release); + { + std::lock_guard lock(g_mutex); + g_info.state = ConnectionState::Connected; + g_info.deviceName = name; + g_info.detail = "Receiving native GameCube reports"; + g_info.inputEndpoint = device.inputPipe; + g_info.outputEndpoint = device.outputPipe; + } + std::array sentRumble{}; + auto rateStart = std::chrono::steady_clock::now(); + uint32_t rateReports = 0; + std::array reportedPorts{}; + + while (!g_stop.load(std::memory_order_acquire)) { + std::array report{}; + ULONG read = 0; + bool timedOut = false; + if (!Transfer(device, true, device.inputPipe, report.data(), report.size(), read, 100, &timedOut)) { + if (timedOut) continue; + break; + } + if (read != report.size() || report[0] != 0x21) continue; + ++rateReports; + std::array decoded{}; + for (size_t port = 0; port < decoded.size(); ++port) decoded[port] = DecodePort(report.data() + 1 + port * 9); + std::array desired{}; + std::array transitions{}; + bool refreshStream = false; + { + std::lock_guard lock(g_mutex); + g_statuses = decoded; + desired = g_rumble; + for (size_t port = 0; port < decoded.size(); ++port) { + g_info.portStatus[port] = report[1 + port * 9]; + const bool present = decoded[port].err == PAD_ERR_NONE; + if (present != reportedPorts[port]) { + reportedPorts[port] = present; + g_info.ports[port] = present; + ++g_info.portChangeSequence[port]; + transitions[port] = present ? 1 : -1; + } + } + const auto now = std::chrono::steady_clock::now(); + const float seconds = std::chrono::duration(now - rateStart).count(); + if (seconds >= 1.0f) { + g_info.pollRateHz = static_cast(rateReports) / seconds; + rateReports = 0; + rateStart = now; + refreshStream = true; + } + } + for (size_t port = 0; port < transitions.size(); ++port) { + if (transitions[port] != 0) { + RT_LOG(RT_TAG_RUNTIME) << "GameCube adapter port " << (port + 1) << " controller " + << (transitions[port] > 0 ? "connected" : "disconnected") << std::endl; + } + } + if (desired != sentRumble) { + if (!SendRumble(device, desired)) break; + sentRumble = desired; + } + if (refreshStream && !RefreshInputStream(device)) break; + } + g_connected.store(false, std::memory_order_release); + ClearConnectedPorts("adapter unavailable"); + { + std::lock_guard lock(g_mutex); + g_info.state = ConnectionState::Searching; + g_info.detail = "Adapter disconnected; waiting for reconnect"; + g_info.pollRateHz = 0.0f; + } + if (!g_stop.load(std::memory_order_acquire)) { + RT_LOG(RT_TAG_RUNTIME) << "WUP-028 GameCube adapter disconnected; waiting for reconnect" << std::endl; + } + } +} + +} // namespace + +void Initialize() { + bool expected = false; + if (!g_running.compare_exchange_strong(expected, true)) return; + for (auto& status : g_statuses) status.err = PAD_ERR_NO_CONTROLLER; + g_stop.store(false, std::memory_order_release); + g_worker = std::thread(Worker); +} + +void Shutdown() { + if (!g_running.exchange(false)) return; + g_stop.store(true, std::memory_order_release); + if (g_worker.joinable()) g_worker.join(); + g_connected.store(false, std::memory_order_release); +} + +bool Read(std::array& statuses) { + if (!g_connected.load(std::memory_order_acquire)) return false; + std::lock_guard lock(g_mutex); + statuses = g_statuses; + return true; +} + +bool SetRumble(uint32_t port, bool enabled) { + if (port >= g_rumble.size() || !g_connected.load(std::memory_order_acquire)) return false; + std::lock_guard lock(g_mutex); + g_rumble[port] = enabled ? 1 : 0; + return true; +} + +AdapterInfo GetInfo() { + std::lock_guard lock(g_mutex); + return g_info; +} + +} // namespace Wup028Adapter