mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-26 07:01:08 -04:00
785 lines
32 KiB
C++
785 lines
32 KiB
C++
#include "wii_remote_input.h"
|
|
|
|
#include "runtime_config.h"
|
|
#include "runtime_log.h"
|
|
|
|
#include <dolphin/pad.h>
|
|
#include <SDL3/SDL_gamepad.h>
|
|
#include <SDL3/SDL_hints.h>
|
|
#include <SDL3/SDL_log.h>
|
|
#include <SDL3/SDL_sensor.h>
|
|
#include <SDL3/SDL_timer.h>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <string>
|
|
#include <unordered_set>
|
|
|
|
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;
|
|
|
|
// 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<PortMemory, PAD_MAX_CONTROLLERS> 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;
|
|
|
|
// 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<SDL_JoystickID, PAD_MAX_CONTROLLERS> g_sensorsEnabledFor{};
|
|
|
|
// Zero-point correction subtracted from the remote's accelerometer, in g and in
|
|
// SDL's sensor frame; loaded from Config.toml on first use, replaced by a
|
|
// calibration run. The Nunchuk accelerometer is left uncorrected.
|
|
std::array<float, 3> g_accelOffset{};
|
|
bool g_accelOffsetLoaded = false;
|
|
|
|
// 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;
|
|
// |mean| outside this range means the remote was not at rest or the
|
|
// accelerometer is far off its nominal scale; either way the offset is useless.
|
|
constexpr float kCalibrationMinGravityG = 0.8f;
|
|
constexpr float kCalibrationMaxGravityG = 1.2f;
|
|
|
|
struct AccelCalibration {
|
|
bool active = false;
|
|
uint32_t chan = 0;
|
|
int count = 0;
|
|
double sum[3] = {};
|
|
float first[3] = {};
|
|
};
|
|
AccelCalibration g_calibration;
|
|
char g_calibrationMessage[160] = {};
|
|
|
|
// 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<LastAcc, PAD_MAX_CONTROLLERS> g_lastAcc{};
|
|
std::array<LastAcc, PAD_MAX_CONTROLLERS> 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;
|
|
}
|
|
}
|
|
|
|
// Loads the stored zero-point correction once.
|
|
const std::array<float, 3>& AccelOffset() {
|
|
if (!g_accelOffsetLoaded) {
|
|
const std::array<double, 3> stored = RuntimeConfigFile::WiiAccelOffset();
|
|
for (size_t i = 0; i < 3; ++i) g_accelOffset[i] = static_cast<float>(stored[i]);
|
|
g_accelOffsetLoaded = true;
|
|
}
|
|
return g_accelOffset;
|
|
}
|
|
|
|
// 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; 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, 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<float, 3>& offset = AccelOffset();
|
|
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, SDL_SensorType sensor, float* kpad) {
|
|
float g[3] = {};
|
|
if (!ReadAccelG(gamepad, 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<unsigned long long>(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<unsigned long long>(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();
|
|
}
|
|
|
|
// Ends a calibration run with a message for the overlay.
|
|
void FinishAccelCalibration(const char* message) {
|
|
g_calibration.active = false;
|
|
std::snprintf(g_calibrationMessage, sizeof(g_calibrationMessage), "%s", 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 = SDL_GetGamepadFromPlayerIndex(static_cast<int>(g_calibration.chan));
|
|
if (gamepad == nullptr || !IsRemoteChannel(g_calibration.chan)) {
|
|
FinishAccelCalibration("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("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<double, 3> 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] < 0.5) {
|
|
FinishAccelCalibration("Failed: the remote was not resting flat with the buttons up.");
|
|
return;
|
|
}
|
|
const std::array<double, 3> offset = {mean[0], mean[1] - 1.0, mean[2]};
|
|
for (int i = 0; i < 3; ++i) g_accelOffset[i] = static_cast<float>(offset[i]);
|
|
g_accelOffsetLoaded = true;
|
|
const bool saved = RuntimeConfigFile::SetWiiAccelOffset(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(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;
|
|
}
|
|
|
|
// 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<std::string> 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);
|
|
}
|
|
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's Wii driver reports to a Kind.
|
|
Kind KindForName(const char* name) {
|
|
// Names come from SDL's hidapi Wii driver: "Nintendo Wii Remote",
|
|
// "Nintendo Wii Remote with Nunchuk", "Nintendo Wii Remote with Classic
|
|
// Controller" and "Nintendo Wii U Pro Controller".
|
|
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<int>(port));
|
|
if (gamepad == nullptr) return Kind::NotWii;
|
|
return KindForName(SDL_GetGamepadName(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<int>(chan));
|
|
const Kind live = gamepad != nullptr ? KindForName(SDL_GetGamepadName(gamepad)) : Kind::NotWii;
|
|
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)) {
|
|
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;
|
|
}
|
|
|
|
// SDL's stick axis (-32767..32767, y down) as WPADCLStatus carries it: WPAD
|
|
// normalises every Classic Controller stick, whatever the report's resolution,
|
|
// to a signed 10-bit value, -512..511 with 0 at the centre and +y up (RVL SDK
|
|
// WPAD.h; wut's WPADStatusClassic documents the same range).
|
|
int16_t ClassicStickRaw(Sint16 axis, bool invert) {
|
|
float value = static_cast<float>(axis) / 32767.0f;
|
|
if (invert) value = -value;
|
|
value = std::clamp(value, -1.0f, 1.0f);
|
|
return static_cast<int16_t>(std::clamp(std::lround(value * 512.0f), -512L, 511L));
|
|
}
|
|
|
|
// 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<int>(chan));
|
|
const Kind kind = gamepad != nullptr ? KindForName(SDL_GetGamepadName(gamepad)) : Kind::NotWii;
|
|
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 = {};
|
|
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, 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.
|
|
const auto cl = [&](int index, uint32_t bit) {
|
|
if (SDL_GetJoystickButton(joystick, index)) 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(gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > 0) sample.clHold |= kClZL;
|
|
if (SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > 0) sample.clHold |= kClZR;
|
|
const Sint16 lx = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX);
|
|
const Sint16 ly = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY);
|
|
const Sint16 rx = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTX);
|
|
const Sint16 ry = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTY);
|
|
sample.clLStick[0] = std::clamp(static_cast<float>(lx) / 32767.0f, -1.0f, 1.0f);
|
|
sample.clLStick[1] = std::clamp(-static_cast<float>(ly) / 32767.0f, -1.0f, 1.0f);
|
|
sample.clRStick[0] = std::clamp(static_cast<float>(rx) / 32767.0f, -1.0f, 1.0f);
|
|
sample.clRStick[1] = std::clamp(-static_cast<float>(ry) / 32767.0f, -1.0f, 1.0f);
|
|
sample.clLStickRaw[0] = ClassicStickRaw(lx, false);
|
|
sample.clLStickRaw[1] = ClassicStickRaw(ly, true);
|
|
sample.clRStickRaw[0] = ClassicStickRaw(rx, false);
|
|
sample.clRStickRaw[1] = ClassicStickRaw(ry, true);
|
|
// 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<float>(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<float>(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, 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<int>(chan));
|
|
if (gamepad == nullptr) {
|
|
return false;
|
|
}
|
|
EnsureSensors(gamepad, chan);
|
|
if (!ReadAccelG(gamepad, 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) {
|
|
if (!IsRemoteChannel(chan)) {
|
|
FinishAccelCalibration("No Wii Remote on this port.");
|
|
return;
|
|
}
|
|
g_calibration = {};
|
|
g_calibration.active = true;
|
|
g_calibration.chan = chan;
|
|
g_calibrationMessage[0] = '\0';
|
|
}
|
|
|
|
// Drops the stored correction and goes back to SDL's raw reading.
|
|
void ClearAccelCalibration() {
|
|
g_calibration.active = false;
|
|
g_accelOffset = {};
|
|
g_accelOffsetLoaded = true;
|
|
RuntimeConfigFile::SetWiiAccelOffset({0.0, 0.0, 0.0});
|
|
std::snprintf(g_calibrationMessage, sizeof(g_calibrationMessage), "Calibration cleared.");
|
|
}
|
|
|
|
// True while a calibration run is collecting samples.
|
|
bool IsAccelCalibrating() {
|
|
return g_calibration.active;
|
|
}
|
|
|
|
// Share of the calibration samples collected so far, 0..1; 0 when idle.
|
|
float AccelCalibrationProgress() {
|
|
return g_calibration.active ? static_cast<float>(g_calibration.count) / kCalibrationSamples : 0.0f;
|
|
}
|
|
|
|
// Outcome of the last calibration run for the overlay, or nullptr before any.
|
|
const char* AccelCalibrationMessage() {
|
|
return g_calibrationMessage[0] != '\0' ? g_calibrationMessage : nullptr;
|
|
}
|
|
|
|
} // namespace WiiRemoteInput
|