pass real wii remotes through emulated bluetooth

This commit is contained in:
patchzyy
2026-10-20 02:05:29 +02:00
parent e3f5e3a3ec
commit 209fba7f64
43 changed files with 5542 additions and 1964 deletions
+19
View File
@@ -0,0 +1,19 @@
#pragma once
// HCI and L2CAP fields are little-endian.
#include <cstdint>
#include <vector>
namespace Bluetooth {
inline uint16_t Le16(const uint8_t* p) {
return static_cast<uint16_t>(p[0] | (p[1] << 8));
}
inline void PutLe16(std::vector<uint8_t>& out, uint16_t v) {
out.push_back(static_cast<uint8_t>(v));
out.push_back(static_cast<uint8_t>(v >> 8));
}
} // namespace Bluetooth
+153
View File
@@ -0,0 +1,153 @@
#pragma once
// Emulated Bluetooth controller behind the Wii's internal USB module
// (/dev/usb/oh1/57e/305). The game's own Broadcom stack (BTE), WUD, WPAD and KPAD
// talk HCI to it; each of the four virtual Wii Remotes it hosts is backed by a
// physical remote on the PC (see RemoteHost), whose HID reports are passed
// through unchanged. Pure host code: no guest memory, so it is unit-testable.
#include <array>
#include <cstddef>
#include <cstdint>
#include <deque>
#include <memory>
#include <vector>
namespace Bluetooth {
// Bluetooth device address in HCI (least significant byte first) order.
using BdAddr = std::array<uint8_t, 6>;
inline constexpr size_t kRemoteSlots = 4;
enum class LinkState : uint8_t {
Down, // no baseband connection to the game
Connecting, // baseband up, HID channels being opened
Ready, // HID control and interrupt channels open
};
// The physical side of the virtual remotes. Called from the thread that drives
// the Controller; implementations synchronize with their own I/O threads.
class RemoteHost {
public:
virtual ~RemoteHost() = default;
// A physical remote is attached to `slot` and can carry a connection.
virtual bool IsAttached(size_t slot) const = 0;
// True once each time the remote on `slot` asks to connect (it was just
// attached, or a button was pressed while it had no connection).
virtual bool ConsumeWake(size_t slot) = 0;
// Next input report (report id first, no HID header), false when none.
virtual bool PopInputReport(size_t slot, std::vector<uint8_t>& report) = 0;
// Output report from the game (report id first, no HID header).
virtual void SendOutputReport(size_t slot, const uint8_t* report, size_t size) = 0;
virtual void OnLinkStateChanged(size_t slot, LinkState state) = 0;
// The game ended a working connection; a real remote powers off here.
virtual void OnGuestDisconnect(size_t slot) = 0;
// The game is searching for remotes (its 1+2 / reconnect screens).
virtual void OnGuestInquiry(bool active) = 0;
// "Nintendo RVL-CNT-01" or "Nintendo RVL-CNT-01-TR".
virtual const char* DeviceName(size_t slot) const = 0;
};
class RemoteLink;
class Controller {
public:
explicit Controller(RemoteHost& host);
~Controller();
Controller(const Controller&) = delete;
Controller& operator=(const Controller&) = delete;
// The guest opened / closed the USB device.
void Open(uint64_t nowUs);
void Close();
bool IsOpen() const { return m_open; }
// Raw HCI command packet (opcode first, no H4 byte).
void ExecuteCommand(const uint8_t* packet, size_t size, uint64_t nowUs);
// Raw ACL packet from the guest (handle first).
void ReceiveAcl(const uint8_t* packet, size_t size, uint64_t nowUs);
// Advances timers, connection attempts, input forwarding and flow control.
void Tick(uint64_t nowUs);
bool HasEvent() const { return !m_events.empty(); }
bool PopEvent(std::vector<uint8_t>& event);
// ACL data is only handed out once every queued event has been delivered,
// so the guest always sees a connection before data on it.
bool PopAcl(std::vector<uint8_t>& packet);
size_t AclQueued() const { return m_acl.size(); }
LinkState SlotLinkState(size_t slot) const;
bool PageScanEnabled() const { return (m_scanEnable & 0x02) != 0; }
bool InquiryActive() const { return m_inquiry.active; }
static BdAddr LocalAddress();
static BdAddr RemoteAddress(size_t slot);
static uint16_t ConnectionHandle(size_t slot);
// Used by RemoteLink.
void QueueEvent(std::vector<uint8_t> event);
void QueueAcl(uint16_t handle, const std::vector<uint8_t>& l2capFrame);
RemoteHost& Host() { return m_host; }
uint32_t PageTimeoutUs() const { return static_cast<uint32_t>(m_pageTimeoutSlots) * 625u; }
private:
struct StoredKey {
BdAddr addr{};
std::array<uint8_t, 16> key{};
};
struct Inquiry {
bool active = false;
uint64_t endUs = 0;
uint8_t maxResponses = 0;
uint8_t reported = 0;
std::array<bool, kRemoteSlots> slotReported{};
};
struct Reassembly {
std::vector<uint8_t> frame;
size_t expected = 0;
};
// A reply the controller sends after a delay (page / name request timeouts).
struct PendingEvent {
uint64_t dueUs = 0;
std::vector<uint8_t> event;
};
void ResetState(bool keepStoredKeys);
void CommandComplete(uint16_t opcode, const std::vector<uint8_t>& params);
void CommandStatus(uint16_t opcode, uint8_t status);
void TickInquiry(uint64_t nowUs);
void FinishInquiry();
void SendNumberOfCompletedPackets();
int SlotForAddress(const BdAddr& addr) const;
int SlotForHandle(uint16_t handle) const;
const StoredKey* FindStoredKey(const BdAddr& addr) const;
void HandleLinkControl(uint16_t opcode, const uint8_t* p, size_t n, uint64_t nowUs);
void HandleLinkPolicy(uint16_t opcode, const uint8_t* p, size_t n);
void HandleControllerBaseband(uint16_t opcode, const uint8_t* p, size_t n, uint64_t nowUs);
void HandleInformational(uint16_t opcode, const uint8_t* p, size_t n);
void HandleStatusParameters(uint16_t opcode, const uint8_t* p, size_t n);
void HandleVendor(uint16_t opcode, const uint8_t* p, size_t n);
void UnknownCommand(uint16_t opcode);
RemoteHost& m_host;
bool m_open = false;
uint8_t m_scanEnable = 0;
uint16_t m_pageTimeoutSlots = 0x2000;
uint8_t m_inquiryMode = 0;
std::array<uint8_t, 3> m_classOfDevice{};
std::array<char, 248> m_localName{};
Inquiry m_inquiry;
bool m_hostInquiryNotified = false;
std::vector<StoredKey> m_storedKeys;
std::array<std::unique_ptr<RemoteLink>, kRemoteSlots> m_links;
std::array<uint16_t, kRemoteSlots> m_completedPackets{};
std::array<Reassembly, kRemoteSlots> m_reassembly;
std::array<bool, kRemoteSlots> m_authPending{};
std::vector<PendingEvent> m_pending;
std::deque<std::vector<uint8_t>> m_events;
std::deque<std::vector<uint8_t>> m_acl;
};
} // namespace Bluetooth
+107
View File
@@ -0,0 +1,107 @@
#pragma once
// One emulated Wii Remote as seen from the controller: its baseband connection,
// L2CAP channels (SDP, HID control, HID interrupt) and the HID transaction layer
// that carries the physical remote's reports.
#include "bluetooth/bt_controller.h"
#include <cstddef>
#include <cstdint>
#include <vector>
namespace Bluetooth {
class RemoteLink {
public:
enum class Baseband : uint8_t {
Idle,
Requesting, // Connection Request sent, waiting for the guest to accept
Paging, // the guest paged this remote (Create Connection)
Connected,
};
RemoteLink(Controller& controller, size_t slot);
// Drops every connection without telling the guest (controller reset/close).
void Reset();
void Tick(uint64_t nowUs);
// Remote side wants a connection and should be offered to the guest's inquiry.
bool Discoverable() const;
bool ConnectWanted() const { return m_connectWanted; }
// Guest accepted / rejected this remote's Connection Request.
bool IsRequesting() const { return m_baseband == Baseband::Requesting; }
void OnAccepted(uint8_t role);
void OnRejected(uint8_t reason, uint64_t nowUs);
// Guest paged this remote. Returns false when the remote cannot answer.
bool OnPaged(uint64_t nowUs);
// Baseband went down. `byGuest` = the guest's HCI Disconnect.
void OnDisconnected(bool byGuest, uint64_t nowUs);
// The physical remote vanished while connected: tells the guest.
void DropFromRemote(uint8_t reason);
void ReceiveL2cap(const uint8_t* frame, size_t size);
Baseband BasebandState() const { return m_baseband; }
LinkState State() const { return m_state; }
private:
struct Channel {
uint16_t psm = 0;
uint16_t localCid = 0;
uint16_t remoteCid = 0;
uint16_t remoteMtu = 672;
bool weInitiated = false;
bool connected = false; // connection response success seen/sent
bool configSent = false; // our Configuration Request is out
bool localConfigured = false; // the guest accepted our configuration
bool remoteConfigured = false; // we accepted the guest's configuration
uint8_t connectIdent = 0;
uint8_t configIdent = 0;
uint64_t requestUs = 0;
std::vector<uint8_t> pendingOptions; // continued Configuration Request
bool Open() const { return connected && localConfigured && remoteConfigured; }
};
Channel* FindByLocal(uint16_t cid);
Channel* FindByPsm(uint16_t psm);
uint16_t AllocateCid() const;
uint8_t NextIdent();
void SendSignal(uint8_t code, uint8_t ident, const std::vector<uint8_t>& data);
void SendData(const Channel& channel, const std::vector<uint8_t>& payload);
void RequestChannel(uint16_t psm, uint64_t nowUs);
void SendConfigRequest(Channel& channel);
void HandleSignalling(const uint8_t* data, size_t size);
void HandleConnectionRequest(uint8_t ident, const uint8_t* p, size_t n);
void HandleConnectionResponse(uint8_t ident, const uint8_t* p, size_t n);
void HandleConfigurationRequest(uint8_t ident, const uint8_t* p, size_t n);
void HandleConfigurationResponse(uint8_t ident, const uint8_t* p, size_t n);
void HandleDisconnectionRequest(uint8_t ident, const uint8_t* p, size_t n);
void HandleDisconnectionResponse(const uint8_t* p, size_t n);
void HandleChannelData(Channel& channel, const uint8_t* data, size_t size);
void HandleHidControl(Channel& channel, const uint8_t* data, size_t size);
void UpdateState();
void SetState(LinkState state);
void ForwardInput();
void SendInput(const Channel& interrupt, const std::vector<uint8_t>& report);
void ClearConnection();
Controller& m_controller;
size_t m_slot;
Baseband m_baseband = Baseband::Idle;
LinkState m_state = LinkState::Down;
bool m_connectWanted = false;
bool m_guestInitiated = false;
bool m_everReady = false;
uint8_t m_ident = 0;
uint64_t m_stateSinceUs = 0;
uint64_t m_retryAtUs = 0;
uint64_t m_nowUs = 0;
std::vector<Channel> m_channels;
std::vector<uint8_t> m_report;
};
} // namespace Bluetooth
+19
View File
@@ -0,0 +1,19 @@
#pragma once
// SDP server of an emulated Wii Remote: the HID service record (0x00010000)
// and the Device ID record (0x00010001) a real RVL-CNT-01 publishes.
#include <cstddef>
#include <cstdint>
#include <vector>
namespace Bluetooth::Sdp {
// The Wii Remote's HID report descriptor, as the game also embeds it.
const std::vector<uint8_t>& WiiRemoteHidDescriptor();
// Answers one SDP request PDU. `maxResponse` bounds the PDU returned (the
// guest's L2CAP MTU on the SDP channel).
std::vector<uint8_t> HandleRequest(const uint8_t* pdu, size_t size, size_t maxResponse);
} // namespace Bluetooth::Sdp
+22
View File
@@ -0,0 +1,22 @@
#pragma once
// IOS side of the emulated Bluetooth module (/dev/usb/oh1/57e/305): routes the
// guest's USB control / bulk / interrupt transfers to Bluetooth::Controller and
// completes the held event and ACL reads from the alarm pump.
#include <cstdint>
struct CpuContext;
// Returns the fd for the Bluetooth device path, 0 when `path` is not ours.
int32_t Bluetooth_HLE_OpenDevice(const char* path);
bool Bluetooth_HLE_IsFd(uint32_t fd);
// Cancels the held reads (their callbacks are queued) and closes the device.
int32_t Bluetooth_HLE_Close(uint32_t fd);
// True when the request completed now with `result`; false when it is held
// until data arrives (its callback is queued later by the pump).
bool Bluetooth_HLE_IoctlvAsync(uint32_t fd, uint32_t cmd, uint32_t numIn, uint32_t numOut, uint32_t vectorPtr,
uint32_t callback, uint32_t callbackArg, int32_t& result);
int32_t Bluetooth_HLE_Ioctlv(uint32_t fd, uint32_t cmd, uint32_t numIn, uint32_t numOut, uint32_t vectorPtr);
// Runs the controller and completes held reads. Emulation thread only.
bool Bluetooth_HLE_ProcessCompletions(CpuContext* cpu);
@@ -41,42 +41,3 @@ inline GuestStatus Encode(const Fields& fields)
}
} // namespace PadStatusContract
namespace WpadContract {
inline constexpr std::size_t kChannelCount = 4;
inline constexpr int32_t kStatusDisabled = 0;
inline constexpr int32_t kStatusReady = 3;
inline constexpr int32_t kErrorNoController = -1;
inline constexpr int32_t kErrorNotReady = -2;
inline constexpr int32_t kErrorBadChannel = -6;
inline constexpr int32_t kExtensionCore = 0;
class State {
public:
void Initialize() { m_initialized = true; }
bool IsInitialized() const { return m_initialized; }
int32_t GetLibraryStatus() const { return m_initialized ? kStatusReady : kStatusDisabled; }
int32_t GetDataFormat(uint32_t chan) const
{
if (chan >= kChannelCount) {
return kErrorBadChannel;
}
return m_initialized ? 0 : kErrorNotReady;
}
int32_t SetDataFormat(uint32_t chan, int32_t format) const
{
(void)format;
if (chan >= kChannelCount) {
return kErrorBadChannel;
}
return m_initialized ? kErrorNoController : kErrorNotReady;
}
private:
bool m_initialized = false;
};
} // namespace WpadContract
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include <cstdint>
// HW_GPIOB_OUT, the GPIO outputs the PPC owns. WPAD sets bit 8 to power the
// sensor bar; there is nothing behind it on a PC, so the register only latches.
inline constexpr uint32_t kHollywoodGpioBOut = 0xCD0000C0u;
bool HollywoodGpio_Read32(uint32_t address, uint32_t& value);
bool HollywoodGpio_Write32(uint32_t address, uint32_t value);
+58 -81
View File
@@ -57,26 +57,16 @@ struct RuntimeUserConfig {
std::optional<bool> audioMuted;
std::optional<bool> audioMixWorker;
std::optional<bool> attenuateMusicWhenMediaPlays;
// Real Wii Remotes (with or without Nunchuk / Classic Controller) and Wii U Pro
// Controllers paired over Bluetooth, driven by SDL's HIDAPI Wii driver. The driver
// is opt-in on SDL's side, so this decides whether the runtime turns it on.
// Real Wii Remotes passed through to the game's own Bluetooth stack. Off hands
// Nintendo controllers (e.g. the Wii U Pro Controller) to SDL as plain gamepads.
std::optional<bool> wiiRemotes;
// Keep re-enumerating Bluetooth HID devices while no Wii controller is connected
// (Dolphin's "continuous scanning"), so a remote that dropped or was switched on
// after launch shows up without restarting.
std::optional<bool> wiiContinuousScan;
// Accelerometer zero-point correction for the Bluetooth Wii Remote, in g and in
// SDL's sensor frame (x right, y out of the button face, z towards the user).
// SDL's Wii driver falls back to a nominal zero point when its read of the
// remote's calibration block times out (common over Bluetooth), so this is
// measured in the overlay with the remote at rest. Stored per remote under
// [controller.wii_accel_offsets]; the old shared
// wii_accel_offset_* keys are only detected, to report that they are ignored.
std::map<std::string, std::array<double, 3>> wiiAccelOffsets;
bool hasLegacyWiiAccelOffset = false;
// Debugging aid: append every KPAD sample of the Bluetooth remote (raw and
// corrected accelerometer, buttons) to wii_accel_trace.csv next to Config.toml.
std::optional<bool> wiiAccelTrace;
// Look for remotes in pairing mode (1+2 / SYNC) while none is connected.
std::optional<bool> wiiAutoPair;
// The console's Wii Remote settings (SYSCONF BT.BAR, BT.SENS, BT.SPKV, BT.MOT).
std::optional<bool> wiiSensorBarAbove;
std::optional<int32_t> wiiIrSensitivity;
std::optional<int32_t> wiiSpeakerVolume;
std::optional<bool> wiiRumble;
std::optional<bool> networkEnabled;
std::optional<bool> discordPresenceEnabled;
// The application ID of the WiiCompiled Discord application. This is only
@@ -382,28 +372,6 @@ inline void AppendOverlayRoots(RuntimeUserConfig& config, const std::string& roo
}
// Reads every supported setting out of a parsed Config.toml document.
// [x, y, z] of finite numbers (integers allowed), else nullopt.
inline std::optional<std::array<double, 3>> ParseAccelOffset(const toml::value& value) {
if (!value.is_array() || value.as_array().size() != 3) {
return std::nullopt;
}
std::array<double, 3> offset{};
for (size_t i = 0; i < 3; ++i) {
const toml::value& element = value.as_array()[i];
if (element.is_floating()) {
offset[i] = element.as_floating();
} else if (element.is_integer()) {
offset[i] = static_cast<double>(element.as_integer());
} else {
return std::nullopt;
}
if (!std::isfinite(offset[i])) {
return std::nullopt;
}
}
return offset;
}
inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
RuntimeUserConfig config;
@@ -427,13 +395,6 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
config.controllerExpressions[key] = value.as_string();
}
}
if (section->contains("wii_accel_offsets") && section->at("wii_accel_offsets").is_table()) {
for (const auto& [key, value] : section->at("wii_accel_offsets").as_table()) {
if (const auto offset = ParseAccelOffset(value)) {
config.wiiAccelOffsets[key] = *offset;
}
}
}
}
config.widescreen = FindConfigValue<bool>(document, "video", "widescreen");
@@ -494,11 +455,17 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
config.attenuateMusicWhenMediaPlays =
FindConfigValue<bool>(document, "audio", "attenuate_music_when_media_plays");
config.wiiRemotes = FindConfigValue<bool>(document, "controller", "wii_remotes");
config.wiiContinuousScan = FindConfigValue<bool>(document, "controller", "wii_continuous_scan");
for (const char* key : {"wii_accel_offset_x", "wii_accel_offset_y", "wii_accel_offset_z"}) {
config.hasLegacyWiiAccelOffset |= FindConfigValue<double>(document, "controller", key).value_or(0.0) != 0.0;
config.wiiAutoPair = FindConfigValue<bool>(document, "controller", "wii_auto_pair");
if (const auto bar = FindConfigValue<std::string>(document, "controller", "wii_sensor_bar")) {
if (*bar == "above" || *bar == "below") config.wiiSensorBarAbove = *bar == "above";
}
config.wiiAccelTrace = FindConfigValue<bool>(document, "controller", "wii_accel_trace");
if (const auto value = FindConfigInt(document, "controller", "wii_ir_sensitivity"); value && *value >= 1 && *value <= 5) {
config.wiiIrSensitivity = *value;
}
if (const auto value = FindConfigInt(document, "controller", "wii_speaker_volume"); value && *value >= 0 && *value <= 127) {
config.wiiSpeakerVolume = *value;
}
config.wiiRumble = FindConfigValue<bool>(document, "controller", "wii_rumble");
config.networkEnabled = FindConfigValue<bool>(document, "network", "enabled");
config.discordPresenceEnabled = FindConfigValue<bool>(document, "discord", "enabled");
config.discordClientId = FindConfigValue<std::string>(document, "discord", "client_id");
@@ -849,51 +816,61 @@ inline bool AttenuateMusicWhenMediaPlays(bool fallback = false) {
return Get().attenuateMusicWhenMediaPlays.value_or(fallback);
}
// Bluetooth Wii Remotes / Wii U Pro Controllers. Read once before SDL's joystick
// subsystem comes up, so a change only takes effect on the next launch.
inline bool WiiRemotesEnabled(bool fallback = true) {
// Bluetooth Wii Remotes. Read once before SDL's joystick subsystem comes up, so a
// change only takes effect on the next launch.
inline bool WiiRemotesEnabled(bool fallback = false) {
return Get().wiiRemotes.value_or(fallback);
}
// Persists the Bluetooth Wii Remote driver switch.
inline bool SetWiiRemotesEnabled(bool value) {
Mutable().wiiRemotes = value;
return WriteSetting("controller", "wii_remotes", value ? "true" : "false");
}
// Whether to keep rescanning Bluetooth while no Wii controller is connected.
inline bool WiiContinuousScanEnabled(bool fallback = false) {
return Get().wiiContinuousScan.value_or(fallback);
inline bool WiiAutoPairEnabled(bool fallback = true) {
return Get().wiiAutoPair.value_or(fallback);
}
// Persists the continuous scanning switch.
inline bool SetWiiContinuousScanEnabled(bool value) {
Mutable().wiiContinuousScan = value;
return WriteSetting("controller", "wii_continuous_scan", value ? "true" : "false");
inline bool SetWiiAutoPairEnabled(bool value) {
Mutable().wiiAutoPair = value;
return WriteSetting("controller", "wii_auto_pair", value ? "true" : "false");
}
// Stored correction of the remote identified by `remoteKey`, if it has one.
inline std::optional<std::array<double, 3>> WiiAccelOffsetFor(const std::string& remoteKey) {
const auto it = Get().wiiAccelOffsets.find(remoteKey);
if (it == Get().wiiAccelOffsets.end()) {
return std::nullopt;
}
return it->second;
// Defaults are the console's.
inline bool WiiSensorBarAbove(bool fallback = false) {
return Get().wiiSensorBarAbove.value_or(fallback);
}
// Whether to write the per-frame accelerometer trace (off unless asked for).
inline bool WiiAccelTraceEnabled(bool fallback = false) {
return Get().wiiAccelTrace.value_or(fallback);
inline bool SetWiiSensorBarAbove(bool value) {
Mutable().wiiSensorBarAbove = value;
return WriteSetting("controller", "wii_sensor_bar", value ? "\"above\"" : "\"below\"");
}
// Persists the accelerometer correction of one remote. `remoteKey` must be a bare TOML key.
inline bool SetWiiAccelOffsetFor(const std::string& remoteKey, const std::array<double, 3>& offset) {
Mutable().wiiAccelOffsets[remoteKey] = offset;
// Always float literals, so a whole-number offset does not come back as a TOML integer.
std::ostringstream formatted;
formatted << std::fixed << std::setprecision(4) << '[' << offset[0] << ", " << offset[1] << ", " << offset[2]
<< ']';
return WriteSetting("controller.wii_accel_offsets", remoteKey, formatted.str());
inline int32_t WiiIrSensitivity(int32_t fallback = 3) {
return Get().wiiIrSensitivity.value_or(fallback);
}
inline bool SetWiiIrSensitivity(int32_t value) {
Mutable().wiiIrSensitivity = value;
return WriteSetting("controller", "wii_ir_sensitivity", std::to_string(value));
}
inline int32_t WiiSpeakerVolume(int32_t fallback = 0x59) {
return Get().wiiSpeakerVolume.value_or(fallback);
}
inline bool SetWiiSpeakerVolume(int32_t value) {
Mutable().wiiSpeakerVolume = value;
return WriteSetting("controller", "wii_speaker_volume", std::to_string(value));
}
inline bool WiiRumbleEnabled(bool fallback = true) {
return Get().wiiRumble.value_or(fallback);
}
inline bool SetWiiRumbleEnabled(bool value) {
Mutable().wiiRumble = value;
return WriteSetting("controller", "wii_rumble", value ? "true" : "false");
}
// Target frame rate for frame interpolation, or 0 to disable it.
-111
View File
@@ -1,111 +0,0 @@
#pragma once
#include <array>
#include <cstdint>
#include <optional>
struct PADStatus;
// Real Wii Remotes paired over Bluetooth.
//
// SDL 3 ships a HIDAPI driver for them (Wii Remote alone, with Nunchuk, with a
// Classic Controller, and the Wii U Pro Controller) that exposes each as a
// regular SDL gamepad; it is off by default on SDL's side and ConfigureSdlHints
// turns it on. A remote, alone or with a Nunchuk or Classic Controller, is
// handed to the game as a real Wii Remote with that extension: the KPAD/WPAD
// HLE builds a KPADStatus (and the WPADCLStatus behind KPADGetUnifiedWpadStatus)
// from it every frame (see ReadKpadSample), so the game's own code does wheelies,
// tricks, Wii Wheel steering and the Classic Controller layout, and plugging an
// extension in or out mid-game switches control scheme like on the console. Only
// the Wii U Pro Controller, which has no Wii-era equivalent, goes through
// aurora's PAD layer as a GameCube pad.
namespace WiiRemoteInput {
enum class Kind : uint8_t {
NotWii,
Remote, // Wii Remote with no extension
RemoteWithNunchuk,
RemoteWithClassic,
WiiUPro,
};
// Must run before SDL's joystick subsystem is initialized (aurora does that
// inside aurora_initialize); SDL only consults the hint on its first device scan.
void ConfigureSdlHints(bool enabled);
// Classifies a gamepad by the name SDL's Wii driver reports for it.
Kind KindForName(const char* gamepadName);
// Classification of the SDL gamepad currently assigned to a game port.
Kind KindForPort(uint32_t port);
const char* KindLabel(Kind kind);
// One frame of a Wii Remote in the units KPAD uses.
struct KpadSample {
uint32_t hold = 0; // WPAD button bits (WPAD_BUTTON_*), Nunchuk C/Z included
float acc[3] = {}; // remote accelerometer in g, KPAD frame (rest: y = -1)
bool hasNunchuk = false;
float stick[2] = {}; // Nunchuk stick, -1..1, +y up
float nunchukAcc[3] = {};
bool hasClassic = false;
uint32_t clHold = 0; // WPAD_CL_BUTTON_* bits
float clLStick[2] = {}; // Classic sticks, -1..1, +y up
float clRStick[2] = {};
int16_t clLStickRaw[2] = {}; // as WPADCLStatus reports them: -512..511, centre 0, +y up
int16_t clRStickRaw[2] = {};
uint8_t clTriggerL = 0; // 0..255; SDL only exposes the digital click
uint8_t clTriggerR = 0;
};
// What the game should see on `chan`: the controller SDL has there right now,
// or, for a few seconds after a remote vanished, the kind it had. SDL's driver
// destroys and re-creates the joystick when an extension is plugged in or out,
// and the console never disconnects for that, so the gap is papered over with
// neutral input instead of a "communications interrupted" prompt.
Kind EffectiveKind(uint32_t chan);
// True when the game reads `chan` through KPAD: a Wii Remote alone, with a
// Nunchuk or with a Classic Controller (live or within the swap grace period).
bool IsRemoteChannel(uint32_t chan);
// Reads the current state of the remote on `chan`; false when IsRemoteChannel
// is false. During the swap grace period the sample is neutral.
bool ReadKpadSample(uint32_t chan, KpadSample& sample);
// Remote accelerometer for the overlay readout: the SDL sample in g after the
// zero-point correction (x right across the face, y out of the button face, z
// towards the user) and the KPAD vector built from it. False when the port has
// no remote or SDL has not delivered a sample yet.
bool ReadAccelDebug(uint32_t chan, float sdlG[3], float kpadAcc[3]);
// Residual zero-point trim on top of SDL's factory-calibrated decode, stored per
// physical remote under [controller.wii_accel_offsets]. Expects the remote lying
// still, buttons up; Poll() averages about a second and stores it for that remote.
void StartAccelCalibration(uint32_t chan);
// Forgets the stored correction of the remote on `chan`.
void ClearAccelCalibration(uint32_t chan);
bool IsAccelCalibrating(uint32_t chan);
// 0..1 while a calibration is collecting samples.
float AccelCalibrationProgress();
// Correction in effect for the remote on `chan` (zeros when uncalibrated), or
// nullopt when the port has no remote.
std::optional<std::array<double, 3>> ReadAccelOffset(uint32_t chan);
// Outcome of the last calibration attempt on `chan` for the overlay, or nullptr.
const char* AccelCalibrationMessage(uint32_t chan);
// Reports "no controller" on the GameCube side for every port served through
// KPAD, so the game never sees the same remote twice. Runs after aurora's PADRead.
void HideRemotesFromPad(PADStatus* statuses, uint32_t count);
// Dolphin-style continuous scanning. SDL's HIDAPI Wii driver drops a remote on
// a failed Bluetooth read or an extension change and never re-adds it on its
// own. Poll() runs once per PADRead and, while no Wii controller is present,
// periodically forces SDL to re-enumerate HIDAPI by toggling the driver hint.
void Poll();
// Forces one re-enumeration right now (settings overlay "Rescan now").
void RescanNow();
// True while Poll() is looking for a remote (no Wii controller connected).
bool IsScanning();
// True where looking means periodic rescans; elsewhere Poll() waits for hotplug.
bool PeriodicRescanEnabled();
// Rescans issued since a Wii controller was last seen.
uint32_t ScanCount();
} // namespace WiiRemoteInput
+24
View File
@@ -0,0 +1,24 @@
#pragma once
// The Wii's extension controller encryption: the game writes a random key to
// the extension, which then XORs and offsets every byte it reports.
#include <array>
#include <cstddef>
#include <cstdint>
#include <optional>
namespace Wiimote::ExtCrypto {
struct Tables {
std::array<uint8_t, 8> ft{};
std::array<uint8_t, 8> sb{};
void Encrypt(uint8_t* data, uint32_t address, size_t length) const;
void Decrypt(uint8_t* data, uint32_t address, size_t length) const;
};
// Tables for the key the game wrote to registers 0x40..0x4F, or nullopt when it
// does not come from the first-party key schedule.
std::optional<Tables> FromKey(const std::array<uint8_t, 16>& keyData);
} // namespace Wiimote::ExtCrypto
+40
View File
@@ -0,0 +1,40 @@
#pragma once
// Follows the game's conversation with a remote's extension (type, calibration,
// encryption key) so its data can be put at rest while the overlay owns input.
#include "wiimote_ext_crypto.h"
#include <array>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <vector>
namespace Wiimote {
class ExtensionState {
public:
void ObserveOutput(const uint8_t* report, size_t size);
void ObserveInput(const uint8_t* report, size_t size);
// Centres the sticks and releases the buttons of the extension data in a
// data report, leaving its motion data alone.
void Neutralize(std::vector<uint8_t>& report) const;
private:
enum class Type : uint8_t { Unknown, Nunchuk, Classic };
void Reset();
uint8_t Calibration(size_t index, uint8_t fallback) const;
Type m_type = Type::Unknown;
uint8_t m_format = 0;
bool m_encrypted = false;
std::array<uint8_t, 16> m_key{};
std::optional<ExtCrypto::Tables> m_tables;
std::array<uint8_t, 16> m_calibration{};
bool m_haveCalibration = false;
uint32_t m_readAddress = 0;
};
} // namespace Wiimote
+52
View File
@@ -0,0 +1,52 @@
#pragma once
// Physical Wii Remotes on the PC. Each one is attached to one of the four
// virtual remotes of the emulated Bluetooth controller and its HID reports are
// passed through unchanged, so the game's own WPAD/KPAD drive it (IR, speaker,
// rumble, extensions, battery).
#include "bluetooth/bt_controller.h"
#include <cstddef>
#include <string>
namespace Wiimote {
struct SlotInfo {
bool attached = false;
// The game disconnected it and the platform could not power it off; any
// button wakes it up again.
bool asleep = false;
Bluetooth::LinkState link = Bluetooth::LinkState::Down;
std::string model; // "Wii Remote" / "Wii Remote Plus"
std::string extension; // empty when none
int battery = -1; // 0..4 like the game's battery bars, -1 unknown
};
struct PairingInfo {
bool supported = false; // this platform can discover and pair remotes itself
bool searching = false;
std::string status; // last pairing outcome, for the overlay
};
// Must run before SDL's joystick subsystem starts (aurora_initialize).
void ConfigureSdlHints(bool enabled);
// Starts device scanning when Wii Remotes are enabled.
void Initialize();
// Powers the remotes off, like a console shutting down, and stops the threads.
void Shutdown();
Bluetooth::RemoteHost& Host();
SlotInfo Slot(size_t slot);
PairingInfo Pairing();
// The console's SYNC button: look for remotes in pairing mode for a while.
void StartPairing();
// Ends the game's connection to the remote on `slot` and powers it off.
void Disconnect(size_t slot);
// Setup problems worth showing (kernel driver, missing permissions), else empty.
std::string PlatformNotice();
// Connected remotes waiting for a free player slot.
size_t WaitingRemotes();
} // namespace Wiimote
+43
View File
@@ -0,0 +1,43 @@
#pragma once
// Host Bluetooth services the Wii Remote manager needs from the OS: finding and
// pairing remotes in pairing mode, naming a HID device, and dropping a remote's
// connection (which powers it off, as a console does).
#include <atomic>
#include <string>
struct SDL_hid_device_info;
namespace Wiimote::Platform {
struct Capabilities {
bool pairing = false;
bool powerOff = false;
};
Capabilities Query();
// One search for remotes in pairing mode (red SYNC or 1+2); pairs and connects
// each one found. Blocks for a few seconds. Returns the number connected.
int DiscoverAndPair(const std::atomic<bool>& cancel, std::string& status);
// Disconnects the remote; it switches itself off. False when not possible.
bool PowerOff(const std::string& address);
// Bluetooth address of a HID device as 12 lowercase hex digits, or empty.
std::string AddressFor(const SDL_hid_device_info& info);
// Bluetooth name ("Nintendo RVL-CNT-01-TR", ...) or the HID product string.
std::string NameFor(const std::string& address, const SDL_hid_device_info& info);
// False only when the OS knows the remote is not connected right now.
bool MaybeConnected(const std::string& address);
// False when the OS driver would fight over the device (Linux hid-wiimote).
bool Usable(const SDL_hid_device_info& info);
// Setup problems worth showing in the overlay, else empty.
std::string Notice();
} // namespace Wiimote::Platform