mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-11 09:25:05 -04:00
Bluetooth Wii Remote support: Wii Remote / Wii Wheel, Nunchuk and Classic Controller through KPAD (#73)
* Bluetooth Wii Remote support: the game reads a real Wii Remote through KPAD Enable SDL3's HIDAPI Wii driver and hand a paired Wii Remote (bare or with Nunchuk) to the game as a real Wii Remote: WPADProbe reports CORE/FREESTYLE and KPADRead fills KPADStatus[0] from SDL every frame (buttons, accelerometer in KPAD's g frame, Nunchuk stick and accelerometer), while the GameCube pad view of that port reports no controller. The game's own motion code then handles wheelies, tricks and Wii Wheel steering. Classic Controllers and Wii U Pro Controllers keep going through the GameCube pad path with a default button table picked by name. SDL's Wii driver drops a remote on a failed Bluetooth read or when the Nunchuk is plugged or unplugged and never re-adds it, so the runtime keeps rescanning (Dolphin style) while no Wii controller is present by toggling the driver hint off and, a few frames later, on again; a dropped remote is back within 1-2 s. Settings live in the F10 overlay under Wii Remotes (Bluetooth) and in Config.toml (wii_remotes, wii_continuous_scan). * Fix Wii U Pro / Classic Controller ZL and ZR not registering SDL's Wii driver reports ZL/ZR as the LEFT_TRIGGER/RIGHT_TRIGGER analog axes, never as digital shoulder buttons. Binding them to LEFT_SHOULDER/RIGHT_SHOULDER meant they never fired and also disabled aurora's own analog-trigger fallback (a button table entry for PAD_TRIGGER_L/R marks the trigger as "handled", even when the bound digital button never actually presses). Leaving them unbound lets the default axis mapping drive them like every other analog-trigger pad. Reported by an end-to-end tester connecting a real Classic Controller to a Wii Remote. * Wii Remotes menu: live raw D-pad/ZL/ZR readout for Classic Controller / Wii U Pro Diagnostic aid for a reported issue where the Classic Controller's D-pad does not do anything in-game (no wheelies). Shows what SDL itself sees so a driver-level problem (nothing lights up) can be told apart from a mapping problem (it lights up but the game does not react). * Fix Classic Controller D-pad input * Address CodeRabbit review on PR #73 - PADRead: hide KPAD-served ports even while input is blocked so the port error state does not flip when the overlay opens/closes. - WPADProbe: run the Wii Remote rescan state machine before probing so a reconnect probe before the next PADRead can see the remote. - EnsureSensors: only cache the gamepad id once every accelerometer enabled, so a failed activation is retried. - ConfigureSdlHints: reset the in-flight rescan bookkeeping. - Settings overlay: disable "Rescan now" while Wii Remotes are turned off. * Bluetooth Wii Remote: fix wheel steering, native Classic Controller, extension hot-swap Accelerometer - The SDL -> KPAD conversion negated the wrong axis: SDL's z is the remote's +Y (towards the user), so KPAD acc is (-wiiX, -wiiZ, +wiiY). Fixes mirrored Wii Wheel steering. - Drop reports whose accelerometer bytes arrive zeroed (+-5.12 g on every axis, a few times a minute over Bluetooth) and repeat the last good sample; they read as a full-lock steer plus a 9 g shake. - One-button zero-point calibration in the overlay (remote flat, buttons up), stored in Config.toml as wii_accel_offset_x/y/z. SDL's read of the remote's factory calibration times out over Bluetooth and falls back to a nominal zero point, which left a per-axis bias of up to ~0.3 g on the tested remote. - Live accelerometer readout and an optional per-frame CSV trace (wii_accel_trace = true) for debugging. Classic Controller through KPAD/WPAD - WPADProbe reports WPAD_DEV_CLASSIC; KPADRead fills ex_status.cl and KPADGetUnifiedWpadStatus the raw WPADCLStatus (WPAD_CL_BUTTON_* bits, sticks in the SDK's signed -512..511 range, triggers), so the game shows the Classic layout and icons and no button mapping is involved. Ports served through KPAD are hidden from PADRead; only the Wii U Pro Controller stays a GameCube pad. Extension hot-swap - SDL's Wii driver destroys the joystick on an extension change but keeps the HID handle open, and HIDAPI never re-creates a joystick for such a device. Patch the vendored SDL at configure time (AuroraSDL3Patches.cmake, wired into AuroraSDL3Provider.cmake for both the downloaded tarball and a pre-provided FETCHCONTENT_SOURCE_DIR_SDL) so the joystick is rebuilt in place with the new extension type, without touching the Bluetooth handle. - Keep a vanished remote's channel alive with neutral input for up to 3 s while SDL re-creates the joystick, so the game never sees a disconnection. The driver-hint rescan stays as a fallback for real drops, starting 3 s after the loss, and also runs from the overlay's per-frame Draw. Log rescans. Mappings / overlay - Do not apply the shared positional [controller] bindings to Wii pads: that override is what made a Classic Controller's A/B and X/Y look swapped. - Raw D-pad fallback also for the Wii U Pro Controller; overlay readouts read joystick buttons directly (SDL's generated HIDAPI mapping expects a hat). - Overlay: Classic Controller readout, accelerometer readout and calibration. - README: Bluetooth Wii Remote section and known limitations. * Review pass on the Wii Remote input path - EffectiveKind: stop bridging an extension swap once a different controller has taken the port, and note that everything touching the scanner state runs on the guest thread. - KPADGetUnifiedWpadStatus: fill every requested entry (the SDK returns `count` recent samples), capped at KPAD's 16 read buffers. - IsKpadKind gets internal linkage; the calibration accessors get their comments; clarify why Draw() also runs Poll(). * Drop the dead Classic-Controller-as-GameCube-pad matching A Wii Remote with a Classic Controller is served through KPAD and its port is hidden from PADRead, so the name matches that once gave it a GameCube button table and the raw D-pad fallback could never take effect any more. Both now match only the Wii U Pro Controller, and the default table is renamed accordingly (g_defaultButtonsWiiUPro). --------- Co-authored-by: LOL <andresguerra2k26@gmail.com> Co-authored-by: Nick <89667145+Nick1232345@users.noreply.github.com>
This commit is contained in:
@@ -1,23 +1,249 @@
|
||||
#include "hle_stubs.h"
|
||||
#include "memory.h"
|
||||
#include "wii_remote_input.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
// KPAD HLE fed by a real Bluetooth Wii Remote. The game calls KPADRead once per
|
||||
// frame with room for 16 KPADStatus entries and only looks at entry 0; with a
|
||||
// Classic Controller it also calls KPADGetUnifiedWpadStatus for the raw
|
||||
// WPADCLStatus (buttons, sticks and triggers of the extension).
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kKpadStatusSize = 0x84;
|
||||
|
||||
// KPADStatus field offsets (RVL SDK).
|
||||
constexpr uint32_t kHold = 0x00, kTrig = 0x04, kRelease = 0x08, kAcc = 0x0C, kAccValue = 0x18,
|
||||
kAccSpeed = 0x1C, kPos = 0x20, kAccVertical = 0x54, kDevType = 0x5C, kWpadErr = 0x5D,
|
||||
kDpdValidFg = 0x5E, kDataFormat = 0x5F, kFsStick = 0x60, kFsAcc = 0x68, kFsAccValue = 0x74,
|
||||
kFsAccSpeed = 0x78;
|
||||
// KPADStatus.ex_status.cl (KPADEXStatus, Classic Controller view).
|
||||
constexpr uint32_t kClHold = 0x60, kClTrig = 0x64, kClRelease = 0x68, kClLStick = 0x6C, kClRStick = 0x74,
|
||||
kClLTrigger = 0x7C, kClRTrigger = 0x80;
|
||||
|
||||
// KPADUnifiedWpadStatus: WPADStatus / WPADFSStatus / WPADCLStatus union, then fmt.
|
||||
constexpr uint32_t kUnifiedSize = 0x38;
|
||||
constexpr uint32_t kUButton = 0x00, kUAccX = 0x02, kUAccY = 0x04, kUAccZ = 0x06, kUObj = 0x08, kUDev = 0x28,
|
||||
kUErr = 0x29, kUFsStickX = 0x2A, kUFsStickY = 0x2B, kUFsAccX = 0x2C, kUFsAccY = 0x2E,
|
||||
kUFsAccZ = 0x30, kUClButton = 0x2A, kUClLStickX = 0x2C, kUClLStickY = 0x2E, kUClRStickX = 0x30,
|
||||
kUClRStickY = 0x32, kUClTriggerL = 0x34, kUClTriggerR = 0x35, kUFmt = 0x36;
|
||||
|
||||
// WPAD device types (WPAD_DEV_*) and the data formats KPAD runs each of them
|
||||
// in (WPAD_FMT_*_ACC_DPD): the values KPADStatus.dev_type / data_format and
|
||||
// KPADUnifiedWpadStatus.dev / fmt carry on the console.
|
||||
constexpr uint8_t kDevCore = 0;
|
||||
constexpr uint8_t kDevFreestyle = 1;
|
||||
constexpr uint8_t kDevClassic = 2;
|
||||
constexpr uint8_t kFmtCoreAccDpd = 2;
|
||||
constexpr uint8_t kFmtFreestyleAccDpd = 5;
|
||||
constexpr uint8_t kFmtClassicAccDpd = 8;
|
||||
constexpr int8_t kWpadErrNone = 0;
|
||||
constexpr int8_t kWpadErrNoController = -1;
|
||||
|
||||
// Raw accelerometer as WPADStatus carries it: 10 bits, 0x200 at 0 g, 100 per g.
|
||||
constexpr float kRawAccZero = 512.0f;
|
||||
constexpr float kRawAccPerG = 100.0f;
|
||||
|
||||
struct ChannelState {
|
||||
uint32_t prevHold = 0;
|
||||
uint32_t prevClHold = 0;
|
||||
float prevAcc[3] = {0.0f, -1.0f, 0.0f};
|
||||
float prevFsAcc[3] = {0.0f, -1.0f, 0.0f};
|
||||
};
|
||||
|
||||
std::array<ChannelState, 4> g_channels{};
|
||||
|
||||
// Euclidean length of a 3-vector.
|
||||
float Length(const float* v) {
|
||||
return std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
|
||||
}
|
||||
|
||||
// Euclidean distance between two 3-vectors.
|
||||
float Distance(const float* a, const float* b) {
|
||||
const float d[3] = {a[0] - b[0], a[1] - b[1], a[2] - b[2]};
|
||||
return Length(d);
|
||||
}
|
||||
|
||||
// Writes three big-endian floats to guest memory.
|
||||
void WriteVec3(uint32_t addr, const float* v) {
|
||||
Memory::WriteFloat32(addr, v[0]);
|
||||
Memory::WriteFloat32(addr + 4, v[1]);
|
||||
Memory::WriteFloat32(addr + 8, v[2]);
|
||||
}
|
||||
|
||||
// Zeroes `count` consecutive floats in guest memory.
|
||||
void WriteZeroFloats(uint32_t addr, uint32_t count) {
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
Memory::WriteFloat32(addr + i * 4, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
// Fills one KPADStatus at `addr` and returns the number of valid entries (1),
|
||||
// or writes an "unplugged" status and returns 0.
|
||||
int32_t WriteStatus(uint32_t chan, uint32_t addr, const WiiRemoteInput::KpadSample* sample) {
|
||||
ChannelState& state = g_channels[chan];
|
||||
if (sample == nullptr) {
|
||||
state = {};
|
||||
Memory::Write32(addr + kHold, 0);
|
||||
Memory::Write32(addr + kTrig, 0);
|
||||
Memory::Write32(addr + kRelease, 0);
|
||||
Memory::Write8(addr + kDevType, kDevCore);
|
||||
Memory::Write8(addr + kWpadErr, static_cast<uint8_t>(kWpadErrNoController));
|
||||
Memory::Write8(addr + kDpdValidFg, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint32_t hold = sample->hold;
|
||||
Memory::Write32(addr + kHold, hold);
|
||||
Memory::Write32(addr + kTrig, hold & ~state.prevHold);
|
||||
Memory::Write32(addr + kRelease, state.prevHold & ~hold);
|
||||
state.prevHold = hold;
|
||||
|
||||
WriteVec3(addr + kAcc, sample->acc);
|
||||
Memory::WriteFloat32(addr + kAccValue, Length(sample->acc));
|
||||
Memory::WriteFloat32(addr + kAccSpeed, Distance(sample->acc, state.prevAcc));
|
||||
for (int i = 0; i < 3; ++i) state.prevAcc[i] = sample->acc[i];
|
||||
|
||||
// No IR pointer: pos .. acc_vertical zeroed and dpd_valid_fg clear, which
|
||||
// the game treats as "pointing away from the screen".
|
||||
WriteZeroFloats(addr + kPos, (kAccVertical + 8 - kPos) / 4);
|
||||
Memory::Write8(addr + kDpdValidFg, 0);
|
||||
|
||||
const uint8_t devType = sample->hasClassic ? kDevClassic : sample->hasNunchuk ? kDevFreestyle : kDevCore;
|
||||
const uint8_t dataFormat =
|
||||
sample->hasClassic ? kFmtClassicAccDpd : sample->hasNunchuk ? kFmtFreestyleAccDpd : kFmtCoreAccDpd;
|
||||
Memory::Write8(addr + kDevType, devType);
|
||||
Memory::Write8(addr + kWpadErr, static_cast<uint8_t>(kWpadErrNone));
|
||||
Memory::Write8(addr + kDataFormat, dataFormat);
|
||||
|
||||
if (sample->hasClassic) {
|
||||
const uint32_t clHold = sample->clHold;
|
||||
Memory::Write32(addr + kClHold, clHold);
|
||||
Memory::Write32(addr + kClTrig, clHold & ~state.prevClHold);
|
||||
Memory::Write32(addr + kClRelease, state.prevClHold & ~clHold);
|
||||
state.prevClHold = clHold;
|
||||
Memory::WriteFloat32(addr + kClLStick, sample->clLStick[0]);
|
||||
Memory::WriteFloat32(addr + kClLStick + 4, sample->clLStick[1]);
|
||||
Memory::WriteFloat32(addr + kClRStick, sample->clRStick[0]);
|
||||
Memory::WriteFloat32(addr + kClRStick + 4, sample->clRStick[1]);
|
||||
Memory::WriteFloat32(addr + kClLTrigger, sample->clTriggerL / 255.0f);
|
||||
Memory::WriteFloat32(addr + kClRTrigger, sample->clTriggerR / 255.0f);
|
||||
} else if (sample->hasNunchuk) {
|
||||
state.prevClHold = 0;
|
||||
Memory::WriteFloat32(addr + kFsStick, sample->stick[0]);
|
||||
Memory::WriteFloat32(addr + kFsStick + 4, sample->stick[1]);
|
||||
WriteVec3(addr + kFsAcc, sample->nunchukAcc);
|
||||
Memory::WriteFloat32(addr + kFsAccValue, Length(sample->nunchukAcc));
|
||||
Memory::WriteFloat32(addr + kFsAccSpeed, Distance(sample->nunchukAcc, state.prevFsAcc));
|
||||
for (int i = 0; i < 3; ++i) state.prevFsAcc[i] = sample->nunchukAcc[i];
|
||||
} else {
|
||||
state.prevClHold = 0;
|
||||
WriteZeroFloats(addr + kFsStick, (kKpadStatusSize - kFsStick) / 4);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// One accelerometer axis of KPAD's g vector back to the 10-bit raw WPAD value.
|
||||
uint16_t RawAcc(float g) {
|
||||
const float raw = kRawAccZero + g * kRawAccPerG;
|
||||
return static_cast<uint16_t>(std::clamp(raw, 0.0f, 1023.0f));
|
||||
}
|
||||
|
||||
// Fills one KPADUnifiedWpadStatus at `addr` from the sample: the WPADStatus core
|
||||
// (remote buttons, raw accelerometer, no IR objects), then the Nunchuk or Classic
|
||||
// Controller tail, then the data format.
|
||||
void WriteUnifiedStatus(uint32_t addr, const WiiRemoteInput::KpadSample* sample) {
|
||||
for (uint32_t offset = 0; offset < kUnifiedSize; offset += 4) {
|
||||
Memory::Write32(addr + offset, 0);
|
||||
}
|
||||
if (sample == nullptr) {
|
||||
Memory::Write8(addr + kUDev, kDevCore);
|
||||
Memory::Write8(addr + kUErr, static_cast<uint8_t>(kWpadErrNoController));
|
||||
Memory::Write8(addr + kUFmt, kFmtCoreAccDpd);
|
||||
return;
|
||||
}
|
||||
Memory::Write16(addr + kUButton, static_cast<uint16_t>(sample->hold & 0xFFFF));
|
||||
// KPAD's acc is (-wiiX, -wiiZ, wiiY); WPADStatus keeps the remote's own axes.
|
||||
Memory::Write16(addr + kUAccX, RawAcc(-sample->acc[0]));
|
||||
Memory::Write16(addr + kUAccY, RawAcc(sample->acc[2]));
|
||||
Memory::Write16(addr + kUAccZ, RawAcc(-sample->acc[1]));
|
||||
// No IR: every DPDObject invalid (x/y at the sensor's out-of-range value).
|
||||
for (uint32_t i = 0; i < 4; ++i) {
|
||||
Memory::Write16(addr + kUObj + i * 8, 0x3FF);
|
||||
Memory::Write16(addr + kUObj + i * 8 + 2, 0x3FF);
|
||||
}
|
||||
Memory::Write8(addr + kUErr, static_cast<uint8_t>(kWpadErrNone));
|
||||
if (sample->hasClassic) {
|
||||
Memory::Write8(addr + kUDev, kDevClassic);
|
||||
Memory::Write16(addr + kUClButton, static_cast<uint16_t>(sample->clHold & 0xFFFF));
|
||||
Memory::Write16(addr + kUClLStickX, static_cast<uint16_t>(sample->clLStickRaw[0]));
|
||||
Memory::Write16(addr + kUClLStickY, static_cast<uint16_t>(sample->clLStickRaw[1]));
|
||||
Memory::Write16(addr + kUClRStickX, static_cast<uint16_t>(sample->clRStickRaw[0]));
|
||||
Memory::Write16(addr + kUClRStickY, static_cast<uint16_t>(sample->clRStickRaw[1]));
|
||||
Memory::Write8(addr + kUClTriggerL, sample->clTriggerL);
|
||||
Memory::Write8(addr + kUClTriggerR, sample->clTriggerR);
|
||||
Memory::Write8(addr + kUFmt, kFmtClassicAccDpd);
|
||||
} else if (sample->hasNunchuk) {
|
||||
Memory::Write8(addr + kUDev, kDevFreestyle);
|
||||
// WPADFSStatus: 8-bit stick (centre 128) and 10-bit Nunchuk accelerometer.
|
||||
Memory::Write8(addr + kUFsStickX,
|
||||
static_cast<uint8_t>(std::clamp(128.0f + sample->stick[0] * 100.0f, 0.0f, 255.0f)));
|
||||
Memory::Write8(addr + kUFsStickY,
|
||||
static_cast<uint8_t>(std::clamp(128.0f + sample->stick[1] * 100.0f, 0.0f, 255.0f)));
|
||||
Memory::Write16(addr + kUFsAccX, RawAcc(-sample->nunchukAcc[0]));
|
||||
Memory::Write16(addr + kUFsAccY, RawAcc(sample->nunchukAcc[2]));
|
||||
Memory::Write16(addr + kUFsAccZ, RawAcc(-sample->nunchukAcc[1]));
|
||||
Memory::Write8(addr + kUFmt, kFmtFreestyleAccDpd);
|
||||
} else {
|
||||
Memory::Write8(addr + kUDev, kDevCore);
|
||||
Memory::Write8(addr + kUFmt, kFmtCoreAccDpd);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// KPADRead: fills KPADStatus[0] for `chan` from the Bluetooth remote, returns the entry count.
|
||||
extern "C" int32_t KPAD__Read_HLE(uint32_t chan, uint32_t statusPtr, uint32_t count)
|
||||
{
|
||||
(void)chan;
|
||||
(void)statusPtr;
|
||||
(void)count;
|
||||
return 0;
|
||||
if (chan >= g_channels.size() || statusPtr == 0 || count == 0) {
|
||||
return 0;
|
||||
}
|
||||
WiiRemoteInput::KpadSample sample;
|
||||
const bool have = WiiRemoteInput::ReadKpadSample(chan, sample);
|
||||
try {
|
||||
return WriteStatus(chan, statusPtr, have ? &sample : nullptr);
|
||||
} catch (const Memory::AccessViolation&) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(80197380, KPAD__Read_HLE, int32_t, (uint32_t chan, uint32_t statusPtr, uint32_t count),
|
||||
(chan, statusPtr, count));
|
||||
|
||||
// KPADGetUnifiedWpadStatus: the raw WPAD status behind KPADStatus. The game
|
||||
// reads the Classic Controller's buttons, sticks and triggers from here. The
|
||||
// SDK fills `count` entries with the channel's recent samples (the game asks for
|
||||
// as many as it asked KPADRead for and looks at entry 0); with one sample per
|
||||
// frame here, every entry gets the current one.
|
||||
extern "C" int32_t KPAD__GetUnifiedWpadStatus_HLE(uint32_t chan, uint32_t statusPtr, uint32_t count)
|
||||
{
|
||||
(void)chan;
|
||||
(void)statusPtr;
|
||||
(void)count;
|
||||
return 0;
|
||||
constexpr uint32_t kMaxEntries = 16; // KPAD_MAX_READ_BUFS
|
||||
if (chan >= g_channels.size() || statusPtr == 0 || count == 0) {
|
||||
return 0;
|
||||
}
|
||||
WiiRemoteInput::KpadSample sample;
|
||||
const bool have = WiiRemoteInput::ReadKpadSample(chan, sample);
|
||||
try {
|
||||
const uint32_t entries = std::min(count, kMaxEntries);
|
||||
for (uint32_t i = 0; i < entries; ++i) {
|
||||
WriteUnifiedStatus(statusPtr + i * kUnifiedSize, have ? &sample : nullptr);
|
||||
}
|
||||
} catch (const Memory::AccessViolation&) {
|
||||
return 0;
|
||||
}
|
||||
return have ? 1 : 0;
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(8019812C, KPAD__GetUnifiedWpadStatus_HLE, int32_t,
|
||||
(uint32_t chan, uint32_t statusPtr, uint32_t count), (chan, statusPtr, count));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "hle_stubs.h"
|
||||
#include "memory.h"
|
||||
#include "hle/controller_status_contract.h"
|
||||
#include "wii_remote_input.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
@@ -37,6 +38,7 @@ extern "C" uint32_t PAD__Init_HLE()
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801AF2F0, PAD__Init_HLE, uint32_t, (), ());
|
||||
|
||||
// PADRead: gathers every GameCube pad source for the frame and writes the statuses to guest memory.
|
||||
extern "C" uint32_t PAD__Read_HLE(uint32_t statusPtr)
|
||||
{
|
||||
if (statusPtr == 0) {
|
||||
@@ -44,7 +46,13 @@ extern "C" uint32_t PAD__Read_HLE(uint32_t statusPtr)
|
||||
}
|
||||
|
||||
PADStatus statuses[PAD_CHANMAX]{};
|
||||
// Keep looking for a Bluetooth Wii Remote that dropped out (or was turned on late).
|
||||
WiiRemoteInput::Poll();
|
||||
uint32_t rumbleMask = PADRead(statuses);
|
||||
// Wii Remotes reach the game through KPAD, not as GameCube pads. This also
|
||||
// applies while input is blocked (overlay open) so the port does not flip
|
||||
// between "connected" and "no controller" every time the overlay toggles.
|
||||
WiiRemoteInput::HideRemotesFromPad(statuses, PAD_CHANMAX);
|
||||
|
||||
try {
|
||||
for (uint32_t i = 0; i < PAD_CHANMAX; ++i) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "hle_stubs.h"
|
||||
#include "memory.h"
|
||||
#include "hle/controller_status_contract.h"
|
||||
#include "wii_remote_input.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -98,18 +99,41 @@ extern "C" int32_t WPADGetDataFormat_HLE(uint32_t chan)
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801C0B54, WPADGetDataFormat_HLE, int32_t, (uint32_t chan), (chan));
|
||||
|
||||
// WPADSetDataFormat: records the per-channel data format the game asked for.
|
||||
extern "C" int32_t WPADSetDataFormat_HLE(uint32_t chan, int32_t format)
|
||||
{
|
||||
return g_state.contract.SetDataFormat(chan, format);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801C0B9C, WPADSetDataFormat_HLE, int32_t, (uint32_t chan, int32_t format), (chan, format));
|
||||
|
||||
// WPADProbe: reports the extension type of a Bluetooth remote on `chan`, or no controller.
|
||||
extern "C" int32_t WPADProbe_HLE(uint32_t chan, uint32_t typePtr)
|
||||
{
|
||||
if (chan >= WpadContract::kChannelCount) {
|
||||
return WpadContract::kErrorBadChannel;
|
||||
}
|
||||
|
||||
// Drive the rescan state machine here too: a reconnect probe can arrive
|
||||
// before the next PADRead, and only Poll() brings a dropped remote back.
|
||||
WiiRemoteInput::Poll();
|
||||
|
||||
// A real Bluetooth remote: WPAD_DEV_CORE (0) for a bare remote,
|
||||
// WPAD_DEV_FREESTYLE (1) with a Nunchuk, WPAD_DEV_CLASSIC (2) with a Classic
|
||||
// Controller. The game reads the type from here (not from
|
||||
// KPADStatus.dev_type) to pick its control scheme, and re-reads it when it
|
||||
// changes, which is what makes an extension swap mid-game work like on the
|
||||
// console. EffectiveKind keeps the last type through SDL's re-creation of
|
||||
// the joystick after a swap.
|
||||
const WiiRemoteInput::Kind kind = WiiRemoteInput::EffectiveKind(chan);
|
||||
if (WiiRemoteInput::IsRemoteChannel(chan)) {
|
||||
if (typePtr != 0) {
|
||||
uint32_t type = WpadContract::kExtensionCore;
|
||||
if (kind == WiiRemoteInput::Kind::RemoteWithNunchuk) type = 1u;
|
||||
if (kind == WiiRemoteInput::Kind::RemoteWithClassic) type = 2u;
|
||||
Memory::Write32(typePtr, type);
|
||||
}
|
||||
return kStatusOk;
|
||||
}
|
||||
if (typePtr != 0) {
|
||||
Memory::Write32(typePtr, WpadContract::kExtensionCore);
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
#include "system_bridge.h"
|
||||
#include "ppc_runtime.h"
|
||||
#include "aurora_events.h"
|
||||
#include "wii_remote_input.h"
|
||||
#include "discord_presence.h"
|
||||
#include "fiber_manager.h"
|
||||
#include "hle_stubs.h"
|
||||
@@ -1301,6 +1302,7 @@ static void TerminateHandler() {
|
||||
std::_Exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
// Runtime entry point: loads the configuration, brings up aurora and runs the game.
|
||||
int RuntimeMain(int argc, char** argv) {
|
||||
// Must run before the transcript duplicates stdout/stderr: it decides what
|
||||
// those descriptors are mirrored to now that the products are GUI-subsystem.
|
||||
@@ -1412,6 +1414,11 @@ int RuntimeMain(int argc, char** argv) {
|
||||
}
|
||||
const AuroraBackend requestedBackend = auroraConfig.desiredBackend;
|
||||
|
||||
// SDL only reads its Wii driver hint when the joystick subsystem starts, which
|
||||
// aurora_initialize does; a Bluetooth Wii Remote paired before launch must be
|
||||
// visible on that first scan.
|
||||
WiiRemoteInput::ConfigureSdlHints(RuntimeConfigFile::WiiRemotesEnabled(true));
|
||||
|
||||
const AuroraInfo auroraInfo = aurora_initialize(0, nullptr, &auroraConfig);
|
||||
if (requestedBackend != BACKEND_AUTO && auroraInfo.backend != requestedBackend) {
|
||||
RT_LOG(RT_TAG_RUNTIME) << "graphics_api=\"" << backend
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "music_attenuation.h"
|
||||
#include "runtime_config.h"
|
||||
#include "runtime_log.h"
|
||||
#include "wii_remote_input.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <SDL3/SDL_events.h>
|
||||
@@ -279,6 +280,15 @@ void ApplyConfiguredMappings() {
|
||||
if (controllerIndex < 0) {
|
||||
continue;
|
||||
}
|
||||
// The [controller] bindings are positional and shared by every port, so
|
||||
// they describe whatever pad the user set them up with (usually an Xbox
|
||||
// layout: a = south). A Wii U Pro Controller has a fixed, known layout
|
||||
// (A on the east position) that aurora already maps by name; applying
|
||||
// the shared bindings on top swaps A/B and X/Y. (Wii Remotes with any
|
||||
// extension never reach the PAD layer: the game reads them through KPAD.)
|
||||
if (WiiRemoteInput::KindForPort(port) == WiiRemoteInput::Kind::WiiUPro) {
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32_t count = 0;
|
||||
if (PADGetButtonMappings(port, &count) == nullptr || count != PAD_BUTTON_COUNT) {
|
||||
@@ -310,6 +320,141 @@ void ApplyConfiguredMappings() {
|
||||
}
|
||||
}
|
||||
|
||||
bool g_wiiRemotesEnabled = RuntimeConfigFile::WiiRemotesEnabled(true);
|
||||
bool g_wiiContinuousScan = RuntimeConfigFile::WiiContinuousScanEnabled(true);
|
||||
|
||||
// Accelerometer readout and zero-point calibration for a bare remote / remote + Nunchuk.
|
||||
void DrawWiiRemoteAccelerometer(uint32_t port) {
|
||||
ImGui::SeparatorText("Accelerometer");
|
||||
float sdlG[3] = {};
|
||||
float kpad[3] = {};
|
||||
if (WiiRemoteInput::ReadAccelDebug(port, sdlG, kpad)) {
|
||||
ImGui::Text("KPAD acc: x %+.2f y %+.2f z %+.2f g", kpad[0], kpad[1], kpad[2]);
|
||||
ImGui::TextDisabled("Flat, buttons up: (0, -1, 0). Sideways as a wheel: (1, 0, 0); z follows the turn.");
|
||||
} else {
|
||||
ImGui::TextDisabled("No accelerometer data yet.");
|
||||
}
|
||||
// SDL's read of the remote's calibration block often times out over Bluetooth
|
||||
// and it falls back to a nominal zero point, leaving a small per-axis bias;
|
||||
// measured here with the remote at rest.
|
||||
if (WiiRemoteInput::IsAccelCalibrating()) {
|
||||
ImGui::ProgressBar(WiiRemoteInput::AccelCalibrationProgress(), ImVec2(220.0f, 0.0f), "Hold still...");
|
||||
} else if (ImGui::Button("Calibrate (remote lying flat, buttons up)")) {
|
||||
WiiRemoteInput::StartAccelCalibration(port);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Put the remote down on a flat surface with the buttons facing up and do not touch it\n"
|
||||
"for about two seconds. Corrects the steering offset of a remote held sideways.");
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(!RuntimeConfigFile::HasWiiAccelOffset() || WiiRemoteInput::IsAccelCalibrating());
|
||||
if (ImGui::Button("Clear calibration")) {
|
||||
WiiRemoteInput::ClearAccelCalibration();
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
if (const char* message = WiiRemoteInput::AccelCalibrationMessage()) {
|
||||
ImGui::TextWrapped("%s", message);
|
||||
} else if (RuntimeConfigFile::HasWiiAccelOffset()) {
|
||||
const std::array<double, 3> offset = RuntimeConfigFile::WiiAccelOffset();
|
||||
ImGui::TextDisabled("Stored offset: x %+.3f y %+.3f z %+.3f g", offset[0], offset[1], offset[2]);
|
||||
} else {
|
||||
ImGui::TextDisabled("Not calibrated (using SDL's zero point; see console.log for \"fallback accelerometer calibration\").");
|
||||
}
|
||||
}
|
||||
|
||||
// Wii Remotes (Bluetooth) menu: driver switch, pairing help, continuous scanning and the port's controller kind.
|
||||
void DrawWiiRemoteSettings(uint32_t selectedGamePort) {
|
||||
if (!ImGui::BeginMenu("Wii Remotes (Bluetooth)")) {
|
||||
return;
|
||||
}
|
||||
if (ImGui::Checkbox("Use Wii Remotes / Wii U Pro Controllers", &g_wiiRemotesEnabled)) {
|
||||
RuntimeConfigFile::SetWiiRemotesEnabled(g_wiiRemotesEnabled);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Takes effect on the next launch. Turn this off if you use a Mayflash DolphinBar.");
|
||||
}
|
||||
ImGui::TextDisabled("Pairing: Windows Settings > Bluetooth > Add device, then press 1+2");
|
||||
ImGui::TextDisabled("(or the red SYNC button) on the remote. Leave the PIN empty.");
|
||||
ImGui::TextDisabled("A remote that was paired before also needs to be turned on with 1+2/SYNC.");
|
||||
if (ImGui::Checkbox("Keep scanning for Wii Remotes (like Dolphin's Continuous Scanning)",
|
||||
&g_wiiContinuousScan)) {
|
||||
RuntimeConfigFile::SetWiiContinuousScanEnabled(g_wiiContinuousScan);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("While no Wii controller is connected, re-check Bluetooth every 2 seconds so a\n"
|
||||
"remote that dropped out (\"Communications with the controller have been\n"
|
||||
"interrupted\") or was turned on after launch comes back by itself.");
|
||||
}
|
||||
// The driver hint is only read at launch, so a rescan after the user turned
|
||||
// the setting off would still re-enumerate Wii devices in this session.
|
||||
ImGui::BeginDisabled(!g_wiiRemotesEnabled);
|
||||
if (ImGui::Button("Rescan now")) {
|
||||
WiiRemoteInput::RescanNow();
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
if (WiiRemoteInput::IsScanning()) {
|
||||
ImGui::TextDisabled("Scanning... (%u so far) - press 1+2 on the remote", WiiRemoteInput::ScanCount());
|
||||
} else {
|
||||
ImGui::TextDisabled("Not scanning");
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
const WiiRemoteInput::Kind kind = WiiRemoteInput::KindForPort(selectedGamePort);
|
||||
ImGui::Text("Port %u: %s", static_cast<unsigned>(selectedGamePort + 1), WiiRemoteInput::KindLabel(kind));
|
||||
if (kind == WiiRemoteInput::Kind::RemoteWithClassic) {
|
||||
WiiRemoteInput::KpadSample sample;
|
||||
if (WiiRemoteInput::ReadKpadSample(selectedGamePort, sample)) {
|
||||
// WPAD_CL_BUTTON_* bits, in the game's own layout (no mapping involved).
|
||||
const auto held = [&](uint32_t bit, const char* on, const char* off) { return (sample.clHold & bit) ? on : off; };
|
||||
ImGui::Text("Classic: %s %s %s %s %s %s %s %s %s %s %s %s %s %s", held(0x0010, "A", "a"),
|
||||
held(0x0040, "B", "b"), held(0x0008, "X", "x"), held(0x0020, "Y", "y"), held(0x2000, "L", "l"),
|
||||
held(0x0200, "R", "r"), held(0x0080, "ZL", "zl"), held(0x0004, "ZR", "zr"),
|
||||
held(0x0400, "PLUS", "plus"), held(0x1000, "MINUS", "minus"), held(0x0001, "UP", "up"),
|
||||
held(0x4000, "DOWN", "down"), held(0x0002, "LEFT", "left"), held(0x8000, "RIGHT", "right"));
|
||||
ImGui::Text("Sticks: L %+.2f %+.2f (WPAD %+d %+d) R %+.2f %+.2f (WPAD %+d %+d)", sample.clLStick[0],
|
||||
sample.clLStick[1], static_cast<int>(sample.clLStickRaw[0]),
|
||||
static_cast<int>(sample.clLStickRaw[1]), sample.clRStick[0], sample.clRStick[1],
|
||||
static_cast<int>(sample.clRStickRaw[0]), static_cast<int>(sample.clRStickRaw[1]));
|
||||
ImGui::TextDisabled("Capitals = held. The game reads this Classic Controller through KPAD, as on the");
|
||||
ImGui::TextDisabled("console: its buttons mean what the game says they mean, no mapping applies.");
|
||||
}
|
||||
}
|
||||
if (kind == WiiRemoteInput::Kind::WiiUPro) {
|
||||
if (SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(selectedGamePort))) {
|
||||
// SDL's Wii driver posts the D-pad as joystick buttons 11-14 (the
|
||||
// SDL_GAMEPAD_BUTTON_DPAD_* values) while its default HIDAPI mapping
|
||||
// expects a hat, so SDL_GetGamepadButton never sees them; read the
|
||||
// joystick directly, like the fallback in aurora's PADRead does.
|
||||
SDL_Joystick* joystick = SDL_GetGamepadJoystick(gamepad);
|
||||
const auto rawButton = [&](int index) {
|
||||
return joystick != nullptr && SDL_GetJoystickButton(joystick, index);
|
||||
};
|
||||
ImGui::Text("Raw D-pad: %s %s %s %s", rawButton(SDL_GAMEPAD_BUTTON_DPAD_UP) ? "UP" : "up",
|
||||
rawButton(SDL_GAMEPAD_BUTTON_DPAD_DOWN) ? "DOWN" : "down",
|
||||
rawButton(SDL_GAMEPAD_BUTTON_DPAD_LEFT) ? "LEFT" : "left",
|
||||
rawButton(SDL_GAMEPAD_BUTTON_DPAD_RIGHT) ? "RIGHT" : "right");
|
||||
ImGui::Text("Raw face buttons: %s %s %s %s", rawButton(SDL_GAMEPAD_BUTTON_EAST) ? "A" : "a",
|
||||
rawButton(SDL_GAMEPAD_BUTTON_SOUTH) ? "B" : "b", rawButton(SDL_GAMEPAD_BUTTON_NORTH) ? "X" : "x",
|
||||
rawButton(SDL_GAMEPAD_BUTTON_WEST) ? "Y" : "y");
|
||||
ImGui::Text("Raw ZL/ZR: %d / %d (pressed above 0)",
|
||||
SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER),
|
||||
SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER));
|
||||
ImGui::TextDisabled("Capitals = held. If a button never turns to capitals while physically held,");
|
||||
ImGui::TextDisabled("that press is not reaching SDL at all (a driver-level issue, not a mapping one).");
|
||||
ImGui::TextDisabled("This pad uses Nintendo's own layout (a/b/x/y as labelled); the shared");
|
||||
ImGui::TextDisabled("button mapping above does not apply to it.");
|
||||
}
|
||||
}
|
||||
if (kind == WiiRemoteInput::Kind::Remote || kind == WiiRemoteInput::Kind::RemoteWithNunchuk ||
|
||||
kind == WiiRemoteInput::Kind::RemoteWithClassic) {
|
||||
DrawWiiRemoteAccelerometer(selectedGamePort);
|
||||
}
|
||||
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
// Controller settings menu: port selection, controller assignment and button mapping.
|
||||
void DrawControllerSettings() {
|
||||
for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
|
||||
const std::string label = "Port " + std::to_string(port + 1);
|
||||
@@ -329,6 +474,7 @@ void DrawControllerSettings() {
|
||||
}
|
||||
ImGui::Separator();
|
||||
controller_mapping_wizard::DrawSetupList();
|
||||
DrawWiiRemoteSettings(selectedGamePort);
|
||||
const uint32_t controllerCount = PADCount();
|
||||
if (controllerCount == 0) {
|
||||
ImGui::TextDisabled("No controller connected");
|
||||
@@ -893,6 +1039,11 @@ void Draw() noexcept {
|
||||
// Wait for the frame worker's DONE phase: it has replayed the previous frame's ImGui draw lists
|
||||
// and started the next ImGui frame, so all overlay callers can now safely issue ImGui commands.
|
||||
aurora_wait_for_frame_worker();
|
||||
// Also drive the Wii Remote rescan from here: PADRead runs it too, but this
|
||||
// runs once per presented frame whatever the game is doing (e.g. sitting in
|
||||
// its "communications interrupted" prompt without polling pads). Same guest
|
||||
// thread as PADRead, so no concurrent access to the scanner's state.
|
||||
WiiRemoteInput::Poll();
|
||||
ApplyConfiguredMappings();
|
||||
PersistDisplayModeIfChanged();
|
||||
UpdateCursorAutoHide();
|
||||
|
||||
@@ -0,0 +1,741 @@
|
||||
#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>
|
||||
|
||||
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.
|
||||
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) {
|
||||
RT_LOG("sdl") << message << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
// 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(true)) {
|
||||
g_scanning = false;
|
||||
return;
|
||||
}
|
||||
const uint64_t now = SDL_GetTicks();
|
||||
if (!g_scanning) {
|
||||
RT_LOG(RT_TAG_CONFIG) << "No Wii Remote connected; scanning for one (press 1+2 on the remote)"
|
||||
<< std::endl;
|
||||
g_scanning = true;
|
||||
g_lostAtMs = now;
|
||||
}
|
||||
if (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;
|
||||
}
|
||||
|
||||
// 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
|
||||
Reference in New Issue
Block a user