This commit is contained in:
patchzyy
2026-08-30 11:21:10 +02:00
parent 8d569ef77b
commit 69be584cc5
19 changed files with 65 additions and 675 deletions
-3
View File
@@ -25,9 +25,6 @@ Code.pul
# Build output
/build/
/build-*/
/.build-output/
/.build-test/
/native-build/
/dist/
/out/
[Bb]in/
+1 -1
View File
@@ -98,7 +98,7 @@ Assert-Directory $dependencySources 'Pinned offline dependency sources'
# to compile (launcher/Prepare-NativePrebuilt.ps1).
# Kept in step with InstalledLayout.DependencyNames by Test-PinnedFacts.ps1: the installed host
# refuses to call a toolkit complete unless every one of these directories is present.
$requiredDependencies = @('abseil-cpp','cppwinrt','dawn_prebuilt','fmt','freetype','imgui','native_prebuilt','png','SDL','sqlite3','tracy','xxhash','zlib','zstd')
$requiredDependencies = @('abseil-cpp','cppwinrt','dawn_prebuilt','fmt','freetype','imgui','libusb','native_prebuilt','png','SDL','sqlite3','tracy','xxhash','zlib','zstd')
# The precompiled archives are only interchangeable with what the user's machine
# compiles if both came from this toolchain and this flag set, so a stale package
@@ -26,7 +26,7 @@ internal static class InstalledLayout
/// </summary>
public static readonly string[] DependencyNames =
[
"abseil-cpp", "cppwinrt", "dawn_prebuilt", "fmt", "freetype", "imgui", "native_prebuilt",
"abseil-cpp", "cppwinrt", "dawn_prebuilt", "fmt", "freetype", "imgui", "libusb", "native_prebuilt",
"png", "SDL", "sqlite3", "tracy", "xxhash", "zlib", "zstd"
];
}
+2
View File
@@ -57,6 +57,8 @@ The port does NOT pretend to be a Wii Remote or Classic Controller.
Mappings are positional (`south`, `east`, `west`, `north`) rather than Xbox-labelled, so the
same config makes sense on Xbox, PlayStation, Nintendo and generic SDL pads alike, and extra
inputs like paddles, touchpads and share buttons show up when the hardware reports them.
The official Wii U / Switch GameCube adapter (WUP-028) works too; as with Dolphin, on Windows the
adapter must be switched to the WinUSB driver once (Zadig).
## Requirements
+2 -1
View File
@@ -118,7 +118,7 @@ Source: <https://github.com/higan-emu/libco>. Full license text:
## Fetched at build time and redistributed in release builds
These are pinned in `aurora-main/extern/CMakeLists.txt` and
These are pinned in `aurora-main/extern/CMakeLists.txt`, `aurora-main/CMakeLists.txt` and
`aurora-main/cmake/AuroraDawnProvider.cmake`. They are not stored in this repository; the build
downloads them, and release installers carry the resulting binaries. Their license texts are
included in the installer's `licenses/` folder.
@@ -129,6 +129,7 @@ included in the installer's `licenses/` folder.
| Tint (part of Dawn) | with Dawn | BSD-3-Clause | <https://dawn.googlesource.com/dawn> |
| DirectXShaderCompiler (`dxcompiler.dll`) | with Dawn | NCSA / University of Illinois Open Source | <https://github.com/microsoft/DirectXShaderCompiler> |
| SDL | 3.4.4 | zlib | <https://github.com/libsdl-org/SDL> |
| libusb (linked into SDL on Windows) | 1.0.30 | LGPL-2.1-or-later | <https://github.com/libusb/libusb> |
| Abseil | LTS 20240722.0 | Apache-2.0 | <https://github.com/abseil/abseil-cpp> |
| Dear ImGui | 1.91.9b-docking | MIT | <https://github.com/ocornut/imgui> |
| {fmt} | 11.1.4 | MIT | <https://github.com/fmtlib/fmt> |
+2
View File
@@ -16,6 +16,7 @@ option(AURORA_CACHE_USE_ZSTD "Compress WebGPU cache entries with zstd" ON)
set(AURORA_DAWN_VERSION "v20260603.191052" CACHE STRING "Dawn version tag (https://github.com/encounter/dawn-build/releases)")
set(AURORA_SDL3_VERSION "3.4.4" CACHE STRING "SDL3 version tag (https://github.com/libsdl-org/SDL/releases)")
set(AURORA_NOD_VERSION "v2.0.0-alpha.8" CACHE STRING "nod version tag (https://github.com/encounter/nod/releases)")
set(AURORA_LIBUSB_VERSION "1.0.30" CACHE STRING "libusb version tag (https://github.com/libusb/libusb/releases)")
# Platform-specific defaults
if (CMAKE_CROSSCOMPILING)
@@ -44,6 +45,7 @@ set(AURORA_SDL3_PROVIDER "${_default_provider}" CACHE STRING
set_property(CACHE AURORA_SDL3_PROVIDER PROPERTY STRINGS auto vendor system package)
set(AURORA_SDL3_LINKAGE "${_default_linkage}" CACHE STRING "SDL3 linkage type preference")
set_property(CACHE AURORA_SDL3_LINKAGE PROPERTY STRINGS shared static)
option(AURORA_SDL3_LIBUSB "Build the vendored SDL3 with libusb on Windows (official GameCube adapter support)" ON)
# nod (if AURORA_ENABLE_DVD)
set(AURORA_NOD_PROVIDER "${_default_provider}" CACHE STRING
+43
View File
@@ -0,0 +1,43 @@
# libusb for SDL3's HIDAPI joystick drivers on Windows.
#
# The official GameCube adapter (WUP-028) is a vendor-specific USB device, not HID, so SDL3
# can only reach it through libusb - which the official SDL3 packages leave out. The vendored
# SDL3 build compiles libusb from the pinned upstream release and links it in statically.
include(FetchContent)
FetchContent_Declare(libusb
URL "https://github.com/libusb/libusb/releases/download/v${AURORA_LIBUSB_VERSION}/libusb-${AURORA_LIBUSB_VERSION}.tar.bz2"
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
# Upstream ships no CMakeLists.txt, so this only populates the source tree.
FetchContent_MakeAvailable(libusb)
set(_libusb_root "${libusb_SOURCE_DIR}/libusb")
add_library(usb-1.0 STATIC
"${_libusb_root}/core.c"
"${_libusb_root}/descriptor.c"
"${_libusb_root}/hotplug.c"
"${_libusb_root}/io.c"
"${_libusb_root}/strerror.c"
"${_libusb_root}/sync.c"
"${_libusb_root}/os/events_windows.c"
"${_libusb_root}/os/threads_windows.c"
"${_libusb_root}/os/windows_common.c"
"${_libusb_root}/os/windows_usbdk.c"
"${_libusb_root}/os/windows_winusb.c"
)
target_include_directories(usb-1.0
PUBLIC "${_libusb_root}"
PRIVATE "${CMAKE_CURRENT_LIST_DIR}/libusb" "${_libusb_root}/os"
)
set_target_properties(usb-1.0 PROPERTIES UNITY_BUILD OFF)
add_library(LibUSB::LibUSB ALIAS usb-1.0)
# SDL's FindLibUSB expects an installed copy. Satisfy its presence checks with this target
# instead: the alias pre-empts the imported target it would otherwise create, and the link
# probe is answered up front because the archive does not exist until build time.
set(LibUSB_INCLUDE_PATH "${_libusb_root}" CACHE PATH "" FORCE)
set(LibUSB_LIBRARY "usb-1.0" CACHE STRING "" FORCE)
set(HAVE_LIBUSB_H 1 CACHE INTERNAL "")
set(SDL_HIDAPI_LIBUSB ON CACHE BOOL "" FORCE)
set(SDL_HIDAPI_LIBUSB_SHARED OFF CACHE BOOL "" FORCE)
@@ -129,6 +129,9 @@ elseif (_aurora_sdl3_provider STREQUAL "vendor")
endif ()
if (WIN32)
set(SDL_LIBC ON CACHE BOOL "Use the system C library" FORCE)
if (AURORA_SDL3_LIBUSB)
include("${CMAKE_CURRENT_LIST_DIR}/AuroraLibUSB.cmake")
endif ()
endif ()
include(FetchContent)
+8
View File
@@ -0,0 +1,8 @@
/* libusb build configuration for the Windows backend (MinGW/Clang). */
#pragma once
#define PLATFORM_WINDOWS 1
#define ENABLE_LOGGING 1
#define DEFAULT_VISIBILITY
#define HAVE_STRUCT_TIMESPEC 1
#define PRINTF_FORMAT(a, b) __attribute__((__format__(__printf__, a, b)))
-1
View File
@@ -229,7 +229,6 @@ s32 PADGetNativeButtonPressed(u32 port);
PADSignedNativeAxis PADGetNativeAxisPulled(u32 port);
void PADRestoreDefaultMapping(u32 port);
void PADBlockInput(bool block);
bool PADIsInputBlocked(void);
/**
* Set the default controller mapping used.
-1
View File
@@ -1540,7 +1540,6 @@ void PADBlockInput(const bool block) {
}
}
bool PADIsInputBlocked() { return g_blockPAD.load(std::memory_order_acquire); }
SDL_Gamepad* PADGetSDLGamepadForIndex(const u32 index) {
const auto* ctrl = __PADGetControllerForIndex(index);
-9
View File
@@ -237,15 +237,6 @@ set(MKW_CPU_BASELINE_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/host_cpu_baseline.cpp
list(REMOVE_ITEM SOURCES ${MKW_BASE_PRODUCT_SOURCE} ${MKW_RETRO_REWIND_PRODUCT_SOURCE}
${MKW_CPU_BASELINE_SOURCE})
# WUP-028 (official GameCube adapter) support talks to the adapter over WinUSB, which only
# exists on Windows - SDL3 already exposes the same hardware as a normal joystick on Linux/macOS,
# so this file has nothing to do there. Every call site into it is separately gated behind
# #if defined(_WIN32) (see wup028_adapter.cpp's own header comment), so it's safe to simply not
# compile it at all on other platforms rather than build a stub implementation.
if(NOT WIN32)
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/wup028_adapter.cpp")
endif()
# The translator emits the complete, content-addressed source graph. Consuming
# this one manifest keeps configure independent of the 28k generated function
# files and of optional Retro Rewind artifacts such as code.map.
+1 -1
View File
@@ -210,7 +210,7 @@ function(mkw_configure_product target)
if(WIN32)
target_link_libraries(${target} PRIVATE
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp setupapi winusb)
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE)
else()
-28
View File
@@ -63,11 +63,6 @@ struct RuntimeUserConfig {
// comma-separated SDL-style physical button names ("south", or
// "dpad_up,left_shoulder") as values; pressing either bound button counts.
std::array<std::optional<std::string>, 12> controllerButtons;
#ifdef _WIN32
// One-based physical WUP-028 adapter port assigned to each game port.
// Zero or a missing value means the adapter does not own that game port.
std::array<uint32_t, 4> gameCubeAdapterPorts{};
#endif
};
namespace RuntimeConfigFile {
@@ -367,14 +362,6 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
config.controllerButtons[index] =
FindConfigValue<std::string>(document, "controller", buttonKeys[index]);
}
#ifdef _WIN32
for (size_t index = 0; index < config.gameCubeAdapterPorts.size(); ++index) {
const std::string key = "adapter_port_" + std::to_string(index + 1);
if (auto value = FindConfigUint(document, "controller", key); value && *value <= 4) {
config.gameCubeAdapterPorts[index] = *value;
}
}
#endif
config.widescreen = FindConfigValue<bool>(document, "video", "widescreen");
config.windowPosX = FindConfigInt(document, "video", "window_x");
@@ -638,21 +625,6 @@ inline bool SetControllerButton(size_t index, std::string value) {
return WriteSetting("controller", kControllerButtonKeys[index], FormatString(value));
}
#ifdef _WIN32
inline int GameCubeAdapterPort(size_t gamePort) {
if (gamePort >= Get().gameCubeAdapterPorts.size()) return -1;
const uint32_t physicalPort = Get().gameCubeAdapterPorts[gamePort];
return physicalPort >= 1 && physicalPort <= 4 ? static_cast<int>(physicalPort - 1) : -1;
}
inline bool SetGameCubeAdapterPort(size_t gamePort, int physicalPort) {
if (gamePort >= Mutable().gameCubeAdapterPorts.size() || physicalPort < -1 || physicalPort >= 4) return false;
const uint32_t storedPort = physicalPort < 0 ? 0u : static_cast<uint32_t>(physicalPort + 1);
Mutable().gameCubeAdapterPorts[gamePort] = storedPort;
return WriteSetting("controller", "adapter_port_" + std::to_string(gamePort + 1), std::to_string(storedPort));
}
#endif
inline bool SetAudioVolume(float value) {
value = std::clamp(value, 0.0f, 1.0f);
Mutable().audioVolume = value;
-46
View File
@@ -1,46 +0,0 @@
#pragma once
#include <array>
#include <cstdint>
#include <string>
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<bool, 4> ports{};
// Raw adapter type/status byte. High nibble 1 is wired, 2 is wireless.
std::array<uint8_t, 4> portStatus{};
std::array<uint64_t, 4> 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<PADStatus, 4>& statuses);
// Assigns a physical adapter port to a game port. Pass -1 to leave the game
// port under Aurora's normal controller assignment.
void SetPortAssignment(uint32_t gamePort, int physicalPort);
int GetPortAssignment(uint32_t gamePort);
// 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
-23
View File
@@ -1,9 +1,6 @@
#include "hle_stubs.h"
#include "memory.h"
#include "hle/controller_status_contract.h"
#ifdef _WIN32
#include "wup028_adapter.h"
#endif
#include <algorithm>
#include <cstdio>
@@ -36,9 +33,6 @@ void WritePadStatus(uint32_t base, const PADStatus& status) {
extern "C" uint32_t PAD__Init_HLE()
{
#if defined(_WIN32)
Wup028Adapter::Initialize();
#endif
return PADInit() ? 1u : 0u;
}
PPC_NATIVE_OVERRIDE(801AF2F0, PAD__Init_HLE, uint32_t, (), ());
@@ -51,17 +45,6 @@ extern "C" uint32_t PAD__Read_HLE(uint32_t statusPtr)
PADStatus statuses[PAD_CHANMAX]{};
uint32_t rumbleMask = PADRead(statuses);
#if defined(_WIN32)
std::array<PADStatus, PAD_CHANMAX> adapterStatuses{};
if (Wup028Adapter::Read(adapterStatuses) && !PADIsInputBlocked()) {
for (uint32_t port = 0; port < PAD_CHANMAX; ++port) {
if (adapterStatuses[port].err == PAD_ERR_NONE) {
statuses[port] = adapterStatuses[port];
rumbleMask |= PAD_CHAN0_BIT >> port;
}
}
}
#endif
try {
for (uint32_t i = 0; i < PAD_CHANMAX; ++i) {
@@ -90,12 +73,6 @@ PPC_NATIVE_OVERRIDE(801AF1E4, PAD__Recalibrate_HLE, uint32_t, (uint32_t mask), (
extern "C" void PAD__ControlMotor_HLE(int32_t chan, uint32_t command)
{
#if defined(_WIN32)
if (!Wup028Adapter::SetRumble(static_cast<uint32_t>(chan), command == PAD_MOTOR_RUMBLE)) {
PADControlMotor(chan, command);
}
#else
PADControlMotor(chan, command);
#endif
}
PPC_NATIVE_OVERRIDE_VOID(801AF908, PAD__ControlMotor_HLE, (int32_t chan, uint32_t command), (chan, command));
-17
View File
@@ -49,7 +49,6 @@
#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"
@@ -59,7 +58,6 @@
#include <aurora/aurora.h>
#include <aurora/gfx.h>
#include <dolphin/gx/GXAurora.h>
#include <dolphin/pad.h>
#include <dolphin/vi.h>
// Defined in `runtime/src/hle/vi.cpp` (used by GX/VI HLE).
@@ -1378,12 +1376,6 @@ int RuntimeMain(int argc, char** argv) {
}
aurora_set_frame_worker_wait_callback(ServiceGuestTimingDuringAuroraFrameWait);
GxGuestWrite::InstallAuroraHooks();
#if defined(_WIN32)
Wup028Adapter::Initialize();
for (uint32_t port = 0; port < PAD_CHANMAX; ++port) {
if (Wup028Adapter::GetPortAssignment(port) >= 0) PADClearPort(port);
}
#endif
UpdateMkwDynamicAspectSurface(auroraInfo.windowSize.native_fb_width,
auroraInfo.windowSize.native_fb_height);
settings_overlay::InitializeRuntimeSettings();
@@ -1425,9 +1417,6 @@ int RuntimeMain(int argc, char** argv) {
// Shutdown fiber system
Fiber::GuestFiberManager::Shutdown();
WindowPlacementPersistence::Flush(true);
#if defined(_WIN32)
Wup028Adapter::Shutdown();
#endif
aurora_shutdown();
SetRuntimeExitCodeImpl(0);
ShutdownProcessTranscript();
@@ -1445,9 +1434,6 @@ int RuntimeMain(int argc, char** argv) {
SetRuntimeExitCodeImpl(1);
Fiber::GuestFiberManager::Shutdown();
WindowPlacementPersistence::Flush(true);
#if defined(_WIN32)
Wup028Adapter::Shutdown();
#endif
aurora_shutdown();
ShutdownProcessTranscript();
return 1;
@@ -1459,9 +1445,6 @@ int RuntimeMain(int argc, char** argv) {
SetRuntimeExitCodeImpl(1);
Fiber::GuestFiberManager::Shutdown();
WindowPlacementPersistence::Flush(true);
#if defined(_WIN32)
Wup028Adapter::Shutdown();
#endif
aurora_shutdown();
ShutdownProcessTranscript();
return 1;
+2 -93
View File
@@ -1,7 +1,4 @@
#include "settings_overlay.h"
#ifdef _WIN32
#include "wup028_adapter.h"
#endif
#include "audio_backend.h"
#include "controller_mapping_wizard.h"
#include "game_graphics_options.h"
@@ -314,41 +311,6 @@ void ApplyConfiguredMappings() {
}
}
void DrawGameCubeAdapterInfo() {
// The official GameCube adapter is Windows-only (see wup028_adapter.cpp); on Linux/macOS,
// Wup028Adapter is a permanently-disconnected stub, and SDL3 already exposes the same
// hardware as a normal joystick, so this menu would only ever show "Searching" and four
// perpetually-empty adapter ports - confusing clutter for a feature that can't do anything
// on this platform. Skip it entirely rather than render a menu that never has content.
#if defined(_WIN32)
ImGui::Separator();
if (!ImGui::BeginMenu("GameCube adapter info")) return;
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<unsigned>(port + 1),
adapter.ports[port] ? "Controller connected" : "Empty", typeName,
adapter.portStatus[port]);
}
}
ImGui::EndMenu();
#endif
}
void DrawControllerSettings() {
for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
const std::string label = "Port " + std::to_string(port + 1);
@@ -360,50 +322,10 @@ void DrawControllerSettings() {
ImGui::Separator();
const uint32_t selectedGamePort = static_cast<uint32_t>(g_controllerPort);
#if defined(_WIN32)
const int adapterAssignment = Wup028Adapter::GetPortAssignment(selectedGamePort);
if (adapterAssignment >= 0) {
ImGui::Text("Assigned: GameCube adapter port %d", adapterAssignment + 1);
} else
#endif
{
const char* currentName = PADGetName(selectedGamePort);
ImGui::Text("Assigned: %s", currentName != nullptr ? currentName : "None");
}
#if defined(_WIN32)
// Windows-only, same reasoning as DrawGameCubeAdapterInfo() above: on other platforms
// adapterAssignment is always -1 and every port would always read "(empty)", so this submenu
// would never have anything real to offer.
if (ImGui::BeginMenu("Assign GameCube adapter port")) {
if (ImGui::MenuItem("None", nullptr, adapterAssignment < 0)) {
Wup028Adapter::SetPortAssignment(selectedGamePort, -1);
RuntimeConfigFile::SetGameCubeAdapterPort(selectedGamePort, -1);
}
const auto adapter = Wup028Adapter::GetInfo();
for (int physicalPort = 0; physicalPort < PAD_CHANMAX; ++physicalPort) {
const std::string label = "Adapter port " + std::to_string(physicalPort + 1) +
(adapter.ports[static_cast<size_t>(physicalPort)] ? " (connected)" : " (empty)");
if (ImGui::MenuItem(label.c_str(), nullptr, adapterAssignment == physicalPort)) {
for (uint32_t gamePort = 0; gamePort < PAD_CHANMAX; ++gamePort) {
if (gamePort != selectedGamePort && Wup028Adapter::GetPortAssignment(gamePort) == physicalPort) {
RuntimeConfigFile::SetGameCubeAdapterPort(gamePort, -1);
}
}
PADClearPort(selectedGamePort);
Wup028Adapter::SetPortAssignment(selectedGamePort, physicalPort);
RuntimeConfigFile::SetGameCubeAdapterPort(selectedGamePort, physicalPort);
g_configuredControllerIndices.fill(std::numeric_limits<int32_t>::min());
}
}
ImGui::EndMenu();
}
#endif
const char* currentName = PADGetName(selectedGamePort);
ImGui::Text("Assigned: %s", currentName != nullptr ? currentName : "None");
if (ImGui::MenuItem("Unassign controller")) {
PADClearPort(selectedGamePort);
#if defined(_WIN32)
Wup028Adapter::SetPortAssignment(selectedGamePort, -1);
RuntimeConfigFile::SetGameCubeAdapterPort(selectedGamePort, -1);
#endif
g_configuredControllerIndices.fill(std::numeric_limits<int32_t>::min());
}
ImGui::Separator();
@@ -411,9 +333,6 @@ void DrawControllerSettings() {
const uint32_t controllerCount = PADCount();
if (controllerCount == 0) {
ImGui::TextDisabled("No controller connected");
#if defined(_WIN32)
DrawGameCubeAdapterInfo();
#endif
return;
}
@@ -422,10 +341,6 @@ void DrawControllerSettings() {
const char* name = PADGetNameForControllerIndex(index);
ImGui::PushID(static_cast<int>(index));
if (ImGui::MenuItem(name != nullptr ? name : "Unknown controller")) {
#if defined(_WIN32)
Wup028Adapter::SetPortAssignment(selectedGamePort, -1);
RuntimeConfigFile::SetGameCubeAdapterPort(selectedGamePort, -1);
#endif
PADSetPortForIndex(index, selectedGamePort);
g_configuredControllerIndices.fill(std::numeric_limits<int32_t>::min());
ApplyConfiguredMappings();
@@ -439,9 +354,6 @@ void DrawControllerSettings() {
PADButtonMapping* mappings = PADGetButtonMappings(static_cast<uint32_t>(g_controllerPort), &mappingCount);
if (mappings == nullptr || mappingCount != PAD_BUTTON_COUNT) {
ImGui::TextDisabled("Assign a controller to edit its buttons");
#if defined(_WIN32)
DrawGameCubeAdapterInfo();
#endif
return;
}
@@ -581,9 +493,6 @@ void DrawControllerSettings() {
ImGui::TextUnformatted(kControllerButtons[i].label);
ImGui::PopID();
}
#if defined(_WIN32)
DrawGameCubeAdapterInfo();
#endif
}
void DrawAudioSettings() {
-450
View File
@@ -1,450 +0,0 @@
#include "wup028_adapter.h"
#include "runtime_log.h"
#include "runtime_config.h"
#include <dolphin/pad.h>
#include <windows.h>
#include <initguid.h>
#include <setupapi.h>
#include <usb.h>
#include <usbiodef.h>
#include <winusb.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <cwctype>
#include <cstring>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
namespace Wup028Adapter {
namespace {
constexpr uint16_t kNintendoVendor = 0x057e;
constexpr uint16_t kAdapterProduct = 0x0337;
constexpr size_t kReportSize = 37;
constexpr auto kInputReportTimeout = std::chrono::milliseconds(500);
std::mutex g_mutex;
std::array<PADStatus, PAD_CHANMAX> g_statuses{};
std::array<uint8_t, PAD_CHANMAX> g_rumble{};
std::array<int8_t, PAD_CHANMAX> g_portAssignments{{-1, -1, -1, -1}};
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_t>(size), '\0');
WideCharToMultiByte(CP_UTF8, 0, value, -1, result.data(), size, nullptr, nullptr);
result.resize(static_cast<size_t>(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<uint8_t> storage(required);
auto* detail = reinterpret_cast<SP_DEVICE_INTERFACE_DETAIL_DATA_W*>(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<wchar_t>(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<wchar_t, 256> description{};
if (SetupDiGetDeviceRegistryPropertyW(devices, &deviceInfo, SPDRP_FRIENDLYNAME, nullptr,
reinterpret_cast<BYTE*>(description.data()),
static_cast<DWORD>(description.size() * sizeof(wchar_t)), nullptr) ||
SetupDiGetDeviceRegistryPropertyW(devices, &deviceInfo, SPDRP_DEVICEDESC, nullptr,
reinterpret_cast<BYTE*>(description.data()),
static_cast<DWORD>(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) {
constexpr int kCenter = 128;
constexpr int kCenterTolerance = 10;
if (raw >= kCenter - kCenterTolerance && raw <= kCenter + kCenterTolerance) return 0;
return static_cast<int8_t>(std::clamp(static_cast<int>(raw) - kCenter, -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<uint8_t, PAD_CHANMAX>& motors) {
std::array<UCHAR, 5> 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);
g_rumble.fill(0);
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<unsigned>(device.inputPipe) << ", output endpoint 0x"
<< static_cast<unsigned>(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<uint8_t, PAD_CHANMAX> sentRumble{};
auto rateStart = std::chrono::steady_clock::now();
uint32_t rateReports = 0;
std::array<bool, PAD_CHANMAX> reportedPorts{};
auto lastReport = std::chrono::steady_clock::now();
while (!g_stop.load(std::memory_order_acquire)) {
std::array<UCHAR, kReportSize> report{};
ULONG read = 0;
bool timedOut = false;
if (!Transfer(device, true, device.inputPipe, report.data(), report.size(), read, 100, &timedOut)) {
if (timedOut && std::chrono::steady_clock::now() - lastReport < kInputReportTimeout) continue;
break;
}
if (read != report.size() || report[0] != 0x21) {
if (std::chrono::steady_clock::now() - lastReport >= kInputReportTimeout) break;
continue;
}
lastReport = std::chrono::steady_clock::now();
++rateReports;
std::array<PADStatus, PAD_CHANMAX> decoded{};
for (size_t port = 0; port < decoded.size(); ++port) decoded[port] = DecodePort(report.data() + 1 + port * 9);
std::array<uint8_t, PAD_CHANMAX> desired{};
std::array<int8_t, PAD_CHANMAX> 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<float>(now - rateStart).count();
if (seconds >= 1.0f) {
g_info.pollRateHz = static_cast<float>(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;
}
// Do not leave a motor latched on when stopping or abandoning this handle.
SendRumble(device, {});
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;
for (size_t gamePort = 0; gamePort < g_portAssignments.size(); ++gamePort) {
g_portAssignments[gamePort] = static_cast<int8_t>(RuntimeConfigFile::GameCubeAdapterPort(gamePort));
}
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<PADStatus, 4>& statuses) {
if (!g_connected.load(std::memory_order_acquire)) return false;
std::lock_guard lock(g_mutex);
for (auto& status : statuses) status.err = PAD_ERR_NO_CONTROLLER;
for (size_t gamePort = 0; gamePort < statuses.size(); ++gamePort) {
const int physicalPort = g_portAssignments[gamePort];
if (physicalPort >= 0) statuses[gamePort] = g_statuses[static_cast<size_t>(physicalPort)];
}
return true;
}
void SetPortAssignment(uint32_t gamePort, int physicalPort) {
if (gamePort >= g_portAssignments.size() || physicalPort < -1 || physicalPort >= PAD_CHANMAX) return;
std::lock_guard lock(g_mutex);
const int oldPhysicalPort = g_portAssignments[gamePort];
if (oldPhysicalPort >= 0) g_rumble[static_cast<size_t>(oldPhysicalPort)] = 0;
if (physicalPort >= 0) {
for (auto& assignment : g_portAssignments) {
if (assignment == physicalPort) {
assignment = -1;
g_rumble[static_cast<size_t>(physicalPort)] = 0;
}
}
}
g_portAssignments[gamePort] = static_cast<int8_t>(physicalPort);
}
int GetPortAssignment(uint32_t gamePort) {
if (gamePort >= g_portAssignments.size()) return -1;
std::lock_guard lock(g_mutex);
return g_portAssignments[gamePort];
}
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);
if (!g_connected.load(std::memory_order_acquire)) return false;
const int physicalPort = g_portAssignments[port];
if (physicalPort < 0) return false;
const size_t adapterPort = static_cast<size_t>(physicalPort);
if (g_statuses[adapterPort].err != PAD_ERR_NONE) {
g_rumble[adapterPort] = 0;
return false;
}
g_rumble[adapterPort] = enabled ? 1 : 0;
return true;
}
AdapterInfo GetInfo() {
std::lock_guard lock(g_mutex);
return g_info;
}
} // namespace Wup028Adapter