#include "wii_remote_input.h" #include "runtime_config.h" #include "runtime_log.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace WiiRemoteInput { namespace { // Dolphin's continuous scanning polls Bluetooth about once a second; SDL's // enumeration walks every HID device on the main thread, so stay a bit lazier. constexpr uint64_t kScanIntervalMs = 2000; // Right after a remote drops it is almost certainly still there, but the first // re-open attempts tend to fail on timed-out reads, so retry quickly for a while. constexpr uint64_t kFastScanIntervalMs = 500; constexpr uint64_t kFastScanWindowMs = 15000; // How long the Wii driver hint stays at "0" during a rescan; SDL applies hint // changes on its next joystick update, once per pumped frame. constexpr uint64_t kRescanDriverOffMs = 100; constexpr float kStandardGravity = 9.80665f; // SDL's Wii driver posts the remote's own buttons as raw joystick buttons // starting at SDL_GAMEPAD_BUTTON_MISC1, in this order (SDL_hidapi_wii.c, // EWiiButtons), whatever the extension. enum RawWiiButton : int { kRawA = SDL_GAMEPAD_BUTTON_MISC1, kRawB, kRawOne, kRawTwo, kRawPlus, kRawMinus, kRawHome, kRawDpadUp, kRawDpadDown, kRawDpadLeft, kRawDpadRight, }; // WPAD_BUTTON_* bits as the game reads them from KPADStatus.hold. constexpr uint32_t kWpadLeft = 0x0001, kWpadRight = 0x0002, kWpadDown = 0x0004, kWpadUp = 0x0008, kWpadPlus = 0x0010, kWpadTwo = 0x0100, kWpadOne = 0x0200, kWpadB = 0x0400, kWpadA = 0x0800, kWpadMinus = 0x1000, kWpadZ = 0x2000, kWpadC = 0x4000, kWpadHome = 0x8000; // WPAD_CL_BUTTON_* bits (WPADCLStatus.clButton / KPADStatus.ex_status.cl.hold): // the Classic Controller's two button bytes, inverted, high byte first. constexpr uint32_t kClUp = 0x0001, kClLeft = 0x0002, kClZR = 0x0004, kClX = 0x0008, kClA = 0x0010, kClY = 0x0020, kClB = 0x0040, kClZL = 0x0080, kClR = 0x0200, kClPlus = 0x0400, kClHome = 0x0800, kClMinus = 0x1000, kClL = 0x2000, kClDown = 0x4000, kClRight = 0x8000; constexpr Uint16 kNintendoVendor = 0x057e; // How long a vanished remote keeps its channel alive with neutral input. SDL's // in-place reconnect after an extension change takes well under a second; a // remote that is really gone shows up as disconnected after this. constexpr uint64_t kExtensionSwapGraceMs = 3000; // Rescanning closes and re-opens the Bluetooth HID handle, which some Windows // stacks answer by dropping the link; leave SDL's own reconnect this long first. constexpr uint64_t kScanStartDelayMs = 3000; // Per-port memory of the last Wii controller seen there, for EffectiveKind. struct PortMemory { Kind lastKind = Kind::NotWii; uint64_t lastSeenMs = 0; }; std::array g_ports{}; bool g_wiiDriverEnabled = false; uint64_t g_lastScanMs = 0; uint32_t g_scanCount = 0; bool g_scanning = false; // Non-zero while a rescan has the Wii driver hint switched off (see RescanNow). uint64_t g_driverOffSinceMs = 0; // When the current scan started (last Wii controller seen). uint64_t g_lostAtMs = 0; bool g_hidrawWarned = false; // Instance ids whose accelerometers have been switched on. SDL keeps sensors // off until asked and forgets that when the gamepad is closed, so a re-paired // remote gets a fresh id and is enabled again. std::array g_sensorsEnabledFor{}; // Zero-point correction subtracted from a remote's accelerometer, in g and in // SDL's sensor frame, with the config key it is stored under. Resolved once per // gamepad instance (a re-created joystick of the same remote resolves to the same // key) and replaced by a calibration run. The Nunchuk accelerometer is left // uncorrected. struct RemoteCalibration { SDL_JoystickID instance = 0; std::string key; std::array offset{}; }; std::array g_remoteCalibration{}; // Frames sampled by a calibration run (about 1.5 s at 60 Hz) and how far a // sample may stray from the first one before the run is declared "moved". constexpr int kCalibrationSamples = 90; constexpr float kCalibrationMaxDeviationG = 0.15f; // A resting remote reads about 1 g and lies within 20 degrees of face up. constexpr double kCalibrationMinGravityG = 0.8; constexpr double kCalibrationMaxGravityG = 1.2; constexpr double kCalibrationMinTiltCosine = 0.9396926; struct AccelCalibration { bool active = false; uint32_t chan = 0; SDL_JoystickID instance = 0; int count = 0; double sum[3] = {}; float first[3] = {}; }; AccelCalibration g_calibration; char g_calibrationMessage[160] = {}; uint32_t g_calibrationMessageChan = 0; SDL_JoystickID g_calibrationMessageRemote = 0; // Last accepted KPAD acc per port, for the remote and for the Nunchuk. Reused on // frames that bring no sample or a glitched one, so the game never sees a jump. struct LastAcc { bool valid = false; float acc[3] = {0.0f, -1.0f, 0.0f}; }; std::array g_lastAcc{}; std::array g_lastNunchukAcc{}; // Any axis beyond this is not a reading the remote's +-3 g sensor can produce. constexpr float kMaxPlausibleRemoteG = 4.0f; // Case-sensitive substring test that tolerates a null name. bool NameContains(const char* name, const char* needle) { return name != nullptr && std::strstr(name, needle) != nullptr; } // Turns on the remote (and Nunchuk) accelerometers once per gamepad instance. void EnsureSensors(SDL_Gamepad* gamepad, uint32_t port) { const SDL_JoystickID id = SDL_GetGamepadID(gamepad); if (g_sensorsEnabledFor[port] == id) { return; } bool allEnabled = true; for (SDL_SensorType sensor : {SDL_SENSOR_ACCEL, SDL_SENSOR_ACCEL_L}) { if (SDL_GamepadHasSensor(gamepad, sensor) && !SDL_SetGamepadSensorEnabled(gamepad, sensor, true)) { RT_LOG(RT_TAG_CONFIG) << "Wii Remote on port " << (port + 1) << ": could not enable an accelerometer: " << SDL_GetError() << std::endl; allEnabled = false; } } // Only remember the instance once every sensor is on, so a failed attempt // is retried on the next sample instead of leaving the accelerometer off. if (allEnabled) { g_sensorsEnabledFor[port] = id; } } // USB interface number of the HID device at `path`, or -1 when it is not a USB // interface or cannot be found. A DolphinBar shows each remote slot as one. int UsbInterfaceForPath(const char* path, Uint16 vendor, Uint16 product) { if (path == nullptr) { return -1; } int interfaceNumber = -1; SDL_hid_device_info* list = SDL_hid_enumerate(vendor, product); for (const SDL_hid_device_info* info = list; info != nullptr; info = info->next) { if (info->path != nullptr && std::strcmp(info->path, path) == 0) { interfaceNumber = info->interface_number; break; } } SDL_hid_free_enumeration(list); return interfaceNumber; } // Config key identifying the physical remote behind a gamepad: its HID serial // (the Bluetooth address when paired directly), else its USB interface number // (a DolphinBar has no serial and one interface per remote slot) and the game // port, else the game port. Only characters valid in a bare TOML key. std::string RemoteKey(SDL_Gamepad* gamepad, uint32_t port) { std::string serial; if (const char* raw = SDL_GetGamepadSerial(gamepad)) { for (const char* c = raw; *c != '\0'; ++c) { if (std::isalnum(static_cast(*c))) { serial += static_cast(std::tolower(static_cast(*c))); } } } if (serial.find_first_not_of('0') != std::string::npos) { return "serial_" + serial; } const Uint16 vendor = SDL_GetGamepadVendor(gamepad); const Uint16 product = SDL_GetGamepadProduct(gamepad); const int interfaceNumber = UsbInterfaceForPath(SDL_GetGamepadPath(gamepad), vendor, product); char key[48]; if (interfaceNumber >= 0) { std::snprintf(key, sizeof(key), "usb_%04x_%04x_if%d_port%u", vendor, product, interfaceNumber, port + 1); } else { std::snprintf(key, sizeof(key), "port_%u", port + 1); } return key; } // Calibration of the remote currently on `port`, resolved on first use per gamepad // instance. A remote without a stored entry has a zero offset. RemoteCalibration& CalibrationFor(SDL_Gamepad* gamepad, uint32_t port) { RemoteCalibration& calibration = g_remoteCalibration[port]; const SDL_JoystickID id = SDL_GetGamepadID(gamepad); if (calibration.instance != id) { static bool s_legacyReported = false; if (!s_legacyReported && RuntimeConfigFile::Get().hasLegacyWiiAccelOffset) { s_legacyReported = true; RT_LOG(RT_TAG_CONFIG) << "Old shared Wii Remote calibration ignored; recalibrate each remote in F10" << std::endl; } calibration.instance = id; calibration.key = RemoteKey(gamepad, port); const std::optional> stored = RuntimeConfigFile::WiiAccelOffsetFor(calibration.key); for (size_t i = 0; i < 3; ++i) calibration.offset[i] = stored ? static_cast((*stored)[i]) : 0.0f; RT_LOG(RT_TAG_CONFIG) << "Wii Remote on port " << (port + 1) << ": calibration key " << calibration.key << (stored ? "" : " (no stored offset)") << std::endl; } return calibration; } // Raw SDL sample in m/s^2, rejecting anything SDL has not delivered yet. bool ReadSdlAccel(SDL_Gamepad* gamepad, SDL_SensorType sensor, float* sdl) { return SDL_GamepadSensorEnabled(gamepad, sensor) && SDL_GetGamepadSensorData(gamepad, sensor, sdl, 3) && std::isfinite(sdl[0]) && std::isfinite(sdl[1]) && std::isfinite(sdl[2]); } // True for a sample that cannot have come from the sensor. Over Bluetooth on // Windows the remote delivers, a few times a minute, a report whose // accelerometer bytes are all zero; SDL decodes that as -0x200 on every axis, // i.e. (+5.12, -5.12, -5.12) g for the remote (100 units/g) and // (+2.56, -2.56, -2.56) g for the Nunchuk (200 units/g). Handed to the game as // is, one such frame is a full-lock steer plus a 9 g "shake". `g` is the // uncorrected SDL sample. bool IsGlitchedSample(SDL_SensorType sensor, const float* g) { // An exact zero vector is SDL's sensor buffer before the first report, not // a reading (the remote never delivers 0 g on all three axes at once). if (g[0] == 0.0f && g[1] == 0.0f && g[2] == 0.0f) { return true; } const float zero = sensor == SDL_SENSOR_ACCEL ? 5.12f : 2.56f; if (std::fabs(g[0] - zero) < 0.03f && std::fabs(g[1] + zero) < 0.03f && std::fabs(g[2] + zero) < 0.03f) { return true; } if (sensor == SDL_SENSOR_ACCEL) { for (int i = 0; i < 3; ++i) { if (std::fabs(g[i]) > kMaxPlausibleRemoteG) return true; } } return false; } // One accelerometer sample in g, SDL's sensor frame. The remote's own axes are // +x left, +y towards the user, +z out of the button face (wiibrew, Dolphin); // SDL_hidapi_wii.c posts (-wiiX, wiiZ, wiiY): x right across the face, y out of // the face (+1 at rest, buttons up), z towards the user, i.e. away from the tip. // The remote's sample gets the zero-point correction of the remote on `port`; the // Nunchuk's does not. False when there is no sample yet or the sample is a glitch // (see above). bool ReadAccelG(SDL_Gamepad* gamepad, uint32_t port, SDL_SensorType sensor, float* g) { float sdl[3] = {}; if (!ReadSdlAccel(gamepad, sensor, sdl)) { return false; } for (int i = 0; i < 3; ++i) g[i] = sdl[i] / kStandardGravity; if (IsGlitchedSample(sensor, g)) { return false; } if (sensor == SDL_SENSOR_ACCEL) { const std::array& offset = CalibrationFor(gamepad, port).offset; for (int i = 0; i < 3; ++i) g[i] -= offset[i]; } return true; } // KPAD's acc is the remote reading as (-wiiX, -wiiZ, wiiY): x right across the // face, y through the back of the remote (rest: -1 with the buttons up), z // towards the user. That is SDL's frame with y negated. Held sideways as a // wheel the rest vector is (1, 0, 0) and a turn shows up as z = sin(angle), so // the sign of z is the direction of the turn; Cemu does the same conversion for // real remotes on the Wii U. void AccelGToKpad(const float* g, float* kpad) { kpad[0] = g[0]; kpad[1] = -g[1]; kpad[2] = g[2]; } // Sensor -> KPAD acc for ReadKpadSample. bool ReadAccelAsKpad(SDL_Gamepad* gamepad, uint32_t port, SDL_SensorType sensor, float* kpad) { float g[3] = {}; if (!ReadAccelG(gamepad, port, sensor, g)) { return false; } AccelGToKpad(g, kpad); return true; } // Debug trace of every remote sample (controller.wii_accel_trace = true): // milliseconds, port, WPAD hold bits, the uncorrected SDL sample in g and the // KPAD acc handed to the game. One CSV per run, truncated at startup. void TraceSample(uint32_t chan, const KpadSample& sample, const float* rawG, bool haveRaw, bool accepted) { static std::ofstream trace; static bool opened = false; if (!opened) { opened = true; const std::filesystem::path path = RuntimeConfigFile::ApplicationDataDirectory() / "wii_accel_trace.csv"; trace.open(path, std::ios::trunc); if (trace) { trace << "ms,port,hold,raw_x,raw_y,raw_z,ok,kpad_x,kpad_y,kpad_z\n"; RT_LOG(RT_TAG_CONFIG) << "Wii Remote accelerometer trace: " << path.string() << std::endl; } else { RT_LOG(RT_TAG_CONFIG) << "Wii Remote accelerometer trace: could not open " << path.string() << std::endl; } } if (!trace) { return; } char line[192]; if (haveRaw) { std::snprintf(line, sizeof(line), "%llu,%u,%04x,%.4f,%.4f,%.4f,%d,%.4f,%.4f,%.4f\n", static_cast(SDL_GetTicks()), chan + 1, sample.hold, rawG[0], rawG[1], rawG[2], accepted ? 1 : 0, sample.acc[0], sample.acc[1], sample.acc[2]); } else { std::snprintf(line, sizeof(line), "%llu,%u,%04x,,,,0,%.4f,%.4f,%.4f\n", static_cast(SDL_GetTicks()), chan + 1, sample.hold, sample.acc[0], sample.acc[1], sample.acc[2]); } trace << line; // A frame per line; flush so a crash or a killed process keeps the tail. trace.flush(); } // Gamepad of the Wii Remote on `chan`, or nullptr. SDL_Gamepad* RemoteGamepad(uint32_t chan) { if (chan >= PAD_MAX_CONTROLLERS || !IsRemoteChannel(chan)) { return nullptr; } return SDL_GetGamepadFromPlayerIndex(static_cast(chan)); } void SetCalibrationMessage(uint32_t chan, const char* message) { SDL_Gamepad* gamepad = RemoteGamepad(chan); g_calibrationMessageChan = chan; g_calibrationMessageRemote = gamepad != nullptr ? SDL_GetGamepadID(gamepad) : 0; std::snprintf(g_calibrationMessage, sizeof(g_calibrationMessage), "%s", message); } // Ends a calibration run with a message for the overlay. void FinishAccelCalibration(uint32_t chan, const char* message) { g_calibration.active = false; SetCalibrationMessage(chan, message); RT_LOG(RT_TAG_CONFIG) << "Wii Remote accelerometer calibration: " << message << std::endl; } // One frame of a calibration run: accumulates the uncorrected sample and, once // enough frames are in, stores the mean minus the ideal rest vector (0, 1, 0). void StepAccelCalibration() { if (!g_calibration.active) { return; } SDL_Gamepad* gamepad = RemoteGamepad(g_calibration.chan); if (gamepad == nullptr || SDL_GetGamepadID(gamepad) != g_calibration.instance) { FinishAccelCalibration(g_calibration.chan, "Cancelled: the Wii Remote went away."); return; } EnsureSensors(gamepad, g_calibration.chan); float sdl[3] = {}; if (!ReadSdlAccel(gamepad, SDL_SENSOR_ACCEL, sdl)) { return; // no sample this frame; keep waiting } float g[3]; for (int i = 0; i < 3; ++i) g[i] = sdl[i] / kStandardGravity; if (IsGlitchedSample(SDL_SENSOR_ACCEL, g)) { return; // a zeroed report, not a movement } if (g_calibration.count == 0) { for (int i = 0; i < 3; ++i) g_calibration.first[i] = g[i]; } else { for (int i = 0; i < 3; ++i) { if (std::fabs(g[i] - g_calibration.first[i]) > kCalibrationMaxDeviationG) { FinishAccelCalibration(g_calibration.chan, "Failed: the remote moved. Put it down, buttons up, and try again."); return; } } } for (int i = 0; i < 3; ++i) g_calibration.sum[i] += g[i]; if (++g_calibration.count < kCalibrationSamples) { return; } std::array mean{}; for (int i = 0; i < 3; ++i) mean[i] = g_calibration.sum[i] / g_calibration.count; const double length = std::sqrt(mean[0] * mean[0] + mean[1] * mean[1] + mean[2] * mean[2]); if (length < kCalibrationMinGravityG || length > kCalibrationMaxGravityG || mean[1] / length < kCalibrationMinTiltCosine) { FinishAccelCalibration(g_calibration.chan, "Failed: the remote was not resting flat with the buttons up."); return; } const std::array offset = {mean[0], mean[1] - 1.0, mean[2]}; RemoteCalibration& calibration = CalibrationFor(gamepad, g_calibration.chan); for (int i = 0; i < 3; ++i) calibration.offset[i] = static_cast(offset[i]); const bool saved = RuntimeConfigFile::SetWiiAccelOffsetFor(calibration.key, offset); char message[160]; std::snprintf(message, sizeof(message), "%s offset x %+.3f y %+.3f z %+.3f g", saved ? "Calibrated:" : "Calibrated (could not write Config.toml):", offset[0], offset[1], offset[2]); FinishAccelCalibration(g_calibration.chan, message); } // True when any controller aurora knows about is a Wii device. bool AnyWiiControllerConnected() { const uint32_t count = PADCount(); for (uint32_t index = 0; index < count; ++index) { if (KindForName(PADGetNameForControllerIndex(index)) != Kind::NotWii) { return true; } } return false; } // True for an evdev node served by the kernel hid-wiimote driver. bool IsKernelWiiNode(SDL_JoystickID id) { const char* path = SDL_GetGamepadPathForID(id); return SDL_GetGamepadVendorForID(id) == kNintendoVendor && path != nullptr && std::strncmp(path, "/dev/input/", 11) == 0; } bool IsClassicNodeName(const char* name) { return NameContains(name, "Classic Controller"); } // HID device behind a kernel evdev node; the remote and its extension nodes share it. std::string KernelParent(SDL_JoystickID id) { static std::unordered_map s_parents; if (const auto it = s_parents.find(id); it != s_parents.end()) { return it->second; } const char* path = SDL_GetGamepadPathForID(id); if (path == nullptr) { return {}; } std::error_code ec; const auto parent = std::filesystem::canonical( std::filesystem::path("/sys/class/input") / std::filesystem::path(path).filename() / "device" / "device", ec); if (ec) { return {}; } return s_parents.emplace(id, parent.string()).first->second; } // The kernel node on the same remote as `gamepad`: its Classic Controller node // when `wantClassic`, else its remote node. SDL_Gamepad* KernelSibling(SDL_Gamepad* gamepad, bool wantClassic) { const SDL_JoystickID self = SDL_GetGamepadID(gamepad); const std::string parent = KernelParent(self); if (parent.empty()) { return nullptr; } int count = 0; SDL_JoystickID* ids = SDL_GetGamepads(&count); if (ids == nullptr) { return nullptr; } SDL_Gamepad* sibling = nullptr; for (int i = 0; i < count && sibling == nullptr; ++i) { if (ids[i] != self && IsKernelWiiNode(ids[i]) && IsClassicNodeName(SDL_GetGamepadNameForID(ids[i])) == wantClassic && KernelParent(ids[i]) == parent) { sibling = SDL_GetGamepadFromID(ids[i]); } } SDL_free(ids); return sibling; } // The kernel exposes a remote and its Classic Controller as separate gamepads. // The remote's port reads both, as on a Wii; the Classic node's own port stays empty. Kind KindForGamepad(SDL_Gamepad* gamepad) { if (gamepad == nullptr) { return Kind::NotWii; } const char* name = SDL_GetGamepadName(gamepad); const Kind kind = KindForName(name); if ((kind != Kind::Remote && kind != Kind::RemoteWithClassic) || !IsKernelWiiNode(SDL_GetGamepadID(gamepad))) { return kind; } if (IsClassicNodeName(name)) { return KernelSibling(gamepad, false) != nullptr ? Kind::NotWii : kind; } return KernelSibling(gamepad, true) != nullptr ? Kind::RemoteWithClassic : kind; } // A kernel Classic Controller node whose remote node carries it. bool IsCarriedKernelNode(SDL_Gamepad* gamepad) { return gamepad != nullptr && IsKernelWiiNode(SDL_GetGamepadID(gamepad)) && IsClassicNodeName(SDL_GetGamepadName(gamepad)) && KernelSibling(gamepad, false) != nullptr; } // Logs once when the kernel driver serves a Wii controller SDL's driver could have had. void WarnIfKernelDriverServing() { if (g_hidrawWarned || !g_wiiDriverEnabled) { return; } const uint64_t now = SDL_GetTicks(); static uint64_t s_nextCheckMs = 0; if (now < s_nextCheckMs) { return; } s_nextCheckMs = now + 2000; int count = 0; SDL_JoystickID* ids = SDL_GetGamepads(&count); if (ids == nullptr) { return; } for (int i = 0; i < count && !g_hidrawWarned; ++i) { const Uint16 product = SDL_GetGamepadProductForID(ids[i]); if ((product == 0x0306 || product == 0x0330) && IsKernelWiiNode(ids[i])) { g_hidrawWarned = true; RT_LOG(RT_TAG_CONFIG) << "A Wii controller is served by the kernel hid-wiimote driver. For full Wii " << "Remote support see the Linux udev rule in the README." << std::endl; } } SDL_free(ids); } // Route SDL's input diagnostics (HIDAPI open failures, the Wii driver's // extension/status messages) into console.log, minus the periodic chatter. // A sub-warning message is written once: SDL repeats the same line on every // enumeration (one "couldn't open /dev/hidraw7: Permission denied" per HID // device per pass), and console.log is unbuffered, so the repeats were a // per-pass burst of writes on the main thread for no new information. void SDLCALL LogSdlMessage(void*, int category, SDL_LogPriority priority, const char* message) { if (message == nullptr) { return; } if (category == SDL_LOG_CATEGORY_INPUT && priority < SDL_LOG_PRIORITY_WARN && (std::strstr(message, "Motion Plus") != nullptr || std::strstr(message, "Resetting report mode") != nullptr)) { return; } if (category != SDL_LOG_CATEGORY_INPUT && priority < SDL_LOG_PRIORITY_WARN) { return; } if (priority < SDL_LOG_PRIORITY_WARN) { // Device paths in these messages keep changing (/dev/hidrawN climbs with // hotplug churn), so cap the set instead of holding one string per line // for the whole session. static std::unordered_set s_seen; if (s_seen.size() >= 256) { s_seen.clear(); } if (!s_seen.insert(message).second) { return; } RT_LOG("sdl") << message << " (further identical messages suppressed)" << std::endl; return; } RT_LOG("sdl") << message << std::endl; } // Whether Poll() drives its own periodic re-enumeration. The 1->0->1 hint // flip below makes SDL close and re-open every HIDAPI device on the main // thread, and on Linux that means an open() attempt on every /dev/hidraw node // (each failing with EACCES until a udev rule grants access), which showed up // as a frame hitch every scan interval even on an empty menu. It exists for // Windows Bluetooth stacks, where a remote that drops or is switched on after // launch is not seen again until the driver re-enumerates. Linux and macOS // already get hotplug from udev / IOKit: SDL re-enumerates when a device // appears, so nothing periodic is needed there. The overlay's "Rescan now" // still works everywhere. #if defined(_WIN32) constexpr bool kPeriodicRescan = true; #else constexpr bool kPeriodicRescan = false; #endif // Second half of a rescan: re-enables the Wii driver once SDL has seen it off. void FinishRescan(uint64_t now) { if (g_driverOffSinceMs == 0 || now - g_driverOffSinceMs < kRescanDriverOffMs) { return; } SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_WII, "1"); g_driverOffSinceMs = 0; g_lastScanMs = now; ++g_scanCount; // The first few and then every tenth, so a remote that never comes back // leaves a trail in console.log without flooding it. if (g_scanCount <= 3 || g_scanCount % 10 == 0) { RT_LOG(RT_TAG_CONFIG) << "Wii Remote rescan #" << g_scanCount << ": HIDAPI Wii driver re-enabled (" << PADCount() << " controller(s) known to aurora)" << std::endl; } } } // namespace // Enables SDL's HIDAPI Wii driver and player LEDs, and routes SDL's input log. void ConfigureSdlHints(bool enabled) { // A rescan may be mid-flight; drop its bookkeeping so Poll() is not left // waiting for a FinishRescan() that can no longer happen. g_driverOffSinceMs = 0; if (!SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_WII, enabled ? "1" : "0")) { RT_LOG(RT_TAG_CONFIG) << "Failed to set " << SDL_HINT_JOYSTICK_HIDAPI_WII << ": " << SDL_GetError() << std::endl; } // Light the player LED that matches the SDL player index, like the console does. if (!SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_WII_PLAYER_LED, "1")) { RT_LOG(RT_TAG_CONFIG) << "Failed to set " << SDL_HINT_JOYSTICK_HIDAPI_WII_PLAYER_LED << ": " << SDL_GetError() << std::endl; } RT_LOG(RT_TAG_CONFIG) << "Bluetooth Wii Remote support " << (enabled ? "enabled" : "disabled") << std::endl; g_wiiDriverEnabled = enabled; if (enabled) { SDL_SetLogPriority(SDL_LOG_CATEGORY_INPUT, SDL_LOG_PRIORITY_DEBUG); SDL_SetLogOutputFunction(LogSdlMessage, nullptr); } } // Starts a rescan by disabling the Wii driver hint; Poll() finishes it. void RescanNow() { if (!g_wiiDriverEnabled || g_driverOffSinceMs != 0) { return; } // SDL only closes the HID handle of a remote it dropped while the Wii driver // is disabled, and only re-opens it when the driver is enabled again; both // must happen on separate joystick updates, so the hint stays at "0" until // FinishRescan() a few frames later. Flipping 1->0->1 within one frame does // nothing: SDL only ever sees the final "1". SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_WII, "0"); g_driverOffSinceMs = SDL_GetTicks(); if (g_scanCount < 3) { RT_LOG(RT_TAG_CONFIG) << "Wii Remote rescan #" << (g_scanCount + 1) << ": HIDAPI Wii driver disabled" << std::endl; } } // Per-frame scanning state machine: rescans while no Wii controller is present. // Also advances an accelerometer calibration run, which needs a sample per frame // whether or not the game is reading KPAD at that moment. void Poll() { StepAccelCalibration(); // Remember what each port had, so EffectiveKind can bridge a swap. for (uint32_t port = 0; port < PAD_MAX_CONTROLLERS; ++port) { (void)EffectiveKind(port); } WarnIfKernelDriverServing(); if (!g_wiiDriverEnabled) { return; } // Always complete a rescan in progress so the driver is never left disabled. FinishRescan(SDL_GetTicks()); if (g_driverOffSinceMs != 0) { return; } if (AnyWiiControllerConnected()) { if (g_scanning) { RT_LOG(RT_TAG_CONFIG) << "Wii Remote found after " << g_scanCount << " rescan(s)" << std::endl; } g_scanning = false; g_scanCount = 0; g_lastScanMs = SDL_GetTicks(); return; } if (!RuntimeConfigFile::WiiContinuousScanEnabled(false)) { g_scanning = false; return; } const uint64_t now = SDL_GetTicks(); if (!g_scanning) { RT_LOG(RT_TAG_CONFIG) << "No Wii Remote connected; " << (kPeriodicRescan ? "scanning for one" : "waiting for one to be paired") << " (press 1+2 on the remote)" << std::endl; g_scanning = true; g_lostAtMs = now; } if (!kPeriodicRescan || now - g_lostAtMs < kScanStartDelayMs) { return; } const uint64_t interval = now - g_lostAtMs < kFastScanWindowMs ? kFastScanIntervalMs : kScanIntervalMs; if (now - g_lastScanMs < interval) { return; } RescanNow(); } // True while Poll() is looking for a remote. bool IsScanning() { return g_scanning; } // Whether looking for a remote means periodic rescans or waiting for hotplug. bool PeriodicRescanEnabled() { return kPeriodicRescan; } // Number of rescans since a Wii controller was last seen. uint32_t ScanCount() { return g_scanCount; } // Maps the gamepad name SDL reports to a Kind: "Nintendo Wii Remote", // "... with Nunchuk", "... with Classic Controller" and "Nintendo Wii U Pro // Controller" from SDL's Wii driver; the Linux kernel's "Nintendo Wii Remote // Classic Controller" is read like the Classic Controller. Kind KindForName(const char* name) { // The kernel's Pro Controller is a plain gamepad with a built-in SDL mapping. if (NameContains(name, "Wii Remote Pro Controller")) return Kind::NotWii; if (NameContains(name, "Wii U Pro Controller")) return Kind::WiiUPro; if (!NameContains(name, "Wii Remote")) return Kind::NotWii; if (NameContains(name, "Nunchuk")) return Kind::RemoteWithNunchuk; if (NameContains(name, "Classic Controller")) return Kind::RemoteWithClassic; return Kind::Remote; } // Kind of the SDL gamepad assigned to a game port, NotWii when empty. Kind KindForPort(uint32_t port) { if (port >= PAD_MAX_CONTROLLERS) return Kind::NotWii; SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast(port)); return KindForGamepad(gamepad); } // Human-readable name of a Kind for the settings overlay. const char* KindLabel(Kind kind) { switch (kind) { case Kind::Remote: return "Wii Remote"; case Kind::RemoteWithNunchuk: return "Wii Remote + Nunchuk"; case Kind::RemoteWithClassic: return "Wii Remote + Classic Controller"; case Kind::WiiUPro: return "Wii U Pro Controller"; default: return "Not a Wii controller"; } } // True for the kinds the game reads through KPAD. static bool IsKpadKind(Kind kind) { return kind == Kind::Remote || kind == Kind::RemoteWithNunchuk || kind == Kind::RemoteWithClassic; } // Live kind of the port, or the remembered one while a swap is in flight. // Called from the guest thread only (PADRead, KPADRead, WPADProbe and the // overlay's Draw all run there), so the port memory needs no locking. Kind EffectiveKind(uint32_t chan) { if (chan >= PAD_MAX_CONTROLLERS) return Kind::NotWii; PortMemory& memory = g_ports[chan]; SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast(chan)); const Kind live = KindForGamepad(gamepad); const uint64_t now = SDL_GetTicks(); if (live != Kind::NotWii) { memory.lastKind = live; memory.lastSeenMs = now; return live; } if (gamepad != nullptr) { // Another controller took the port: the remote is not coming back here. memory.lastKind = Kind::NotWii; return Kind::NotWii; } if (IsKpadKind(memory.lastKind) && memory.lastSeenMs != 0 && now - memory.lastSeenMs < kExtensionSwapGraceMs) { return memory.lastKind; } return Kind::NotWii; } // True when the game reads the port through KPAD (live or bridging a swap). bool IsRemoteChannel(uint32_t chan) { return IsKpadKind(EffectiveKind(chan)); } // Marks KPAD-served ports as "no controller" in the GameCube pad statuses. void HideRemotesFromPad(PADStatus* statuses, uint32_t count) { for (uint32_t port = 0; port < count && port < PAD_MAX_CONTROLLERS; ++port) { if (IsRemoteChannel(port) || IsCarriedKernelNode(SDL_GetGamepadFromPlayerIndex(static_cast(port)))) { statuses[port] = {}; statuses[port].err = PAD_ERR_NO_CONTROLLER; } } } // Neutral sample of the remembered kind, for the frames of an extension swap. void FillGraceSample(uint32_t chan, Kind kind, KpadSample& sample) { sample = {}; for (int i = 0; i < 3; ++i) sample.acc[i] = g_lastAcc[chan].acc[i]; sample.hasNunchuk = kind == Kind::RemoteWithNunchuk; sample.hasClassic = kind == Kind::RemoteWithClassic; } // The patched SDL Wii driver posts Classic Controller sticks as WPAD's own // value times 64 (aurora-main/cmake/AuroraSDL3Patches.cmake), so WPADCLStatus // is the axis divided back: signed, 0 at the centre, -512..511, +y up. int16_t ClassicStickRaw(Sint16 axis, bool invert) { const long value = invert ? -static_cast(axis) : axis; return static_cast(std::clamp(std::lround(value / 64.0), -512L, 511L)); } // The kernel driver's evdev axes span SDL's full range; scaled to WPAD's. int16_t KernelClassicStickRaw(Sint16 axis, bool invert) { float value = static_cast(axis) / 32767.0f; if (invert) value = -value; value = std::clamp(value, -1.0f, 1.0f); return static_cast(std::clamp(std::lround(value * 512.0f), -512L, 511L)); } // KPAD's default Classic Controller stick limits; the game uses the cross clamp. constexpr float kKpadStickMin = 60.0f; constexpr float kKpadStickMax = 308.0f; // KPAD cross clamp: per-axis dead zone and scale, then the unit circle. void ClassicStickFloat(const int16_t raw[2], float out[2]) { for (int i = 0; i < 2; ++i) { const float magnitude = std::fabs(static_cast(raw[i])); const float scaled = std::clamp((magnitude - kKpadStickMin) / (kKpadStickMax - kKpadStickMin), 0.0f, 1.0f); out[i] = raw[i] < 0 ? -scaled : scaled; } const float length = std::sqrt(out[0] * out[0] + out[1] * out[1]); if (length > 1.0f) { out[0] /= length; out[1] /= length; } } // Samples buttons, accelerometers and the extension of the remote on a port. bool ReadKpadSample(uint32_t chan, KpadSample& sample) { if (chan >= PAD_MAX_CONTROLLERS) { return false; } SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast(chan)); const Kind kind = KindForGamepad(gamepad); if (!IsKpadKind(kind)) { const Kind remembered = EffectiveKind(chan); if (!IsKpadKind(remembered)) { return false; } FillGraceSample(chan, remembered, sample); return true; } SDL_Joystick* joystick = SDL_GetGamepadJoystick(gamepad); if (joystick == nullptr) { return false; } EnsureSensors(gamepad, chan); sample = {}; // The kernel's evdev nodes have regular SDL mappings, so they are read as gamepads. const bool kernel = IsKernelWiiNode(SDL_GetGamepadID(gamepad)); const bool classicNode = kernel && IsClassicNodeName(SDL_GetGamepadName(gamepad)); if (kernel) { const auto mapped = [&](SDL_GamepadButton button, uint32_t bit) { if (SDL_GetGamepadButton(gamepad, button)) sample.hold |= bit; }; if (!classicNode) { mapped(SDL_GAMEPAD_BUTTON_EAST, kWpadA); mapped(SDL_GAMEPAD_BUTTON_SOUTH, kWpadB); mapped(SDL_GAMEPAD_BUTTON_WEST, kWpadOne); mapped(SDL_GAMEPAD_BUTTON_NORTH, kWpadTwo); mapped(SDL_GAMEPAD_BUTTON_START, kWpadPlus); mapped(SDL_GAMEPAD_BUTTON_BACK, kWpadMinus); mapped(SDL_GAMEPAD_BUTTON_GUIDE, kWpadHome); mapped(SDL_GAMEPAD_BUTTON_DPAD_UP, kWpadUp); mapped(SDL_GAMEPAD_BUTTON_DPAD_DOWN, kWpadDown); mapped(SDL_GAMEPAD_BUTTON_DPAD_LEFT, kWpadLeft); mapped(SDL_GAMEPAD_BUTTON_DPAD_RIGHT, kWpadRight); } } else { const auto raw = [&](int index, uint32_t bit) { if (SDL_GetJoystickButton(joystick, index)) sample.hold |= bit; }; raw(kRawA, kWpadA); raw(kRawB, kWpadB); raw(kRawOne, kWpadOne); raw(kRawTwo, kWpadTwo); raw(kRawPlus, kWpadPlus); raw(kRawMinus, kWpadMinus); raw(kRawHome, kWpadHome); raw(kRawDpadUp, kWpadUp); raw(kRawDpadDown, kWpadDown); raw(kRawDpadLeft, kWpadLeft); raw(kRawDpadRight, kWpadRight); } float rawG[3] = {}; const bool haveRaw = ReadSdlAccel(gamepad, SDL_SENSOR_ACCEL, rawG); for (float& v : rawG) v /= kStandardGravity; LastAcc& last = g_lastAcc[chan]; const bool accepted = ReadAccelAsKpad(gamepad, chan, SDL_SENSOR_ACCEL, sample.acc); if (accepted) { last.valid = true; for (int i = 0; i < 3; ++i) last.acc[i] = sample.acc[i]; } else { // No sample this frame or a glitched one: repeat the last good reading // (rest pose, buttons up, until there is one). for (int i = 0; i < 3; ++i) sample.acc[i] = last.acc[i]; } if (RuntimeConfigFile::WiiAccelTraceEnabled(false)) { TraceSample(chan, sample, rawG, haveRaw, accepted); } if (kind == Kind::RemoteWithClassic) { sample.hasClassic = true; // The driver posts the extension's buttons as joystick buttons numbered // by SDL_GAMEPAD_BUTTON_*: a/b/x/y by position (a on the east), +/-, // Home, the L/R clicks as shoulders, the D-pad as buttons 11-14 (never // through SDL's gamepad mapping, which expects a hat), ZL/ZR as the // trigger axes. SDL_Gamepad* classic = gamepad; if (kernel && !classicNode) { classic = KernelSibling(gamepad, true); if (classic == nullptr) { classic = gamepad; } } const auto cl = [&](SDL_GamepadButton button, uint32_t bit) { if (kernel ? SDL_GetGamepadButton(classic, button) : SDL_GetJoystickButton(joystick, button)) { sample.clHold |= bit; } }; cl(SDL_GAMEPAD_BUTTON_EAST, kClA); cl(SDL_GAMEPAD_BUTTON_SOUTH, kClB); cl(SDL_GAMEPAD_BUTTON_NORTH, kClX); cl(SDL_GAMEPAD_BUTTON_WEST, kClY); cl(SDL_GAMEPAD_BUTTON_START, kClPlus); cl(SDL_GAMEPAD_BUTTON_BACK, kClMinus); cl(SDL_GAMEPAD_BUTTON_GUIDE, kClHome); cl(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, kClL); cl(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, kClR); cl(SDL_GAMEPAD_BUTTON_DPAD_UP, kClUp); cl(SDL_GAMEPAD_BUTTON_DPAD_DOWN, kClDown); cl(SDL_GAMEPAD_BUTTON_DPAD_LEFT, kClLeft); cl(SDL_GAMEPAD_BUTTON_DPAD_RIGHT, kClRight); if (SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > 0) sample.clHold |= kClZL; if (SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > 0) sample.clHold |= kClZR; const Sint16 lx = SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_LEFTX); const Sint16 ly = SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_LEFTY); const Sint16 rx = SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_RIGHTX); const Sint16 ry = SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_RIGHTY); if (kernel) { sample.clLStickRaw[0] = KernelClassicStickRaw(lx, false); sample.clLStickRaw[1] = KernelClassicStickRaw(ly, true); sample.clRStickRaw[0] = KernelClassicStickRaw(rx, false); sample.clRStickRaw[1] = KernelClassicStickRaw(ry, true); sample.clLStick[0] = std::clamp(static_cast(lx) / 32767.0f, -1.0f, 1.0f); sample.clLStick[1] = std::clamp(-static_cast(ly) / 32767.0f, -1.0f, 1.0f); sample.clRStick[0] = std::clamp(static_cast(rx) / 32767.0f, -1.0f, 1.0f); sample.clRStick[1] = std::clamp(-static_cast(ry) / 32767.0f, -1.0f, 1.0f); } else { sample.clLStickRaw[0] = ClassicStickRaw(lx, false); sample.clLStickRaw[1] = ClassicStickRaw(ly, true); sample.clRStickRaw[0] = ClassicStickRaw(rx, false); sample.clRStickRaw[1] = ClassicStickRaw(ry, true); ClassicStickFloat(sample.clLStickRaw, sample.clLStick); ClassicStickFloat(sample.clRStickRaw, sample.clRStick); } // Only the full-press click of L/R reaches SDL; report it as a full pull. sample.clTriggerL = (sample.clHold & kClL) ? 255 : 0; sample.clTriggerR = (sample.clHold & kClR) ? 255 : 0; } if (kind == Kind::RemoteWithNunchuk) { sample.hasNunchuk = true; if (SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER)) sample.hold |= kWpadC; if (SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > 0) sample.hold |= kWpadZ; sample.stick[0] = static_cast(SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX)) / 32767.0f; // SDL's y grows downwards; KPAD's stick y is up-positive. sample.stick[1] = -static_cast(SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY)) / 32767.0f; for (float& v : sample.stick) v = std::clamp(v, -1.0f, 1.0f); LastAcc& lastNunchuk = g_lastNunchukAcc[chan]; if (ReadAccelAsKpad(gamepad, chan, SDL_SENSOR_ACCEL_L, sample.nunchukAcc)) { lastNunchuk.valid = true; for (int i = 0; i < 3; ++i) lastNunchuk.acc[i] = sample.nunchukAcc[i]; } else { for (int i = 0; i < 3; ++i) sample.nunchukAcc[i] = lastNunchuk.acc[i]; } } return true; } // Corrected SDL sample and KPAD vector of the remote on a port, for the overlay. bool ReadAccelDebug(uint32_t chan, float sdlG[3], float kpadAcc[3]) { if (!IsRemoteChannel(chan)) { return false; } SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast(chan)); if (gamepad == nullptr) { return false; } EnsureSensors(gamepad, chan); if (!ReadAccelG(gamepad, chan, SDL_SENSOR_ACCEL, sdlG)) { return false; } AccelGToKpad(sdlG, kpadAcc); return true; } // Begins collecting rest samples from the remote on `chan`. void StartAccelCalibration(uint32_t chan) { SDL_Gamepad* gamepad = RemoteGamepad(chan); if (gamepad == nullptr) { FinishAccelCalibration(chan, "No Wii Remote on this port."); return; } g_calibration = {}; g_calibration.active = true; g_calibration.chan = chan; g_calibration.instance = SDL_GetGamepadID(gamepad); g_calibrationMessage[0] = '\0'; } // Zeroes the stored correction of the remote on `chan`. void ClearAccelCalibration(uint32_t chan) { if (g_calibration.chan == chan) { g_calibration.active = false; } SDL_Gamepad* gamepad = RemoteGamepad(chan); if (gamepad == nullptr) { SetCalibrationMessage(chan, "No Wii Remote on this port."); return; } RemoteCalibration& calibration = CalibrationFor(gamepad, chan); calibration.offset = {}; RuntimeConfigFile::SetWiiAccelOffsetFor(calibration.key, {0.0, 0.0, 0.0}); SetCalibrationMessage(chan, "Calibration cleared."); } // Correction in effect for the remote on `chan`, for the overlay. std::optional> ReadAccelOffset(uint32_t chan) { SDL_Gamepad* gamepad = RemoteGamepad(chan); if (gamepad == nullptr) { return std::nullopt; } const RemoteCalibration& calibration = CalibrationFor(gamepad, chan); return std::array{calibration.offset[0], calibration.offset[1], calibration.offset[2]}; } // True while a calibration run is collecting samples from `chan`. bool IsAccelCalibrating(uint32_t chan) { return g_calibration.active && g_calibration.chan == chan; } // Share of the calibration samples collected so far, 0..1; 0 when idle. float AccelCalibrationProgress() { return g_calibration.active ? static_cast(g_calibration.count) / kCalibrationSamples : 0.0f; } // Outcome of the last calibration run on `chan` for the overlay, or nullptr. const char* AccelCalibrationMessage(uint32_t chan) { if (g_calibrationMessage[0] == '\0' || g_calibrationMessageChan != chan) { return nullptr; } SDL_Gamepad* gamepad = RemoteGamepad(chan); const SDL_JoystickID remote = gamepad != nullptr ? SDL_GetGamepadID(gamepad) : 0; return remote == g_calibrationMessageRemote ? g_calibrationMessage : nullptr; } } // namespace WiiRemoteInput