mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-10-20 13:36:47 -04:00
Support Linux Wii controller mappings (#309)
* Support Linux Wii controller mappings * read kernel remote buttons and pair classic node
This commit is contained in:
@@ -798,6 +798,12 @@ void DrawControllerSettings() {
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
if (WiiRemoteInput::IsRemoteChannel(selectedGamePort)) {
|
||||
ImGui::TextDisabled("The game reads this controller as a Wii Remote, so the button mapping does not apply.");
|
||||
DrawRumbleSettings();
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t mappingCount = 0;
|
||||
PADButtonMapping* mappings = PADGetButtonMappings(static_cast<uint32_t>(g_controllerPort), &mappingCount);
|
||||
if (mappings == nullptr || mappingCount != PAD_BUTTON_COUNT) {
|
||||
|
||||
@@ -17,9 +17,12 @@
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace WiiRemoteInput {
|
||||
@@ -66,6 +69,8 @@ constexpr uint32_t kClUp = 0x0001, kClLeft = 0x0002, kClZR = 0x0004, kClX = 0x00
|
||||
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.
|
||||
@@ -89,6 +94,7 @@ bool g_scanning = false;
|
||||
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
|
||||
@@ -435,6 +441,110 @@ bool AnyWiiControllerConnected() {
|
||||
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<SDL_JoystickID, std::string> 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
|
||||
@@ -553,6 +663,7 @@ void Poll() {
|
||||
for (uint32_t port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
|
||||
(void)EffectiveKind(port);
|
||||
}
|
||||
WarnIfKernelDriverServing();
|
||||
if (!g_wiiDriverEnabled) {
|
||||
return;
|
||||
}
|
||||
@@ -607,11 +718,13 @@ uint32_t ScanCount() {
|
||||
return g_scanCount;
|
||||
}
|
||||
|
||||
// Maps the gamepad name SDL's Wii driver reports to a Kind.
|
||||
// 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) {
|
||||
// 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".
|
||||
// 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;
|
||||
@@ -623,8 +736,7 @@ Kind KindForName(const char* name) {
|
||||
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));
|
||||
return KindForGamepad(gamepad);
|
||||
}
|
||||
|
||||
// Human-readable name of a Kind for the settings overlay.
|
||||
@@ -650,7 +762,7 @@ 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 Kind live = KindForGamepad(gamepad);
|
||||
const uint64_t now = SDL_GetTicks();
|
||||
if (live != Kind::NotWii) {
|
||||
memory.lastKind = live;
|
||||
@@ -676,7 +788,8 @@ bool IsRemoteChannel(uint32_t 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)) {
|
||||
if (IsRemoteChannel(port) ||
|
||||
IsCarriedKernelNode(SDL_GetGamepadFromPlayerIndex(static_cast<int>(port)))) {
|
||||
statuses[port] = {};
|
||||
statuses[port].err = PAD_ERR_NO_CONTROLLER;
|
||||
}
|
||||
@@ -699,6 +812,14 @@ int16_t ClassicStickRaw(Sint16 axis, bool invert) {
|
||||
return static_cast<int16_t>(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<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));
|
||||
}
|
||||
|
||||
// KPAD's default Classic Controller stick limits; the game uses the cross clamp.
|
||||
constexpr float kKpadStickMin = 60.0f;
|
||||
constexpr float kKpadStickMax = 308.0f;
|
||||
@@ -723,7 +844,7 @@ bool ReadKpadSample(uint32_t chan, KpadSample& sample) {
|
||||
return false;
|
||||
}
|
||||
SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(chan));
|
||||
const Kind kind = gamepad != nullptr ? KindForName(SDL_GetGamepadName(gamepad)) : Kind::NotWii;
|
||||
const Kind kind = KindForGamepad(gamepad);
|
||||
if (!IsKpadKind(kind)) {
|
||||
const Kind remembered = EffectiveKind(chan);
|
||||
if (!IsKpadKind(remembered)) {
|
||||
@@ -739,20 +860,42 @@ bool ReadKpadSample(uint32_t chan, KpadSample& sample) {
|
||||
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);
|
||||
// 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);
|
||||
@@ -778,8 +921,17 @@ bool ReadKpadSample(uint32_t chan, KpadSample& sample) {
|
||||
// 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;
|
||||
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);
|
||||
@@ -794,18 +946,29 @@ bool ReadKpadSample(uint32_t chan, KpadSample& sample) {
|
||||
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.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);
|
||||
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<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);
|
||||
} 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;
|
||||
|
||||
Reference in New Issue
Block a user