Real Wii Remote support over Bluetooth (#323)

* pass real wii remotes through emulated bluetooth

* build bluetooth tests on macos ci

* address review comments

* cancel replaced bluetooth reads
This commit is contained in:
patchzyy
2026-10-20 12:21:46 +02:00
committed by GitHub
parent e3f5e3a3ec
commit dfe32f61e1
44 changed files with 5548 additions and 1964 deletions
+1
View File
@@ -102,6 +102,7 @@ jobs:
mkw_nand_settings_tests \
mkw_sc_serial_tests \
mkw_input_expr_tests \
mkw_bluetooth_controller_tests \
mkw_macos_native_compile \
mkw_macos_context_abi_tests \
mkw_macos_host_context_tests \
+10
View File
@@ -63,6 +63,16 @@ target_include_directories(mkw_input_expr_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}
target_compile_features(mkw_input_expr_tests PRIVATE cxx_std_17)
add_test(NAME mkw_input_expr_tests COMMAND mkw_input_expr_tests)
# The emulated Bluetooth controller is pure host code.
add_executable(mkw_bluetooth_controller_tests
"${MKW_TEST_RUNTIME_DIR}/tests/bluetooth_controller_tests.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/hle/bluetooth/bt_controller.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/hle/bluetooth/bt_remote_link.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/hle/bluetooth/bt_sdp.cpp")
target_include_directories(mkw_bluetooth_controller_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
target_compile_features(mkw_bluetooth_controller_tests PRIVATE cxx_std_20)
add_test(NAME mkw_bluetooth_controller_tests COMMAND mkw_bluetooth_controller_tests)
# HostContext deliberately keeps the platform-specific context primitive out
# of fiber_manager.cpp. Exercise the Linux libco handoff directly so future
# refactors cannot silently remove its headers, implementation, or link edge.
+1 -1
View File
@@ -206,7 +206,7 @@ function(mkw_configure_product target)
if(MKW_PLATFORM_WINDOWS)
target_link_libraries(${target} PRIVATE
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp bthprops)
set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE)
elseif(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64)
+1 -1
View File
@@ -66,7 +66,7 @@ function(mkw_runtime_link_dependencies target scope)
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
target_link_libraries(${target} ${scope} mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
if(MKW_PLATFORM_WINDOWS)
target_link_libraries(${target} ${scope} shell32 windowsapp)
target_link_libraries(${target} ${scope} shell32 windowsapp bthprops)
elseif(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64)
# ${CMAKE_DL_LIBS} for music_attenuation.cpp's dlopen of libdbus-1 (MPRIS
# media monitoring). Empty on platforms where dl* is already in libc/libSystem.
+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
+951
View File
@@ -0,0 +1,951 @@
#include "bluetooth/bt_controller.h"
#include "bluetooth/bt_bytes.h"
#include "bluetooth/bt_remote_link.h"
#include <algorithm>
#include <cstring>
#include <iostream>
namespace Bluetooth {
namespace HciDetail {
constexpr uint16_t kOgfLinkControl = 0x01;
constexpr uint16_t kOgfLinkPolicy = 0x02;
constexpr uint16_t kOgfControllerBaseband = 0x03;
constexpr uint16_t kOgfInformational = 0x04;
constexpr uint16_t kOgfStatus = 0x05;
constexpr uint16_t kOgfVendor = 0x3F;
constexpr uint8_t kEventInquiryComplete = 0x01;
constexpr uint8_t kEventInquiryResult = 0x02;
constexpr uint8_t kEventConnectionComplete = 0x03;
constexpr uint8_t kEventDisconnectionComplete = 0x05;
constexpr uint8_t kEventAuthenticationComplete = 0x06;
constexpr uint8_t kEventRemoteNameComplete = 0x07;
constexpr uint8_t kEventEncryptionChange = 0x08;
constexpr uint8_t kEventRemoteFeatures = 0x0B;
constexpr uint8_t kEventRemoteVersion = 0x0C;
constexpr uint8_t kEventCommandComplete = 0x0E;
constexpr uint8_t kEventCommandStatus = 0x0F;
constexpr uint8_t kEventRoleChange = 0x12;
constexpr uint8_t kEventNumberOfCompletedPackets = 0x13;
constexpr uint8_t kEventModeChange = 0x14;
constexpr uint8_t kEventReturnLinkKeys = 0x15;
constexpr uint8_t kEventPinCodeRequest = 0x16;
constexpr uint8_t kEventLinkKeyRequest = 0x17;
constexpr uint8_t kEventLinkKeyNotification = 0x18;
constexpr uint8_t kEventClockOffset = 0x1C;
constexpr uint8_t kEventPacketTypeChanged = 0x1D;
constexpr uint8_t kEventInquiryResultWithRssi = 0x22;
constexpr uint8_t kEventExtendedInquiryResult = 0x2F;
constexpr uint8_t kStatusSuccess = 0x00;
constexpr uint8_t kStatusUnknownCommand = 0x01;
constexpr uint8_t kStatusUnknownConnection = 0x02;
constexpr uint8_t kStatusPageTimeout = 0x04;
constexpr uint8_t kStatusAuthenticationFailure = 0x05;
constexpr uint8_t kStatusKeyMissing = 0x06;
constexpr uint8_t kStatusUnsupportedFeature = 0x11;
constexpr uint8_t kReasonLocalHostTerminated = 0x16;
// What a Wii's BCM2045 reports (the version makes WUD apply its usual patch).
constexpr BdAddr kLocalAddress = {0x11, 0x02, 0x19, 0x79, 0x00, 0xFF};
constexpr std::array<uint8_t, 8> kLocalFeatures = {0xFF, 0xFF, 0x8D, 0xFE, 0x9B, 0xF9, 0x00, 0x80};
// Never advertise more than 10 ACL buffers: the stack keeps the count in a u8.
constexpr uint16_t kAclBufferSize = 339;
constexpr uint16_t kAclBufferCount = 10;
constexpr uint8_t kScoBufferSize = 64;
// A real RVL-CNT-01.
constexpr std::array<uint8_t, 8> kRemoteFeatures = {0xBC, 0x02, 0x04, 0x38, 0x08, 0x00, 0x00, 0x00};
constexpr uint8_t kRemoteLmpVersion = 0x02;
constexpr uint16_t kRemoteManufacturer = 0x000F;
constexpr uint16_t kRemoteLmpSubversion = 0x0229;
constexpr uint16_t kRemoteClockOffset = 0x3818;
constexpr uint8_t kInquiryRssi = 0xD8; // -40 dBm
constexpr uint64_t kInquiryUnitUs = 1'280'000;
constexpr uint16_t kMaxStoredKeys = 0x00FF;
constexpr uint8_t kKeysPerEvent = 11;
void PutAddr(std::vector<uint8_t>& out, const BdAddr& addr) {
out.insert(out.end(), addr.begin(), addr.end());
}
BdAddr ReadAddr(const uint8_t* p) {
BdAddr addr{};
std::memcpy(addr.data(), p, addr.size());
return addr;
}
std::vector<uint8_t> Event(uint8_t code, const std::vector<uint8_t>& params) {
std::vector<uint8_t> event;
event.reserve(2 + params.size());
event.push_back(code);
event.push_back(static_cast<uint8_t>(params.size()));
event.insert(event.end(), params.begin(), params.end());
return event;
}
std::array<uint8_t, 16> SlotLinkKey(size_t slot) {
std::array<uint8_t, 16> key{};
for (size_t i = 0; i < key.size(); ++i) {
key[i] = static_cast<uint8_t>(0xA0 + slot + i * 0x11);
}
return key;
}
} // namespace HciDetail
using namespace HciDetail;
Controller::Controller(RemoteHost& host) : m_host(host) {
for (size_t slot = 0; slot < kRemoteSlots; ++slot) {
m_links[slot] = std::make_unique<RemoteLink>(*this, slot);
m_storedKeys.push_back({RemoteAddress(slot), SlotLinkKey(slot)});
}
ResetState(true);
}
Controller::~Controller() = default;
BdAddr Controller::LocalAddress() {
return kLocalAddress;
}
BdAddr Controller::RemoteAddress(size_t slot) {
return {0x11, 0x02, 0x19, 0x79, 0x00, static_cast<uint8_t>(slot)};
}
uint16_t Controller::ConnectionHandle(size_t slot) {
return static_cast<uint16_t>(0x0100 + slot);
}
void Controller::Open(uint64_t nowUs) {
(void)nowUs;
ResetState(true);
m_open = true;
}
void Controller::Close() {
ResetState(true);
m_open = false;
}
void Controller::ResetState(bool keepStoredKeys) {
for (size_t slot = 0; slot < kRemoteSlots; ++slot) {
RemoteLink& link = *m_links[slot];
// A reset drops the radio links: the remotes lose their connection.
if (link.State() == LinkState::Ready) {
m_host.OnGuestDisconnect(slot);
}
link.Reset();
}
if (!keepStoredKeys) {
m_storedKeys.clear();
}
if (m_hostInquiryNotified) {
m_host.OnGuestInquiry(false);
m_hostInquiryNotified = false;
}
m_inquiry = {};
m_scanEnable = 0;
m_pageTimeoutSlots = 0x2000;
m_inquiryMode = 0;
m_classOfDevice = {};
m_localName = {};
m_completedPackets = {};
for (Reassembly& r : m_reassembly) r = {};
m_events.clear();
m_acl.clear();
m_pending.clear();
m_authPending = {};
}
LinkState Controller::SlotLinkState(size_t slot) const {
return slot < kRemoteSlots ? m_links[slot]->State() : LinkState::Down;
}
int Controller::SlotForAddress(const BdAddr& addr) const {
for (size_t slot = 0; slot < kRemoteSlots; ++slot) {
if (RemoteAddress(slot) == addr) return static_cast<int>(slot);
}
return -1;
}
int Controller::SlotForHandle(uint16_t handle) const {
for (size_t slot = 0; slot < kRemoteSlots; ++slot) {
if (ConnectionHandle(slot) == (handle & 0x0FFF) &&
m_links[slot]->BasebandState() == RemoteLink::Baseband::Connected) {
return static_cast<int>(slot);
}
}
return -1;
}
const Controller::StoredKey* Controller::FindStoredKey(const BdAddr& addr) const {
for (const StoredKey& key : m_storedKeys) {
if (key.addr == addr) return &key;
}
return nullptr;
}
void Controller::QueueEvent(std::vector<uint8_t> event) {
m_events.push_back(std::move(event));
}
void Controller::QueueAcl(uint16_t handle, const std::vector<uint8_t>& l2capFrame) {
std::vector<uint8_t> packet;
packet.reserve(4 + l2capFrame.size());
// Packet boundary 0x2: first (and only) fragment, automatically flushable.
PutLe16(packet, static_cast<uint16_t>((handle & 0x0FFF) | 0x2000));
PutLe16(packet, static_cast<uint16_t>(l2capFrame.size()));
packet.insert(packet.end(), l2capFrame.begin(), l2capFrame.end());
m_acl.push_back(std::move(packet));
}
bool Controller::PopEvent(std::vector<uint8_t>& event) {
if (m_events.empty()) return false;
event = std::move(m_events.front());
m_events.pop_front();
return true;
}
bool Controller::PopAcl(std::vector<uint8_t>& packet) {
if (!m_events.empty() || m_acl.empty()) return false;
packet = std::move(m_acl.front());
m_acl.pop_front();
return true;
}
void Controller::CommandComplete(uint16_t opcode, const std::vector<uint8_t>& params) {
std::vector<uint8_t> body = {0x01};
PutLe16(body, opcode);
body.insert(body.end(), params.begin(), params.end());
QueueEvent(Event(kEventCommandComplete, body));
}
void Controller::CommandStatus(uint16_t opcode, uint8_t status) {
std::vector<uint8_t> body = {status, 0x01};
PutLe16(body, opcode);
QueueEvent(Event(kEventCommandStatus, body));
}
void Controller::UnknownCommand(uint16_t opcode) {
std::cerr << "[bluetooth] unsupported HCI command 0x" << std::hex << opcode << std::dec << std::endl;
CommandStatus(opcode, kStatusUnknownCommand);
}
void Controller::ExecuteCommand(const uint8_t* packet, size_t size, uint64_t nowUs) {
if (size < 3) return;
const uint16_t opcode = Le16(packet);
const size_t length = packet[2];
if (3 + length > size) {
std::cerr << "[bluetooth] truncated HCI command 0x" << std::hex << opcode << std::dec << std::endl;
return;
}
const uint8_t* p = packet + 3;
switch (opcode >> 10) {
case kOgfLinkControl: HandleLinkControl(opcode, p, length, nowUs); break;
case kOgfLinkPolicy: HandleLinkPolicy(opcode, p, length); break;
case kOgfControllerBaseband: HandleControllerBaseband(opcode, p, length, nowUs); break;
case kOgfInformational: HandleInformational(opcode, p, length); break;
case kOgfStatus: HandleStatusParameters(opcode, p, length); break;
case kOgfVendor: HandleVendor(opcode, p, length); break;
default: UnknownCommand(opcode); break;
}
}
void Controller::HandleLinkControl(uint16_t opcode, const uint8_t* p, size_t n, uint64_t nowUs) {
const auto handleSlot = [&](size_t offset) { return n >= offset + 2 ? SlotForHandle(Le16(p + offset)) : -1; };
switch (opcode & 0x3FF) {
case 0x0001: { // Inquiry
if (n < 5) break;
CommandStatus(opcode, kStatusSuccess);
m_inquiry = {};
m_inquiry.active = true;
m_inquiry.endUs = nowUs + std::max<uint8_t>(p[3], 1) * kInquiryUnitUs;
m_inquiry.maxResponses = p[4];
if (!m_hostInquiryNotified) {
m_host.OnGuestInquiry(true);
m_hostInquiryNotified = true;
}
return;
}
case 0x0002: // Inquiry Cancel
m_inquiry = {};
if (m_hostInquiryNotified) {
m_host.OnGuestInquiry(false);
m_hostInquiryNotified = false;
}
CommandComplete(opcode, {kStatusSuccess});
return;
case 0x0003: // Periodic Inquiry Mode
case 0x0004: // Exit Periodic Inquiry Mode
CommandComplete(opcode, {kStatusUnsupportedFeature});
return;
case 0x0005: { // Create Connection
if (n < 6) break;
const BdAddr addr = ReadAddr(p);
CommandStatus(opcode, kStatusSuccess);
const int slot = SlotForAddress(addr);
if (slot >= 0 && m_links[slot]->OnPaged(nowUs)) {
std::vector<uint8_t> body = {kStatusSuccess};
PutLe16(body, ConnectionHandle(slot));
PutAddr(body, addr);
body.insert(body.end(), {0x01, 0x00});
QueueEvent(Event(kEventConnectionComplete, body));
} else {
std::vector<uint8_t> body = {kStatusPageTimeout, 0x00, 0x00};
PutAddr(body, addr);
body.insert(body.end(), {0x01, 0x00});
m_pending.push_back({nowUs + PageTimeoutUs(), Event(kEventConnectionComplete, body)});
}
return;
}
case 0x0006: { // Disconnect
const int slot = handleSlot(0);
if (slot < 0 || n < 3) {
CommandStatus(opcode, kStatusUnknownConnection);
return;
}
CommandStatus(opcode, kStatusSuccess);
std::vector<uint8_t> body = {kStatusSuccess};
PutLe16(body, ConnectionHandle(slot));
body.push_back(kReasonLocalHostTerminated);
QueueEvent(Event(kEventDisconnectionComplete, body));
m_completedPackets[slot] = 0;
m_reassembly[slot] = {};
m_authPending[slot] = false;
m_links[slot]->OnDisconnected(true, nowUs);
return;
}
case 0x0008: { // Create Connection Cancel
if (n < 6) break;
const BdAddr addr = ReadAddr(p);
auto it = std::find_if(m_pending.begin(), m_pending.end(), [&](const PendingEvent& e) {
return e.event[0] == kEventConnectionComplete && ReadAddr(&e.event[5]) == addr;
});
std::vector<uint8_t> body = {static_cast<uint8_t>(it == m_pending.end() ? kStatusUnknownConnection
: kStatusSuccess)};
PutAddr(body, addr);
CommandComplete(opcode, body);
if (it != m_pending.end()) {
std::vector<uint8_t> complete = it->event;
complete[2] = kStatusUnknownConnection;
m_pending.erase(it);
QueueEvent(std::move(complete));
}
return;
}
case 0x0009: { // Accept Connection Request
if (n < 7) break;
const BdAddr addr = ReadAddr(p);
const uint8_t role = p[6];
const int slot = SlotForAddress(addr);
if (slot < 0 || !m_links[slot]->IsRequesting()) {
CommandStatus(opcode, kStatusUnknownConnection);
return;
}
CommandStatus(opcode, kStatusSuccess);
if (role == 0x00) {
std::vector<uint8_t> change = {kStatusSuccess};
PutAddr(change, addr);
change.push_back(0x00);
QueueEvent(Event(kEventRoleChange, change));
}
std::vector<uint8_t> body = {kStatusSuccess};
PutLe16(body, ConnectionHandle(slot));
PutAddr(body, addr);
body.insert(body.end(), {0x01, 0x00});
QueueEvent(Event(kEventConnectionComplete, body));
m_completedPackets[slot] = 0;
m_reassembly[slot] = {};
m_links[slot]->OnAccepted(role);
return;
}
case 0x000A: { // Reject Connection Request
if (n < 7) break;
const BdAddr addr = ReadAddr(p);
const int slot = SlotForAddress(addr);
if (slot < 0 || !m_links[slot]->IsRequesting()) {
CommandStatus(opcode, kStatusUnknownConnection);
return;
}
CommandStatus(opcode, kStatusSuccess);
std::vector<uint8_t> body = {p[6], 0x00, 0x00};
PutAddr(body, addr);
body.insert(body.end(), {0x01, 0x00});
QueueEvent(Event(kEventConnectionComplete, body));
m_links[slot]->OnRejected(p[6], nowUs);
return;
}
case 0x000B: { // Link Key Request Reply
if (n < 22) break;
const BdAddr addr = ReadAddr(p);
std::vector<uint8_t> body = {kStatusSuccess};
PutAddr(body, addr);
CommandComplete(opcode, body);
const int slot = SlotForAddress(addr);
if (slot >= 0 && m_authPending[slot]) {
m_authPending[slot] = false;
const bool match = std::equal(p + 6, p + 22, SlotLinkKey(slot).begin());
std::vector<uint8_t> auth = {static_cast<uint8_t>(match ? kStatusSuccess : kStatusKeyMissing)};
PutLe16(auth, ConnectionHandle(slot));
QueueEvent(Event(kEventAuthenticationComplete, auth));
}
return;
}
case 0x000C: { // Link Key Request Negative Reply
if (n < 6) break;
const BdAddr addr = ReadAddr(p);
std::vector<uint8_t> body = {kStatusSuccess};
PutAddr(body, addr);
CommandComplete(opcode, body);
const int slot = SlotForAddress(addr);
if (slot >= 0 && m_authPending[slot]) {
std::vector<uint8_t> request;
PutAddr(request, addr);
QueueEvent(Event(kEventPinCodeRequest, request));
}
return;
}
case 0x000D: { // PIN Code Request Reply
if (n < 23) break;
const BdAddr addr = ReadAddr(p);
std::vector<uint8_t> body = {kStatusSuccess};
PutAddr(body, addr);
CommandComplete(opcode, body);
const int slot = SlotForAddress(addr);
if (slot < 0 || !m_authPending[slot]) return;
m_authPending[slot] = false;
// A remote takes the host's address (SYNC button) or its own (1+2) as PIN.
const uint8_t pinLength = p[6];
const uint8_t* pin = p + 7;
const BdAddr local = LocalAddress();
const bool valid = pinLength == 6 && (std::equal(pin, pin + 6, local.begin()) ||
std::equal(pin, pin + 6, addr.begin()));
if (valid) {
std::vector<uint8_t> notification;
PutAddr(notification, addr);
const auto key = SlotLinkKey(slot);
notification.insert(notification.end(), key.begin(), key.end());
notification.push_back(0x00); // combination key
QueueEvent(Event(kEventLinkKeyNotification, notification));
}
std::vector<uint8_t> auth = {static_cast<uint8_t>(valid ? kStatusSuccess : kStatusAuthenticationFailure)};
PutLe16(auth, ConnectionHandle(slot));
QueueEvent(Event(kEventAuthenticationComplete, auth));
return;
}
case 0x000E: { // PIN Code Request Negative Reply
if (n < 6) break;
const BdAddr addr = ReadAddr(p);
std::vector<uint8_t> body = {kStatusSuccess};
PutAddr(body, addr);
CommandComplete(opcode, body);
const int slot = SlotForAddress(addr);
if (slot >= 0 && m_authPending[slot]) {
m_authPending[slot] = false;
std::vector<uint8_t> auth = {kStatusKeyMissing};
PutLe16(auth, ConnectionHandle(slot));
QueueEvent(Event(kEventAuthenticationComplete, auth));
}
return;
}
case 0x000F: { // Change Connection Packet Type
const int slot = handleSlot(0);
if (slot < 0 || n < 4) {
CommandStatus(opcode, kStatusUnknownConnection);
return;
}
CommandStatus(opcode, kStatusSuccess);
std::vector<uint8_t> body = {kStatusSuccess};
PutLe16(body, ConnectionHandle(slot));
PutLe16(body, Le16(p + 2));
QueueEvent(Event(kEventPacketTypeChanged, body));
return;
}
case 0x0011: { // Authentication Requested
const int slot = handleSlot(0);
if (slot < 0) {
CommandStatus(opcode, kStatusUnknownConnection);
return;
}
CommandStatus(opcode, kStatusSuccess);
m_authPending[slot] = true;
std::vector<uint8_t> request;
PutAddr(request, RemoteAddress(slot));
QueueEvent(Event(kEventLinkKeyRequest, request));
return;
}
case 0x0013: { // Set Connection Encryption
const int slot = handleSlot(0);
if (slot < 0 || n < 3) {
CommandStatus(opcode, kStatusUnknownConnection);
return;
}
CommandStatus(opcode, kStatusSuccess);
std::vector<uint8_t> body = {kStatusSuccess};
PutLe16(body, ConnectionHandle(slot));
body.push_back(p[2]);
QueueEvent(Event(kEventEncryptionChange, body));
return;
}
case 0x0019: { // Remote Name Request
if (n < 6) break;
const BdAddr addr = ReadAddr(p);
CommandStatus(opcode, kStatusSuccess);
const int slot = SlotForAddress(addr);
const bool reachable = slot >= 0 && (m_links[slot]->BasebandState() == RemoteLink::Baseband::Connected ||
m_links[slot]->Discoverable());
std::vector<uint8_t> body = {static_cast<uint8_t>(reachable ? kStatusSuccess : kStatusPageTimeout)};
PutAddr(body, addr);
std::array<char, 248> name{};
if (reachable) {
std::strncpy(name.data(), m_host.DeviceName(slot), name.size() - 1);
}
body.insert(body.end(), name.begin(), name.end());
if (reachable) {
QueueEvent(Event(kEventRemoteNameComplete, body));
} else {
m_pending.push_back({nowUs + PageTimeoutUs(), Event(kEventRemoteNameComplete, body)});
}
return;
}
case 0x001A: { // Remote Name Request Cancel
if (n < 6) break;
const BdAddr addr = ReadAddr(p);
auto it = std::find_if(m_pending.begin(), m_pending.end(), [&](const PendingEvent& e) {
return e.event[0] == kEventRemoteNameComplete && ReadAddr(&e.event[3]) == addr;
});
std::vector<uint8_t> body = {static_cast<uint8_t>(it == m_pending.end() ? kStatusUnknownConnection
: kStatusSuccess)};
PutAddr(body, addr);
CommandComplete(opcode, body);
if (it != m_pending.end()) {
std::vector<uint8_t> complete = it->event;
complete[2] = kStatusUnknownConnection;
m_pending.erase(it);
QueueEvent(std::move(complete));
}
return;
}
case 0x001B: { // Read Remote Supported Features
const int slot = handleSlot(0);
if (slot < 0) {
CommandStatus(opcode, kStatusUnknownConnection);
return;
}
CommandStatus(opcode, kStatusSuccess);
std::vector<uint8_t> body = {kStatusSuccess};
PutLe16(body, ConnectionHandle(slot));
body.insert(body.end(), kRemoteFeatures.begin(), kRemoteFeatures.end());
QueueEvent(Event(kEventRemoteFeatures, body));
return;
}
case 0x001D: { // Read Remote Version Information
const int slot = handleSlot(0);
if (slot < 0) {
CommandStatus(opcode, kStatusUnknownConnection);
return;
}
CommandStatus(opcode, kStatusSuccess);
std::vector<uint8_t> body = {kStatusSuccess};
PutLe16(body, ConnectionHandle(slot));
body.push_back(kRemoteLmpVersion);
PutLe16(body, kRemoteManufacturer);
PutLe16(body, kRemoteLmpSubversion);
QueueEvent(Event(kEventRemoteVersion, body));
return;
}
case 0x001F: { // Read Clock Offset
const int slot = handleSlot(0);
if (slot < 0) {
CommandStatus(opcode, kStatusUnknownConnection);
return;
}
CommandStatus(opcode, kStatusSuccess);
std::vector<uint8_t> body = {kStatusSuccess};
PutLe16(body, ConnectionHandle(slot));
PutLe16(body, kRemoteClockOffset);
QueueEvent(Event(kEventClockOffset, body));
return;
}
case 0x0007: // Add SCO Connection
case 0x0028: // Setup Synchronous Connection
case 0x0029: // Accept Synchronous Connection Request
case 0x002A: // Reject Synchronous Connection Request
CommandStatus(opcode, kStatusUnsupportedFeature);
return;
default:
UnknownCommand(opcode);
return;
}
std::cerr << "[bluetooth] short HCI command 0x" << std::hex << opcode << std::dec << std::endl;
CommandStatus(opcode, kStatusUnknownCommand);
}
void Controller::HandleLinkPolicy(uint16_t opcode, const uint8_t* p, size_t n) {
const int slot = n >= 2 ? SlotForHandle(Le16(p)) : -1;
const auto modeChange = [&](uint8_t mode, uint16_t interval) {
if (slot < 0) {
CommandStatus(opcode, kStatusUnknownConnection);
return;
}
CommandStatus(opcode, kStatusSuccess);
std::vector<uint8_t> body = {kStatusSuccess};
PutLe16(body, ConnectionHandle(static_cast<size_t>(slot)));
body.push_back(mode);
PutLe16(body, interval);
QueueEvent(Event(kEventModeChange, body));
};
const auto handleReply = [&](std::vector<uint8_t> extra) {
std::vector<uint8_t> body = {static_cast<uint8_t>(slot < 0 ? kStatusUnknownConnection : kStatusSuccess)};
PutLe16(body, n >= 2 ? Le16(p) : 0);
body.insert(body.end(), extra.begin(), extra.end());
CommandComplete(opcode, body);
};
switch (opcode & 0x3FF) {
case 0x0001: modeChange(0x01, n >= 4 ? Le16(p + 2) : 0); return; // Hold Mode
case 0x0003: modeChange(0x02, n >= 4 ? Le16(p + 2) : 0); return; // Sniff Mode
case 0x0004: modeChange(0x00, 0); return; // Exit Sniff Mode
case 0x0005: modeChange(0x03, n >= 4 ? Le16(p + 2) : 0); return; // Park State
case 0x0006: modeChange(0x00, 0); return; // Exit Park State
case 0x0009: handleReply({0x00}); return; // Role Discovery
case 0x000B: { // Switch Role
if (n < 7) break;
CommandStatus(opcode, kStatusSuccess);
std::vector<uint8_t> body = {kStatusSuccess};
body.insert(body.end(), p, p + 7);
QueueEvent(Event(kEventRoleChange, body));
return;
}
case 0x000C: handleReply({0x05, 0x00}); return; // Read Link Policy Settings
case 0x000D: handleReply({}); return; // Write Link Policy Settings
case 0x000E: CommandComplete(opcode, {kStatusSuccess, 0x05, 0x00}); return; // Read Default Link Policy
case 0x000F: CommandComplete(opcode, {kStatusSuccess}); return; // Write Default Link Policy
default:
UnknownCommand(opcode);
return;
}
CommandStatus(opcode, kStatusUnknownCommand);
}
void Controller::HandleControllerBaseband(uint16_t opcode, const uint8_t* p, size_t n, uint64_t nowUs) {
(void)nowUs;
const auto handleReply = [&](std::vector<uint8_t> extra) {
const int slot = n >= 2 ? SlotForHandle(Le16(p)) : -1;
std::vector<uint8_t> body = {static_cast<uint8_t>(slot < 0 ? kStatusUnknownConnection : kStatusSuccess)};
PutLe16(body, n >= 2 ? Le16(p) : 0);
body.insert(body.end(), extra.begin(), extra.end());
CommandComplete(opcode, body);
};
switch (opcode & 0x3FF) {
case 0x0003: // Reset
ResetState(true);
CommandComplete(opcode, {kStatusSuccess});
return;
case 0x000D: { // Read Stored Link Key
if (n < 7) break;
const BdAddr addr = ReadAddr(p);
const bool readAll = p[6] == 0x01;
std::vector<StoredKey> keys;
for (const StoredKey& key : m_storedKeys) {
if (readAll || key.addr == addr) keys.push_back(key);
}
for (size_t start = 0; start < keys.size(); start += kKeysPerEvent) {
const size_t count = std::min<size_t>(kKeysPerEvent, keys.size() - start);
std::vector<uint8_t> body = {static_cast<uint8_t>(count)};
for (size_t i = start; i < start + count; ++i) {
PutAddr(body, keys[i].addr);
body.insert(body.end(), keys[i].key.begin(), keys[i].key.end());
}
QueueEvent(Event(kEventReturnLinkKeys, body));
}
std::vector<uint8_t> body = {kStatusSuccess};
PutLe16(body, kMaxStoredKeys);
PutLe16(body, static_cast<uint16_t>(keys.size()));
CommandComplete(opcode, body);
return;
}
case 0x0011: { // Write Stored Link Key
if (n < 1) break;
const uint8_t count = p[0];
if (n < 1 + count * size_t{22}) break;
for (uint8_t i = 0; i < count; ++i) {
const uint8_t* entry = p + 1 + i * 22;
StoredKey key{ReadAddr(entry), {}};
std::copy(entry + 6, entry + 22, key.key.begin());
auto existing = std::find_if(m_storedKeys.begin(), m_storedKeys.end(),
[&](const StoredKey& k) { return k.addr == key.addr; });
if (existing != m_storedKeys.end()) {
*existing = key;
} else if (m_storedKeys.size() < kMaxStoredKeys) {
m_storedKeys.push_back(key);
}
}
CommandComplete(opcode, {kStatusSuccess, count});
return;
}
case 0x0012: { // Delete Stored Link Key
if (n < 7) break;
const BdAddr addr = ReadAddr(p);
const bool deleteAll = p[6] == 0x01;
const size_t before = m_storedKeys.size();
m_storedKeys.erase(std::remove_if(m_storedKeys.begin(), m_storedKeys.end(),
[&](const StoredKey& k) { return deleteAll || k.addr == addr; }),
m_storedKeys.end());
std::vector<uint8_t> body = {kStatusSuccess};
PutLe16(body, static_cast<uint16_t>(before - m_storedKeys.size()));
CommandComplete(opcode, body);
return;
}
case 0x0013: // Change Local Name
if (n < m_localName.size()) break;
std::memcpy(m_localName.data(), p, m_localName.size());
CommandComplete(opcode, {kStatusSuccess});
return;
case 0x0014: { // Read Local Name
std::vector<uint8_t> body = {kStatusSuccess};
body.insert(body.end(), m_localName.begin(), m_localName.end());
CommandComplete(opcode, body);
return;
}
case 0x0018: // Write Page Timeout
if (n < 2) break;
m_pageTimeoutSlots = Le16(p);
CommandComplete(opcode, {kStatusSuccess});
return;
case 0x0019: // Read Scan Enable
CommandComplete(opcode, {kStatusSuccess, m_scanEnable});
return;
case 0x001A: // Write Scan Enable
if (n < 1) break;
m_scanEnable = p[0];
CommandComplete(opcode, {kStatusSuccess});
return;
case 0x0023: // Read Class of Device
CommandComplete(opcode, {kStatusSuccess, m_classOfDevice[0], m_classOfDevice[1], m_classOfDevice[2]});
return;
case 0x0024: // Write Class of Device
if (n < 3) break;
std::copy(p, p + 3, m_classOfDevice.begin());
CommandComplete(opcode, {kStatusSuccess});
return;
case 0x0045: // Write Inquiry Mode
if (n < 1) break;
m_inquiryMode = p[0];
CommandComplete(opcode, {kStatusSuccess});
return;
case 0x0028: // Write Automatic Flush Timeout
case 0x0037: // Write Link Supervision Timeout
handleReply({});
return;
case 0x0036: // Read Link Supervision Timeout
handleReply({0x80, 0x0C});
return;
case 0x0001: // Set Event Mask
case 0x0005: // Set Event Filter
case 0x000A: // Write PIN Type
case 0x0016: // Write Connection Accept Timeout
case 0x001C: // Write Page Scan Activity
case 0x001E: // Write Inquiry Scan Activity
case 0x0020: // Write Authentication Enable
case 0x0022: // Write Encryption Mode
case 0x0031: // Set Controller To Host Flow Control
case 0x0033: // Host Buffer Size
case 0x003A: // Write Current IAC LAP
case 0x003F: // Set AFH Host Channel Classification
case 0x0043: // Write Inquiry Scan Type
case 0x0047: // Write Page Scan Type
CommandComplete(opcode, {kStatusSuccess});
return;
case 0x0035: // Host Number Of Completed Packets: no event
return;
default:
UnknownCommand(opcode);
return;
}
std::cerr << "[bluetooth] short HCI command 0x" << std::hex << opcode << std::dec << std::endl;
CommandComplete(opcode, {kStatusUnknownCommand});
}
void Controller::HandleInformational(uint16_t opcode, const uint8_t* p, size_t n) {
(void)p;
(void)n;
switch (opcode & 0x3FF) {
case 0x0001: { // Read Local Version Information
std::vector<uint8_t> body = {kStatusSuccess, 0x03};
PutLe16(body, 0x40A7);
body.push_back(0x03);
PutLe16(body, 0x000F);
PutLe16(body, 0x430E);
CommandComplete(opcode, body);
return;
}
case 0x0003: { // Read Local Supported Features
std::vector<uint8_t> body = {kStatusSuccess};
body.insert(body.end(), kLocalFeatures.begin(), kLocalFeatures.end());
CommandComplete(opcode, body);
return;
}
case 0x0005: { // Read Buffer Size
std::vector<uint8_t> body = {kStatusSuccess};
PutLe16(body, kAclBufferSize);
body.push_back(kScoBufferSize);
PutLe16(body, kAclBufferCount);
PutLe16(body, 0);
CommandComplete(opcode, body);
return;
}
case 0x0009: { // Read BD_ADDR
std::vector<uint8_t> body = {kStatusSuccess};
PutAddr(body, LocalAddress());
CommandComplete(opcode, body);
return;
}
default:
UnknownCommand(opcode);
return;
}
}
void Controller::HandleStatusParameters(uint16_t opcode, const uint8_t* p, size_t n) {
const int slot = n >= 2 ? SlotForHandle(Le16(p)) : -1;
std::vector<uint8_t> body = {static_cast<uint8_t>(slot < 0 ? kStatusUnknownConnection : kStatusSuccess)};
PutLe16(body, n >= 2 ? Le16(p) : 0);
switch (opcode & 0x3FF) {
case 0x0003: // Get Link Quality
body.push_back(0xFF);
CommandComplete(opcode, body);
return;
case 0x0005: // Read RSSI: 0 = inside the golden receive power range
body.push_back(0x00);
CommandComplete(opcode, body);
return;
default:
UnknownCommand(opcode);
return;
}
}
void Controller::HandleVendor(uint16_t opcode, const uint8_t* p, size_t n) {
(void)p;
(void)n;
// WUD's firmware patch upload (0xFC4C writes, 0xFC4F install) and friends.
CommandComplete(opcode, {kStatusSuccess});
}
void Controller::ReceiveAcl(const uint8_t* packet, size_t size, uint64_t nowUs) {
(void)nowUs;
if (size < 4) return;
const uint16_t header = Le16(packet);
const uint16_t length = Le16(packet + 2);
if (length != size - 4) {
std::cerr << "[bluetooth] dropped malformed ACL packet" << std::endl;
return;
}
const int slot = SlotForHandle(header & 0x0FFF);
if (slot < 0) return;
++m_completedPackets[slot];
const uint8_t boundary = static_cast<uint8_t>((header >> 12) & 0x3);
Reassembly& r = m_reassembly[slot];
const uint8_t* data = packet + 4;
if (boundary == 0x1) {
if (r.expected == 0) return;
r.frame.insert(r.frame.end(), data, data + length);
} else {
r.frame.assign(data, data + length);
r.expected = length >= 2 ? static_cast<size_t>(Le16(data)) + 4 : 0;
}
if (r.expected != 0 && r.frame.size() >= r.expected) {
const std::vector<uint8_t> frame(r.frame.begin(), r.frame.begin() + static_cast<std::ptrdiff_t>(r.expected));
r = {};
m_links[slot]->ReceiveL2cap(frame.data(), frame.size());
}
}
void Controller::Tick(uint64_t nowUs) {
if (!m_open) return;
for (size_t i = 0; i < m_pending.size();) {
if (nowUs >= m_pending[i].dueUs) {
QueueEvent(std::move(m_pending[i].event));
m_pending.erase(m_pending.begin() + static_cast<std::ptrdiff_t>(i));
} else {
++i;
}
}
TickInquiry(nowUs);
for (auto& link : m_links) {
link->Tick(nowUs);
}
SendNumberOfCompletedPackets();
}
void Controller::TickInquiry(uint64_t nowUs) {
if (!m_inquiry.active) return;
if (nowUs >= m_inquiry.endUs ||
(m_inquiry.maxResponses != 0 && m_inquiry.reported >= m_inquiry.maxResponses)) {
FinishInquiry();
return;
}
// One result per tick, lowest slot first, so remotes keep their order.
for (size_t slot = 0; slot < kRemoteSlots; ++slot) {
if (m_inquiry.slotReported[slot] || !m_links[slot]->Discoverable()) continue;
m_inquiry.slotReported[slot] = true;
++m_inquiry.reported;
const BdAddr addr = RemoteAddress(slot);
std::vector<uint8_t> body = {0x01};
PutAddr(body, addr);
body.push_back(0x01); // page scan repetition mode R1
if (m_inquiryMode == 0) {
body.insert(body.end(), {0x00, 0x00, 0x04, 0x25, 0x00});
PutLe16(body, kRemoteClockOffset);
QueueEvent(Event(kEventInquiryResult, body));
} else {
body.insert(body.end(), {0x00, 0x04, 0x25, 0x00});
PutLe16(body, kRemoteClockOffset);
body.push_back(kInquiryRssi);
if (m_inquiryMode == 2) {
std::array<uint8_t, 240> eir{};
const char* name = m_host.DeviceName(slot);
const size_t nameLength = std::min<size_t>(std::strlen(name), eir.size() - 2);
eir[0] = static_cast<uint8_t>(nameLength + 1);
eir[1] = 0x09; // complete local name
std::memcpy(&eir[2], name, nameLength);
body.insert(body.end(), eir.begin(), eir.end());
QueueEvent(Event(kEventExtendedInquiryResult, body));
} else {
QueueEvent(Event(kEventInquiryResultWithRssi, body));
}
}
return;
}
}
void Controller::FinishInquiry() {
m_inquiry = {};
QueueEvent(Event(kEventInquiryComplete, {kStatusSuccess}));
if (m_hostInquiryNotified) {
m_host.OnGuestInquiry(false);
m_hostInquiryNotified = false;
}
}
void Controller::SendNumberOfCompletedPackets() {
std::vector<uint8_t> body = {0};
for (size_t slot = 0; slot < kRemoteSlots; ++slot) {
if (m_completedPackets[slot] == 0) continue;
PutLe16(body, ConnectionHandle(slot));
PutLe16(body, m_completedPackets[slot]);
m_completedPackets[slot] = 0;
++body[0];
}
if (body[0] != 0) {
QueueEvent(Event(kEventNumberOfCompletedPackets, body));
}
}
} // namespace Bluetooth
@@ -0,0 +1,567 @@
#include "bluetooth/bt_remote_link.h"
#include "bluetooth/bt_bytes.h"
#include "bluetooth/bt_sdp.h"
#include <algorithm>
#include <iostream>
namespace Bluetooth {
namespace L2capDetail {
constexpr uint16_t kSignallingCid = 0x0001;
constexpr uint16_t kFirstDynamicCid = 0x0040;
constexpr uint16_t kPsmSdp = 0x0001;
constexpr uint16_t kPsmHidControl = 0x0011;
constexpr uint16_t kPsmHidInterrupt = 0x0013;
constexpr uint8_t kCommandReject = 0x01;
constexpr uint8_t kConnectionRequest = 0x02;
constexpr uint8_t kConnectionResponse = 0x03;
constexpr uint8_t kConfigurationRequest = 0x04;
constexpr uint8_t kConfigurationResponse = 0x05;
constexpr uint8_t kDisconnectionRequest = 0x06;
constexpr uint8_t kDisconnectionResponse = 0x07;
constexpr uint8_t kEchoRequest = 0x08;
constexpr uint8_t kEchoResponse = 0x09;
constexpr uint8_t kInformationRequest = 0x0A;
constexpr uint8_t kInformationResponse = 0x0B;
constexpr uint16_t kResultSuccess = 0x0000;
constexpr uint16_t kResultPending = 0x0001;
constexpr uint16_t kResultPsmNotSupported = 0x0002;
constexpr uint16_t kResultNoResources = 0x0004;
constexpr uint8_t kOptionMtu = 0x01;
constexpr uint8_t kOptionFlushTimeout = 0x02;
// A real remote asks for this MTU on its own channels.
constexpr uint16_t kRemoteMtu = 185;
constexpr uint8_t kHidHandshake = 0x0;
constexpr uint8_t kHidControl = 0x1;
constexpr uint8_t kHidSetReport = 0x5;
constexpr uint8_t kHidData = 0xA;
constexpr uint8_t kHidParamOutput = 0x2;
constexpr uint8_t kHidHandshakeSuccess = 0x00;
constexpr uint8_t kHidHandshakeUnsupported = 0x03;
constexpr uint8_t kHidVirtualCableUnplug = 0x5;
constexpr uint8_t kHidDataInput = 0xA1;
constexpr uint64_t kRequestTimeoutUs = 10'000'000;
constexpr uint64_t kRetryDelayUs = 3'000'000;
constexpr uint8_t kReasonRemoteUserTerminated = 0x13;
// Output reports carry at most 21 payload bytes; anything longer is not a Wii Remote report.
constexpr size_t kMaxReportSize = 22;
constexpr size_t kInputBacklog = 8;
} // namespace L2capDetail
using namespace L2capDetail;
RemoteLink::RemoteLink(Controller& controller, size_t slot) : m_controller(controller), m_slot(slot) {}
void RemoteLink::Reset() {
ClearConnection();
m_baseband = Baseband::Idle;
SetState(LinkState::Down);
m_retryAtUs = 0;
}
void RemoteLink::ClearConnection() {
m_channels.clear();
m_guestInitiated = false;
m_everReady = false;
}
bool RemoteLink::Discoverable() const {
return m_baseband == Baseband::Idle && m_connectWanted && m_controller.Host().IsAttached(m_slot);
}
void RemoteLink::Tick(uint64_t nowUs) {
m_nowUs = nowUs;
RemoteHost& host = m_controller.Host();
if (m_baseband == Baseband::Idle) {
if (host.ConsumeWake(m_slot)) {
m_connectWanted = true;
m_retryAtUs = 0;
}
if (!host.IsAttached(m_slot)) {
m_connectWanted = false;
}
if (m_connectWanted && m_controller.PageScanEnabled() && nowUs >= m_retryAtUs) {
std::vector<uint8_t> event = {0x04, 10};
const BdAddr addr = Controller::RemoteAddress(m_slot);
event.insert(event.end(), addr.begin(), addr.end());
// Class of device 0x002504: peripheral, joystick.
event.insert(event.end(), {0x04, 0x25, 0x00, 0x01});
m_controller.QueueEvent(std::move(event));
m_baseband = Baseband::Requesting;
m_stateSinceUs = nowUs;
}
} else if (m_baseband == Baseband::Requesting) {
if (nowUs - m_stateSinceUs > kRequestTimeoutUs) {
m_baseband = Baseband::Idle;
m_retryAtUs = nowUs + kRetryDelayUs;
}
} else if (m_baseband == Baseband::Connected) {
if (!host.IsAttached(m_slot)) {
DropFromRemote(kReasonRemoteUserTerminated);
m_connectWanted = false;
return;
}
if (!m_guestInitiated) {
const Channel* control = FindByPsm(kPsmHidControl);
if (control == nullptr) {
RequestChannel(kPsmHidControl, nowUs);
} else if (control->Open() && FindByPsm(kPsmHidInterrupt) == nullptr) {
RequestChannel(kPsmHidInterrupt, nowUs);
}
}
for (const Channel& channel : m_channels) {
const bool waiting = (channel.weInitiated && !channel.connected) ||
(channel.configSent && !channel.localConfigured);
if (waiting && nowUs - channel.requestUs > kRequestTimeoutUs) {
std::cerr << "[bluetooth] Wii Remote " << (m_slot + 1) << ": L2CAP channel 0x" << std::hex
<< channel.psm << std::dec << " did not open, dropping the connection" << std::endl;
DropFromRemote(kReasonRemoteUserTerminated);
m_retryAtUs = nowUs + kRetryDelayUs;
return;
}
}
}
ForwardInput();
}
void RemoteLink::ForwardInput() {
RemoteHost& host = m_controller.Host();
if (m_state != LinkState::Ready) {
while (host.PopInputReport(m_slot, m_report)) {
}
return;
}
const Channel* interrupt = FindByPsm(kPsmHidInterrupt);
while (m_controller.AclQueued() < kInputBacklog && host.PopInputReport(m_slot, m_report)) {
if (m_report.empty() || m_report.size() > kMaxReportSize) continue;
SendInput(*interrupt, m_report);
}
}
void RemoteLink::SendInput(const Channel& interrupt, const std::vector<uint8_t>& report) {
std::vector<uint8_t> payload;
payload.reserve(report.size() + 1);
payload.push_back(kHidDataInput);
payload.insert(payload.end(), report.begin(), report.end());
if (payload.size() <= interrupt.remoteMtu) {
SendData(interrupt, payload);
}
}
void RemoteLink::OnAccepted(uint8_t role) {
(void)role;
m_baseband = Baseband::Connected;
m_guestInitiated = false;
m_everReady = false;
m_stateSinceUs = m_nowUs;
SetState(LinkState::Connecting);
}
void RemoteLink::OnRejected(uint8_t reason, uint64_t nowUs) {
std::cerr << "[bluetooth] Wii Remote " << (m_slot + 1) << ": connection rejected (0x" << std::hex
<< static_cast<int>(reason) << std::dec << ")" << std::endl;
m_baseband = Baseband::Idle;
m_retryAtUs = nowUs + kRetryDelayUs;
}
bool RemoteLink::OnPaged(uint64_t nowUs) {
if (!Discoverable()) {
return false;
}
m_nowUs = nowUs;
m_baseband = Baseband::Connected;
m_guestInitiated = true;
m_everReady = false;
m_stateSinceUs = nowUs;
SetState(LinkState::Connecting);
return true;
}
void RemoteLink::OnDisconnected(bool byGuest, uint64_t nowUs) {
const bool wasReady = m_everReady;
ClearConnection();
m_baseband = Baseband::Idle;
SetState(LinkState::Down);
if (byGuest && wasReady) {
m_connectWanted = false;
m_controller.Host().OnGuestDisconnect(m_slot);
} else {
m_retryAtUs = nowUs + kRetryDelayUs;
}
}
void RemoteLink::DropFromRemote(uint8_t reason) {
std::vector<uint8_t> event = {0x05, 4, 0x00};
PutLe16(event, Controller::ConnectionHandle(m_slot));
event.push_back(reason);
m_controller.QueueEvent(std::move(event));
ClearConnection();
m_baseband = Baseband::Idle;
SetState(LinkState::Down);
}
void RemoteLink::SetState(LinkState state) {
if (m_state == state) return;
m_state = state;
if (state == LinkState::Ready) {
m_everReady = true;
m_connectWanted = false;
}
m_controller.Host().OnLinkStateChanged(m_slot, state);
}
void RemoteLink::UpdateState() {
if (m_baseband != Baseband::Connected) {
SetState(LinkState::Down);
return;
}
const Channel* control = FindByPsm(kPsmHidControl);
const Channel* interrupt = FindByPsm(kPsmHidInterrupt);
const bool ready = control != nullptr && interrupt != nullptr && control->Open() && interrupt->Open();
SetState(ready ? LinkState::Ready : LinkState::Connecting);
}
RemoteLink::Channel* RemoteLink::FindByLocal(uint16_t cid) {
for (Channel& channel : m_channels) {
if (channel.localCid == cid) return &channel;
}
return nullptr;
}
RemoteLink::Channel* RemoteLink::FindByPsm(uint16_t psm) {
for (Channel& channel : m_channels) {
if (channel.psm == psm) return &channel;
}
return nullptr;
}
uint16_t RemoteLink::AllocateCid() const {
uint16_t cid = kFirstDynamicCid;
while (std::any_of(m_channels.begin(), m_channels.end(), [&](const Channel& c) { return c.localCid == cid; })) {
++cid;
}
return cid;
}
uint8_t RemoteLink::NextIdent() {
m_ident = static_cast<uint8_t>(m_ident + 1);
if (m_ident == 0) m_ident = 1;
return m_ident;
}
void RemoteLink::SendSignal(uint8_t code, uint8_t ident, const std::vector<uint8_t>& data) {
std::vector<uint8_t> frame;
frame.reserve(8 + data.size());
PutLe16(frame, static_cast<uint16_t>(4 + data.size()));
PutLe16(frame, kSignallingCid);
frame.push_back(code);
frame.push_back(ident);
PutLe16(frame, static_cast<uint16_t>(data.size()));
frame.insert(frame.end(), data.begin(), data.end());
m_controller.QueueAcl(Controller::ConnectionHandle(m_slot), frame);
}
void RemoteLink::SendData(const Channel& channel, const std::vector<uint8_t>& payload) {
std::vector<uint8_t> frame;
frame.reserve(4 + payload.size());
PutLe16(frame, static_cast<uint16_t>(payload.size()));
PutLe16(frame, channel.remoteCid);
frame.insert(frame.end(), payload.begin(), payload.end());
m_controller.QueueAcl(Controller::ConnectionHandle(m_slot), frame);
}
void RemoteLink::RequestChannel(uint16_t psm, uint64_t nowUs) {
Channel channel;
channel.psm = psm;
channel.localCid = AllocateCid();
channel.weInitiated = true;
channel.connectIdent = NextIdent();
channel.requestUs = nowUs;
std::vector<uint8_t> data;
PutLe16(data, psm);
PutLe16(data, channel.localCid);
SendSignal(kConnectionRequest, channel.connectIdent, data);
m_channels.push_back(channel);
}
void RemoteLink::SendConfigRequest(Channel& channel) {
channel.configSent = true;
channel.configIdent = NextIdent();
channel.requestUs = m_nowUs;
std::vector<uint8_t> data;
PutLe16(data, channel.remoteCid);
PutLe16(data, 0);
data.push_back(kOptionMtu);
data.push_back(2);
PutLe16(data, kRemoteMtu);
SendSignal(kConfigurationRequest, channel.configIdent, data);
}
void RemoteLink::ReceiveL2cap(const uint8_t* frame, size_t size) {
if (m_baseband != Baseband::Connected || size < 4) return;
const uint16_t length = Le16(frame);
const uint16_t cid = Le16(frame + 2);
if (length != size - 4) {
std::cerr << "[bluetooth] Wii Remote " << (m_slot + 1) << ": dropped malformed L2CAP frame" << std::endl;
return;
}
if (cid == kSignallingCid) {
HandleSignalling(frame + 4, length);
} else if (Channel* channel = FindByLocal(cid)) {
HandleChannelData(*channel, frame + 4, length);
}
}
void RemoteLink::HandleSignalling(const uint8_t* data, size_t size) {
while (size >= 4) {
const uint8_t code = data[0];
const uint8_t ident = data[1];
const uint16_t length = Le16(data + 2);
if (static_cast<size_t>(length) + 4 > size) {
return;
}
const uint8_t* p = data + 4;
switch (code) {
case kConnectionRequest: HandleConnectionRequest(ident, p, length); break;
case kConnectionResponse: HandleConnectionResponse(ident, p, length); break;
case kConfigurationRequest: HandleConfigurationRequest(ident, p, length); break;
case kConfigurationResponse: HandleConfigurationResponse(ident, p, length); break;
case kDisconnectionRequest: HandleDisconnectionRequest(ident, p, length); break;
case kDisconnectionResponse: HandleDisconnectionResponse(p, length); break;
case kEchoRequest: SendSignal(kEchoResponse, ident, std::vector<uint8_t>(p, p + length)); break;
case kInformationRequest: {
if (length >= 2) {
std::vector<uint8_t> reply;
PutLe16(reply, Le16(p));
PutLe16(reply, 0x0001); // not supported
SendSignal(kInformationResponse, ident, reply);
}
break;
}
case kCommandReject:
std::cerr << "[bluetooth] Wii Remote " << (m_slot + 1) << ": the game rejected L2CAP command "
<< static_cast<int>(ident) << std::endl;
break;
case kEchoResponse:
case kInformationResponse:
break;
default: {
std::vector<uint8_t> reply;
PutLe16(reply, 0x0000); // command not understood
SendSignal(kCommandReject, ident, reply);
break;
}
}
data += 4 + length;
size -= 4 + length;
}
}
void RemoteLink::HandleConnectionRequest(uint8_t ident, const uint8_t* p, size_t n) {
if (n < 4) return;
const uint16_t psm = Le16(p);
const uint16_t scid = Le16(p + 2);
uint16_t result = kResultSuccess;
uint16_t dcid = 0;
if (psm != kPsmSdp && psm != kPsmHidControl && psm != kPsmHidInterrupt) {
result = kResultPsmNotSupported;
} else if (FindByPsm(psm) != nullptr) {
// A real remote refuses a second channel on the same PSM.
result = kResultNoResources;
} else {
Channel channel;
channel.psm = psm;
channel.localCid = AllocateCid();
channel.remoteCid = scid;
channel.connected = true;
dcid = channel.localCid;
m_channels.push_back(channel);
}
std::vector<uint8_t> reply;
PutLe16(reply, dcid);
PutLe16(reply, scid);
PutLe16(reply, result);
PutLe16(reply, 0);
SendSignal(kConnectionResponse, ident, reply);
if (result == kResultSuccess) {
SendConfigRequest(*FindByLocal(dcid));
}
}
void RemoteLink::HandleConnectionResponse(uint8_t ident, const uint8_t* p, size_t n) {
if (n < 8) return;
const uint16_t dcid = Le16(p);
const uint16_t scid = Le16(p + 2);
const uint16_t result = Le16(p + 4);
Channel* channel = FindByLocal(scid);
if (channel == nullptr || !channel->weInitiated || channel->connected || channel->connectIdent != ident) {
return;
}
if (result == kResultSuccess) {
channel->remoteCid = dcid;
channel->connected = true;
SendConfigRequest(*channel);
} else if (result == kResultPending) {
channel->requestUs = m_nowUs;
} else {
std::cerr << "[bluetooth] Wii Remote " << (m_slot + 1) << ": the game refused L2CAP PSM 0x" << std::hex
<< channel->psm << " (result 0x" << result << ")" << std::dec << std::endl;
DropFromRemote(kReasonRemoteUserTerminated);
m_retryAtUs = m_nowUs + kRetryDelayUs;
}
}
void RemoteLink::HandleConfigurationRequest(uint8_t ident, const uint8_t* p, size_t n) {
if (n < 4) return;
const uint16_t dcid = Le16(p);
const uint16_t flags = Le16(p + 2);
Channel* channel = FindByLocal(dcid);
if (channel == nullptr) {
std::vector<uint8_t> reply;
PutLe16(reply, 0x0002); // invalid CID
PutLe16(reply, dcid);
PutLe16(reply, 0);
SendSignal(kCommandReject, ident, reply);
return;
}
channel->pendingOptions.insert(channel->pendingOptions.end(), p + 4, p + n);
std::vector<uint8_t> reply;
PutLe16(reply, channel->remoteCid);
if (flags & 1) {
PutLe16(reply, 1);
PutLe16(reply, kResultSuccess);
SendSignal(kConfigurationResponse, ident, reply);
return;
}
PutLe16(reply, 0);
PutLe16(reply, kResultSuccess);
const std::vector<uint8_t>& options = channel->pendingOptions;
for (size_t i = 0; i + 2 <= options.size();) {
const uint8_t type = options[i] & 0x7F;
const uint8_t length = options[i + 1];
if (i + 2 + length > options.size()) break;
if (type == kOptionMtu && length == 2) {
channel->remoteMtu = Le16(&options[i + 2]);
reply.insert(reply.end(), options.begin() + static_cast<std::ptrdiff_t>(i),
options.begin() + static_cast<std::ptrdiff_t>(i + 4));
} else if (type == kOptionFlushTimeout && length == 2) {
reply.insert(reply.end(), options.begin() + static_cast<std::ptrdiff_t>(i),
options.begin() + static_cast<std::ptrdiff_t>(i + 4));
}
i += 2 + length;
}
channel->pendingOptions.clear();
channel->remoteConfigured = true;
SendSignal(kConfigurationResponse, ident, reply);
UpdateState();
}
void RemoteLink::HandleConfigurationResponse(uint8_t ident, const uint8_t* p, size_t n) {
if (n < 6) return;
const uint16_t scid = Le16(p);
const uint16_t flags = Le16(p + 2);
const uint16_t result = Le16(p + 4);
Channel* channel = FindByLocal(scid);
if (channel == nullptr || !channel->configSent || channel->configIdent != ident) return;
if (flags & 1) {
return;
}
if (result == kResultSuccess) {
channel->localConfigured = true;
UpdateState();
return;
}
// Our only option is the MTU; offer the default instead.
channel->configIdent = NextIdent();
channel->requestUs = m_nowUs;
std::vector<uint8_t> data;
PutLe16(data, channel->remoteCid);
PutLe16(data, 0);
SendSignal(kConfigurationRequest, channel->configIdent, data);
}
void RemoteLink::HandleDisconnectionRequest(uint8_t ident, const uint8_t* p, size_t n) {
if (n < 4) return;
const uint16_t dcid = Le16(p);
const uint16_t scid = Le16(p + 2);
std::vector<uint8_t> reply;
PutLe16(reply, dcid);
PutLe16(reply, scid);
SendSignal(kDisconnectionResponse, ident, reply);
m_channels.erase(std::remove_if(m_channels.begin(), m_channels.end(),
[&](const Channel& c) { return c.localCid == dcid; }),
m_channels.end());
UpdateState();
}
void RemoteLink::HandleDisconnectionResponse(const uint8_t* p, size_t n) {
if (n < 4) return;
const uint16_t scid = Le16(p + 2);
m_channels.erase(std::remove_if(m_channels.begin(), m_channels.end(),
[&](const Channel& c) { return c.localCid == scid; }),
m_channels.end());
UpdateState();
}
void RemoteLink::HandleChannelData(Channel& channel, const uint8_t* data, size_t size) {
if (size == 0) return;
if (channel.psm == kPsmSdp) {
const std::vector<uint8_t> response = Sdp::HandleRequest(data, size, channel.remoteMtu);
SendData(channel, response);
return;
}
if (channel.psm == kPsmHidControl) {
HandleHidControl(channel, data, size);
return;
}
const uint8_t type = data[0] >> 4;
const uint8_t param = data[0] & 0x0F;
if (type == kHidData && param == kHidParamOutput && size >= 2 && size - 1 <= kMaxReportSize) {
m_controller.Host().SendOutputReport(m_slot, data + 1, size - 1);
}
}
void RemoteLink::HandleHidControl(Channel& channel, const uint8_t* data, size_t size) {
const uint8_t type = data[0] >> 4;
const uint8_t param = data[0] & 0x0F;
switch (type) {
case kHidHandshake:
return;
case kHidControl:
if (param == kHidVirtualCableUnplug) {
const bool wasReady = m_everReady;
DropFromRemote(kReasonRemoteUserTerminated);
if (wasReady) {
m_connectWanted = false;
m_controller.Host().OnGuestDisconnect(m_slot);
}
}
return;
case kHidSetReport:
SendData(channel, {kHidHandshakeSuccess});
if ((param & 0x3) == kHidParamOutput && size >= 2 && size - 1 <= kMaxReportSize) {
m_controller.Host().SendOutputReport(m_slot, data + 1, size - 1);
}
return;
case kHidData:
if (param == kHidParamOutput && size >= 2 && size - 1 <= kMaxReportSize) {
m_controller.Host().SendOutputReport(m_slot, data + 1, size - 1);
}
return;
default:
SendData(channel, {kHidHandshakeUnsupported});
return;
}
}
} // namespace Bluetooth
+438
View File
@@ -0,0 +1,438 @@
#include "bluetooth/bt_sdp.h"
#include <algorithm>
#include <array>
#include <initializer_list>
#include <optional>
#include <string_view>
#include <utility>
namespace Bluetooth::Sdp {
namespace {
using Bytes = std::vector<uint8_t>;
constexpr uint8_t kErrorResponse = 0x01;
constexpr uint8_t kServiceSearchRequest = 0x02;
constexpr uint8_t kServiceSearchResponse = 0x03;
constexpr uint8_t kServiceAttributeRequest = 0x04;
constexpr uint8_t kServiceAttributeResponse = 0x05;
constexpr uint8_t kServiceSearchAttributeRequest = 0x06;
constexpr uint8_t kServiceSearchAttributeResponse = 0x07;
constexpr uint16_t kErrInvalidHandle = 0x0002;
constexpr uint16_t kErrInvalidSyntax = 0x0003;
constexpr uint16_t kErrInvalidPduSize = 0x0004;
constexpr uint16_t kErrInvalidContinuation = 0x0005;
void PutBe16(Bytes& out, uint16_t v) {
out.push_back(static_cast<uint8_t>(v >> 8));
out.push_back(static_cast<uint8_t>(v));
}
void PutBe32(Bytes& out, uint32_t v) {
PutBe16(out, static_cast<uint16_t>(v >> 16));
PutBe16(out, static_cast<uint16_t>(v));
}
uint16_t Be16(const uint8_t* p) {
return static_cast<uint16_t>((p[0] << 8) | p[1]);
}
uint32_t Be32(const uint8_t* p) {
return (static_cast<uint32_t>(Be16(p)) << 16) | Be16(p + 2);
}
// Data element constructors (Bluetooth Core, Vol 3 Part B, 3.2).
Bytes U8(uint8_t v) { return {0x08, v}; }
Bytes U16(uint16_t v) { Bytes b{0x09}; PutBe16(b, v); return b; }
Bytes U32(uint32_t v) { Bytes b{0x0A}; PutBe32(b, v); return b; }
Bytes Uuid16(uint16_t v) { Bytes b{0x19}; PutBe16(b, v); return b; }
Bytes Bool(bool v) { return {0x28, static_cast<uint8_t>(v ? 1 : 0)}; }
Bytes WithLength(uint8_t type, size_t length, bool wide = false) {
if (length <= 0xFF && !wide) {
return {static_cast<uint8_t>((type << 3) | 5), static_cast<uint8_t>(length)};
}
Bytes b{static_cast<uint8_t>((type << 3) | 6)};
PutBe16(b, static_cast<uint16_t>(length));
return b;
}
Bytes Text(const Bytes& raw) {
Bytes b = WithLength(4, raw.size());
b.insert(b.end(), raw.begin(), raw.end());
return b;
}
Bytes Text(std::string_view s) {
return Text(Bytes(s.begin(), s.end()));
}
Bytes Seq(std::initializer_list<Bytes> items) {
size_t length = 0;
for (const Bytes& item : items) length += item.size();
Bytes b = WithLength(6, length);
for (const Bytes& item : items) b.insert(b.end(), item.begin(), item.end());
return b;
}
using Record = std::vector<std::pair<uint16_t, Bytes>>;
const Bytes kHidDescriptor = {
0x05, 0x01, 0x09, 0x05, 0xA1, 0x01, 0x85, 0x10, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95,
0x01, 0x06, 0x00, 0xFF, 0x09, 0x01, 0x91, 0x00, 0x85, 0x11, 0x95, 0x01, 0x09, 0x01, 0x91, 0x00,
0x85, 0x12, 0x95, 0x02, 0x09, 0x01, 0x91, 0x00, 0x85, 0x13, 0x95, 0x01, 0x09, 0x01, 0x91, 0x00,
0x85, 0x14, 0x95, 0x01, 0x09, 0x01, 0x91, 0x00, 0x85, 0x15, 0x95, 0x01, 0x09, 0x01, 0x91, 0x00,
0x85, 0x16, 0x95, 0x15, 0x09, 0x01, 0x91, 0x00, 0x85, 0x17, 0x95, 0x06, 0x09, 0x01, 0x91, 0x00,
0x85, 0x18, 0x95, 0x15, 0x09, 0x01, 0x91, 0x00, 0x85, 0x19, 0x95, 0x01, 0x09, 0x01, 0x91, 0x00,
0x85, 0x1A, 0x95, 0x01, 0x09, 0x01, 0x91, 0x00, 0x85, 0x20, 0x95, 0x06, 0x09, 0x01, 0x81, 0x00,
0x85, 0x21, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00, 0x85, 0x22, 0x95, 0x04, 0x09, 0x01, 0x81, 0x00,
0x85, 0x30, 0x95, 0x02, 0x09, 0x01, 0x81, 0x00, 0x85, 0x31, 0x95, 0x05, 0x09, 0x01, 0x81, 0x00,
0x85, 0x32, 0x95, 0x0A, 0x09, 0x01, 0x81, 0x00, 0x85, 0x33, 0x95, 0x11, 0x09, 0x01, 0x81, 0x00,
0x85, 0x34, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00, 0x85, 0x35, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00,
0x85, 0x36, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00, 0x85, 0x37, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00,
0x85, 0x3D, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00, 0x85, 0x3E, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00,
0x85, 0x3F, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00, 0xC0,
};
constexpr uint32_t kHidRecordHandle = 0x00010000;
constexpr uint32_t kDeviceIdRecordHandle = 0x00010001;
const Record& HidRecord() {
static const Record record = {
{0x0000, U32(kHidRecordHandle)},
{0x0001, Seq({Uuid16(0x1124)})},
{0x0004, Seq({Seq({Uuid16(0x0100), U16(0x0011)}), Seq({Uuid16(0x0011)})})},
{0x0005, Seq({Uuid16(0x1002)})},
{0x0006, Seq({U16(0x656E), U16(0x006A), U16(0x0100)})},
{0x0009, Seq({Seq({Uuid16(0x1124), U16(0x0100)})})},
{0x000D, Seq({Seq({Seq({Uuid16(0x0100), U16(0x0013)}), Seq({Uuid16(0x0011)})})})},
{0x0100, Text("Nintendo RVL-CNT-01")},
{0x0101, Text("Nintendo RVL-CNT-01")},
{0x0102, Text("Nintendo")},
{0x0200, U16(0x0100)},
{0x0201, U16(0x0111)},
{0x0202, U8(0x04)},
{0x0203, U8(0x33)},
{0x0204, Bool(false)},
{0x0205, Bool(true)},
{0x0206, Seq({Seq({U8(0x22), Text(kHidDescriptor)})})},
{0x0207, Seq({Seq({U16(0x0409), U16(0x0100)})})},
{0x0208, Bool(false)},
{0x0209, Bool(true)},
{0x020A, Bool(true)},
{0x020B, U16(0x0100)},
{0x020C, U16(0x0C80)},
{0x020D, Bool(false)},
{0x020E, Bool(false)},
};
return record;
}
const Record& DeviceIdRecord() {
static const Record record = {
{0x0000, U32(kDeviceIdRecordHandle)},
{0x0001, Seq({Uuid16(0x1200)})},
{0x0004, Seq({Seq({Uuid16(0x0100), U16(0x0001)}), Seq({Uuid16(0x0001)})})},
{0x0005, Seq({Uuid16(0x1002)})},
{0x0009, Seq({Seq({Uuid16(0x1200), U16(0x0100)})})},
{0x0200, U16(0x0100)},
{0x0201, U16(0x057E)},
{0x0202, U16(0x0306)},
{0x0203, U16(0x0600)},
{0x0204, Bool(true)},
{0x0205, U16(0x0002)},
};
return record;
}
struct NamedRecord {
uint32_t handle;
const Record& (*get)();
};
constexpr std::array<NamedRecord, 2> kRecords = {{
{kHidRecordHandle, HidRecord},
{kDeviceIdRecordHandle, DeviceIdRecord},
}};
using Uuid = std::array<uint8_t, 16>;
Uuid FromShort(uint32_t value) {
// Bluetooth base UUID 00000000-0000-1000-8000-00805F9B34FB.
Uuid u = {0, 0, 0, 0, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB};
u[0] = static_cast<uint8_t>(value >> 24);
u[1] = static_cast<uint8_t>(value >> 16);
u[2] = static_cast<uint8_t>(value >> 8);
u[3] = static_cast<uint8_t>(value);
return u;
}
// One parsed data element header: type, header size and payload size.
struct ElementHeader {
uint8_t type = 0;
size_t headerSize = 0;
size_t dataSize = 0;
};
std::optional<ElementHeader> ParseHeader(const uint8_t* p, size_t n) {
if (n == 0) return std::nullopt;
ElementHeader h;
h.type = static_cast<uint8_t>(p[0] >> 3);
const uint8_t sizeIndex = p[0] & 7;
if (h.type == 0) {
h.headerSize = 1;
h.dataSize = 0;
} else if (sizeIndex <= 4) {
h.headerSize = 1;
h.dataSize = size_t{1} << sizeIndex;
} else if (sizeIndex == 5) {
if (n < 2) return std::nullopt;
h.headerSize = 2;
h.dataSize = p[1];
} else if (sizeIndex == 6) {
if (n < 3) return std::nullopt;
h.headerSize = 3;
h.dataSize = Be16(p + 1);
} else {
if (n < 5) return std::nullopt;
h.headerSize = 5;
h.dataSize = Be32(p + 1);
}
if (h.headerSize + h.dataSize > n) return std::nullopt;
return h;
}
std::optional<Uuid> ReadUuid(const uint8_t* p, const ElementHeader& h) {
if (h.type != 3) return std::nullopt;
const uint8_t* d = p + h.headerSize;
if (h.dataSize == 2) return FromShort(Be16(d));
if (h.dataSize == 4) return FromShort(Be32(d));
if (h.dataSize == 16) {
Uuid u{};
std::copy(d, d + 16, u.begin());
return u;
}
return std::nullopt;
}
void CollectUuids(const uint8_t* p, size_t n, std::vector<Uuid>& out) {
while (n > 0) {
const auto h = ParseHeader(p, n);
if (!h) return;
if (h->type == 6 || h->type == 7) {
CollectUuids(p + h->headerSize, h->dataSize, out);
} else if (const auto uuid = ReadUuid(p, *h)) {
out.push_back(*uuid);
}
const size_t total = h->headerSize + h->dataSize;
p += total;
n -= total;
}
}
bool RecordMatches(const Record& record, const std::vector<Uuid>& pattern) {
std::vector<Uuid> present;
for (const auto& [id, value] : record) CollectUuids(value.data(), value.size(), present);
return std::all_of(pattern.begin(), pattern.end(), [&](const Uuid& wanted) {
return std::find(present.begin(), present.end(), wanted) != present.end();
});
}
// Parses a data element sequence at `p`; advances `p`/`n` past it.
std::optional<std::pair<const uint8_t*, size_t>> TakeSequence(const uint8_t*& p, size_t& n) {
const auto h = ParseHeader(p, n);
if (!h || (h->type != 6)) return std::nullopt;
const std::pair<const uint8_t*, size_t> body{p + h->headerSize, h->dataSize};
p += h->headerSize + h->dataSize;
n -= h->headerSize + h->dataSize;
return body;
}
std::optional<std::vector<Uuid>> ParsePattern(const uint8_t*& p, size_t& n) {
const auto body = TakeSequence(p, n);
if (!body) return std::nullopt;
std::vector<Uuid> pattern;
const uint8_t* q = body->first;
size_t m = body->second;
while (m > 0) {
const auto h = ParseHeader(q, m);
if (!h) return std::nullopt;
const auto uuid = ReadUuid(q, *h);
if (!uuid) return std::nullopt;
pattern.push_back(*uuid);
q += h->headerSize + h->dataSize;
m -= h->headerSize + h->dataSize;
}
if (pattern.empty() || pattern.size() > 12) return std::nullopt;
return pattern;
}
using IdRange = std::pair<uint16_t, uint16_t>;
std::optional<std::vector<IdRange>> ParseAttributeIds(const uint8_t*& p, size_t& n) {
const auto body = TakeSequence(p, n);
if (!body) return std::nullopt;
std::vector<IdRange> ranges;
const uint8_t* q = body->first;
size_t m = body->second;
while (m > 0) {
const auto h = ParseHeader(q, m);
if (!h || h->type != 1) return std::nullopt;
const uint8_t* d = q + h->headerSize;
if (h->dataSize == 2) {
ranges.emplace_back(Be16(d), Be16(d));
} else if (h->dataSize == 4) {
ranges.emplace_back(Be16(d), Be16(d + 2));
} else {
return std::nullopt;
}
q += h->headerSize + h->dataSize;
m -= h->headerSize + h->dataSize;
}
if (ranges.empty()) return std::nullopt;
return ranges;
}
// Continuation state is the byte offset into the complete response (2 bytes BE).
std::optional<size_t> ParseContinuation(const uint8_t* p, size_t n) {
if (n < 1 || p[0] + size_t{1} != n) return std::nullopt;
if (p[0] == 0) return 0;
if (p[0] != 2) return std::nullopt;
return Be16(p + 1);
}
Bytes AttributeList(const Record& record, const std::vector<IdRange>& ranges) {
Bytes inner;
for (const auto& [id, value] : record) {
const bool wanted = std::any_of(ranges.begin(), ranges.end(),
[id = id](const IdRange& r) { return id >= r.first && id <= r.second; });
if (!wanted) continue;
const Bytes idElement = U16(id);
inner.insert(inner.end(), idElement.begin(), idElement.end());
inner.insert(inner.end(), value.begin(), value.end());
}
// A real remote always sends attribute lists with a 16-bit length.
Bytes list = WithLength(6, inner.size(), true);
list.insert(list.end(), inner.begin(), inner.end());
return list;
}
Bytes Pdu(uint8_t id, uint16_t tid, const Bytes& params) {
Bytes out{id};
PutBe16(out, tid);
PutBe16(out, static_cast<uint16_t>(params.size()));
out.insert(out.end(), params.begin(), params.end());
return out;
}
Bytes Error(uint16_t tid, uint16_t code) {
Bytes params;
PutBe16(params, code);
return Pdu(kErrorResponse, tid, params);
}
// Emits the slice of `full` starting at `offset` that fits, plus continuation.
Bytes Chunked(uint8_t responseId, uint16_t tid, const Bytes& full, size_t offset, uint16_t maxBytes,
size_t maxResponse) {
// PDU header 5, byte count 2, continuation up to 3.
const size_t room = maxResponse > 10 ? maxResponse - 10 : 1;
const size_t limit = std::max<size_t>(1, std::min<size_t>(maxBytes, room));
const size_t count = std::min(limit, full.size() - offset);
Bytes params;
PutBe16(params, static_cast<uint16_t>(count));
params.insert(params.end(), full.begin() + static_cast<std::ptrdiff_t>(offset),
full.begin() + static_cast<std::ptrdiff_t>(offset + count));
const size_t next = offset + count;
if (next < full.size()) {
params.push_back(2);
PutBe16(params, static_cast<uint16_t>(next));
} else {
params.push_back(0);
}
return Pdu(responseId, tid, params);
}
} // namespace
const std::vector<uint8_t>& WiiRemoteHidDescriptor() {
return kHidDescriptor;
}
std::vector<uint8_t> HandleRequest(const uint8_t* pdu, size_t size, size_t maxResponse) {
if (size < 5) {
return Error(0, kErrInvalidPduSize);
}
const uint8_t id = pdu[0];
const uint16_t tid = Be16(pdu + 1);
const uint16_t paramLength = Be16(pdu + 3);
if (paramLength != size - 5) {
return Error(tid, kErrInvalidPduSize);
}
const uint8_t* p = pdu + 5;
size_t n = paramLength;
switch (id) {
case kServiceSearchRequest: {
const auto pattern = ParsePattern(p, n);
if (!pattern || n < 2) return Error(tid, kErrInvalidSyntax);
const uint16_t maxCount = Be16(p);
if (!ParseContinuation(p + 2, n - 2)) return Error(tid, kErrInvalidContinuation);
Bytes handles;
uint16_t found = 0;
for (const NamedRecord& record : kRecords) {
if (found < maxCount && RecordMatches(record.get(), *pattern)) {
PutBe32(handles, record.handle);
++found;
}
}
Bytes params;
PutBe16(params, found);
PutBe16(params, found);
params.insert(params.end(), handles.begin(), handles.end());
params.push_back(0);
return Pdu(kServiceSearchResponse, tid, params);
}
case kServiceAttributeRequest: {
if (n < 6) return Error(tid, kErrInvalidSyntax);
const uint32_t handle = Be32(p);
const uint16_t maxBytes = Be16(p + 4);
p += 6;
n -= 6;
const auto ranges = ParseAttributeIds(p, n);
if (!ranges || maxBytes < 7) return Error(tid, kErrInvalidSyntax);
const auto record = std::find_if(kRecords.begin(), kRecords.end(),
[&](const NamedRecord& r) { return r.handle == handle; });
if (record == kRecords.end()) return Error(tid, kErrInvalidHandle);
const Bytes full = AttributeList(record->get(), *ranges);
const auto offset = ParseContinuation(p, n);
if (!offset || *offset > full.size()) return Error(tid, kErrInvalidContinuation);
return Chunked(kServiceAttributeResponse, tid, full, *offset, maxBytes, maxResponse);
}
case kServiceSearchAttributeRequest: {
const auto pattern = ParsePattern(p, n);
if (!pattern || n < 2) return Error(tid, kErrInvalidSyntax);
const uint16_t maxBytes = Be16(p);
p += 2;
n -= 2;
const auto ranges = ParseAttributeIds(p, n);
if (!ranges || maxBytes < 7) return Error(tid, kErrInvalidSyntax);
Bytes lists;
for (const NamedRecord& record : kRecords) {
if (RecordMatches(record.get(), *pattern)) {
const Bytes list = AttributeList(record.get(), *ranges);
lists.insert(lists.end(), list.begin(), list.end());
}
}
Bytes full = WithLength(6, lists.size());
full.insert(full.end(), lists.begin(), lists.end());
const auto offset = ParseContinuation(p, n);
if (!offset || *offset > full.size()) return Error(tid, kErrInvalidContinuation);
return Chunked(kServiceSearchAttributeResponse, tid, full, *offset, maxBytes, maxResponse);
}
default:
return Error(tid, kErrInvalidSyntax);
}
}
} // namespace Bluetooth::Sdp
+126
View File
@@ -0,0 +1,126 @@
// SYSCONF items behind the Wii Remote stack. The runtime never parses a
// SYSCONF (SCCheckStatus is HLE'd), so these accessors serve the Bluetooth items
// directly: BT.DINF registers the four virtual remotes of the emulated
// controller, and the console settings come from Config.toml.
#include "bluetooth/bt_controller.h"
#include "hle_stubs.h"
#include "memory.h"
#include "runtime_config.h"
#include "runtime_log.h"
#include <algorithm>
#include <array>
#include <cstring>
namespace BluetoothSysconf {
constexpr uint32_t kDeviceInfoSize = 0x461;
constexpr uint32_t kCmpDeviceInfoSize = 0x205;
constexpr uint32_t kDeviceEntrySize = 0x46;
// [0..9] registered remotes, [10..13] the remote last connected on each channel.
constexpr uint32_t kActiveEntriesStart = 10;
constexpr const char kRemoteName[] = "Nintendo RVL-CNT-01";
void Fill(uint32_t buffer, uint32_t size, uint8_t value) {
std::memset(Memory::GetPointer(buffer, size), value, size);
}
void WriteEntry(uint32_t buffer, uint32_t index, size_t slot) {
const uint32_t entry = buffer + 1 + index * kDeviceEntrySize;
// SYSCONF keeps addresses most significant byte first.
const Bluetooth::BdAddr addr = Bluetooth::Controller::RemoteAddress(slot);
for (size_t i = 0; i < addr.size(); ++i) {
Memory::Write8(entry + static_cast<uint32_t>(i), addr[addr.size() - 1 - i]);
}
std::memcpy(Memory::GetPointer(entry + 6, sizeof(kRemoteName)), kRemoteName, sizeof(kRemoteName));
}
void SetReturn(CpuContext* ctx, uint32_t value) {
ctx->gpr[3] = value;
}
} // namespace BluetoothSysconf
extern "C" void SCGetBtDeviceInfoArray_HLE(CpuContext* ctx) {
const uint32_t buffer = ctx->gpr[3];
try {
BluetoothSysconf::Fill(buffer, BluetoothSysconf::kDeviceInfoSize, 0);
Memory::Write8(buffer, static_cast<uint8_t>(Bluetooth::kRemoteSlots));
for (size_t slot = 0; slot < Bluetooth::kRemoteSlots; ++slot) {
BluetoothSysconf::WriteEntry(buffer, static_cast<uint32_t>(slot), slot);
BluetoothSysconf::WriteEntry(buffer, BluetoothSysconf::kActiveEntriesStart + static_cast<uint32_t>(slot), slot);
}
BluetoothSysconf::SetReturn(ctx, 1);
} catch (const Memory::AccessViolation& e) {
LogMemoryError(RT_TAG_HLE, "SCGetBtDeviceInfoArray", e);
BluetoothSysconf::SetReturn(ctx, 0);
}
}
PPC_NATIVE_OVERRIDE_VOID(801B1ED0, SCGetBtDeviceInfoArray_HLE, (CpuContext* ctx), (ctx));
// WUD writes the list back after reconciling it with the controller's link
// keys, and WPAD whenever a channel's remote changes. The virtual remotes are
// fixed, so the list is rebuilt on every boot.
extern "C" void SCSetBtDeviceInfoArray_HLE(CpuContext* ctx) {
BluetoothSysconf::SetReturn(ctx, 1);
}
PPC_NATIVE_OVERRIDE_VOID(801B1EDC, SCSetBtDeviceInfoArray_HLE, (CpuContext* ctx), (ctx));
// Remotes paired with 1+2 on the game's own sync screens. The virtual remotes
// are all registered, so the list is always empty.
extern "C" void SCGetBtCmpDevInfoArray_HLE(CpuContext* ctx) {
try {
BluetoothSysconf::Fill(ctx->gpr[3], BluetoothSysconf::kCmpDeviceInfoSize, 0);
BluetoothSysconf::SetReturn(ctx, 1);
} catch (const Memory::AccessViolation& e) {
LogMemoryError(RT_TAG_HLE, "SCGetBtCmpDevInfoArray", e);
BluetoothSysconf::SetReturn(ctx, 0);
}
}
PPC_NATIVE_OVERRIDE_VOID(801B1EE8, SCGetBtCmpDevInfoArray_HLE, (CpuContext* ctx), (ctx));
extern "C" void SCSetBtCmpDevInfoArray_HLE(CpuContext* ctx) {
BluetoothSysconf::SetReturn(ctx, 1);
}
PPC_NATIVE_OVERRIDE_VOID(801B1EF4, SCSetBtCmpDevInfoArray_HLE, (CpuContext* ctx), (ctx));
extern "C" void SCGetBtDpdSensibility_HLE(CpuContext* ctx) {
BluetoothSysconf::SetReturn(ctx, static_cast<uint32_t>(std::clamp(RuntimeConfigFile::WiiIrSensitivity(), 1, 5)));
}
PPC_NATIVE_OVERRIDE_VOID(801B1F00, SCGetBtDpdSensibility_HLE, (CpuContext* ctx), (ctx));
extern "C" void SCGetWpadMotorMode_HLE(CpuContext* ctx) {
BluetoothSysconf::SetReturn(ctx, RuntimeConfigFile::WiiRumbleEnabled() ? 1u : 0u);
}
PPC_NATIVE_OVERRIDE_VOID(801B1F68, SCGetWpadMotorMode_HLE, (CpuContext* ctx), (ctx));
// The Home Menu's vibration switch, saved by WPADSaveConfig.
extern "C" void SCSetWpadMotorMode_HLE(CpuContext* ctx) {
const bool enabled = (ctx->gpr[3] & 0xFF) == 1;
if (enabled != RuntimeConfigFile::WiiRumbleEnabled()) {
RuntimeConfigFile::SetWiiRumbleEnabled(enabled);
}
BluetoothSysconf::SetReturn(ctx, 1);
}
PPC_NATIVE_OVERRIDE_VOID(801B1FBC, SCSetWpadMotorMode_HLE, (CpuContext* ctx), (ctx));
extern "C" void SCGetWpadSensorBarPosition_HLE(CpuContext* ctx) {
BluetoothSysconf::SetReturn(ctx, RuntimeConfigFile::WiiSensorBarAbove() ? 1u : 0u);
}
PPC_NATIVE_OVERRIDE_VOID(801B1FC4, SCGetWpadSensorBarPosition_HLE, (CpuContext* ctx), (ctx));
extern "C" void SCGetWpadSpeakerVolume_HLE(CpuContext* ctx) {
BluetoothSysconf::SetReturn(ctx, static_cast<uint32_t>(std::clamp(RuntimeConfigFile::WiiSpeakerVolume(), 0, 0x7F)));
}
PPC_NATIVE_OVERRIDE_VOID(801B2018, SCGetWpadSpeakerVolume_HLE, (CpuContext* ctx), (ctx));
// The Home Menu's speaker volume, saved by WPADSaveConfig.
extern "C" void SCSetWpadSpeakerVolume_HLE(CpuContext* ctx) {
const int32_t volume = std::min<int32_t>(static_cast<int32_t>(ctx->gpr[3] & 0xFF), 0x7F);
if (volume != RuntimeConfigFile::WiiSpeakerVolume()) {
RuntimeConfigFile::SetWiiSpeakerVolume(volume);
}
BluetoothSysconf::SetReturn(ctx, 1);
}
PPC_NATIVE_OVERRIDE_VOID(801B206C, SCSetWpadSpeakerVolume_HLE, (CpuContext* ctx), (ctx));
+270
View File
@@ -0,0 +1,270 @@
#include "hle/bluetooth_hle.h"
#include "bluetooth/bt_controller.h"
#include "hle/runtime_parse_helpers.h"
#include "memory.h"
#include "runtime_log.h"
#include "wiimote_host.h"
#include <algorithm>
#include <chrono>
#include <cstring>
#include <memory>
#include <vector>
void NandQueueIosCallback(uint32_t callbackPtr, int32_t result, uint32_t callbackArg);
namespace BluetoothUsb {
constexpr const char* kDevicePath = "/dev/usb/oh1/57e/305";
constexpr int32_t kDeviceFd = 5;
constexpr int32_t kIpcSuccess = 0;
constexpr int32_t kIpcEinval = -4;
constexpr uint32_t kCtrlMsg = 0;
constexpr uint32_t kBulkMsg = 1;
constexpr uint32_t kIntrMsg = 2;
constexpr uint8_t kEndpointEvents = 0x81;
constexpr uint8_t kEndpointAclIn = 0x82;
constexpr uint8_t kEndpointAclOut = 0x02;
// The guest copies each delivered packet into a GKI buffer that its 2 ms
// Bluetooth tick frees; a pool it cannot refill drops the packet, so ACL data is
// paced. Plenty for four remotes reporting at 200 Hz.
constexpr double kAclPacketsPerMs = 3.0;
constexpr double kAclBurst = 6.0;
struct HeldRead {
bool active = false;
uint32_t buffer = 0;
uint32_t length = 0;
uint32_t callback = 0;
uint32_t callbackArg = 0;
};
struct Device {
bool open = false;
std::unique_ptr<Bluetooth::Controller> controller;
HeldRead eventRead;
HeldRead aclRead;
double aclTokens = kAclBurst;
uint64_t lastRefillUs = 0;
};
Device& Dev() {
static Device device;
return device;
}
uint64_t NowUs() {
using namespace std::chrono;
return static_cast<uint64_t>(duration_cast<microseconds>(steady_clock::now().time_since_epoch()).count());
}
Bluetooth::Controller& ControllerFor(Device& device) {
if (!device.controller) {
device.controller = std::make_unique<Bluetooth::Controller>(Wiimote::Host());
}
return *device.controller;
}
std::vector<uint8_t> ReadGuest(uint32_t address, uint32_t length) {
std::vector<uint8_t> data(length);
if (length != 0) {
std::memcpy(data.data(), Memory::GetPointer(address, length), length);
}
return data;
}
void Complete(HeldRead& read, const std::vector<uint8_t>& data) {
const uint32_t size = static_cast<uint32_t>(std::min<size_t>(data.size(), read.length));
if (size < data.size()) {
RT_LOG(RT_TAG_HLE) << "Bluetooth: packet of " << data.size() << " bytes truncated to " << size << std::endl;
}
if (size != 0) {
std::memcpy(Memory::GetPointer(read.buffer, size), data.data(), size);
}
read.active = false;
NandQueueIosCallback(read.callback, static_cast<int32_t>(size), read.callbackArg);
}
// Hands queued events and ACL data to whichever reads the guest has armed.
bool Deliver(Device& device, uint64_t nowUs) {
Bluetooth::Controller& controller = ControllerFor(device);
bool delivered = false;
std::vector<uint8_t> packet;
if (device.eventRead.active && controller.PopEvent(packet)) {
Complete(device.eventRead, packet);
delivered = true;
}
if (nowUs > device.lastRefillUs) {
device.aclTokens = std::min(kAclBurst, device.aclTokens + (nowUs - device.lastRefillUs) / 1000.0 * kAclPacketsPerMs);
device.lastRefillUs = nowUs;
}
if (device.aclRead.active && device.aclTokens >= 1.0 && controller.PopAcl(packet)) {
device.aclTokens -= 1.0;
Complete(device.aclRead, packet);
delivered = true;
}
return delivered;
}
bool Hold(HeldRead& read, uint32_t buffer, uint32_t length, uint32_t callback, uint32_t callbackArg) {
if (length != 0) {
Memory::GetPointer(buffer, length);
}
if (read.active) {
RT_LOG(RT_TAG_HLE) << "Bluetooth: a second read on the same endpoint replaced the first" << std::endl;
NandQueueIosCallback(read.callback, kIpcEinval, read.callbackArg);
}
read = {true, buffer, length, callback, callbackArg};
return true;
}
// Executes a transfer. Returns true with `result` when it completed now.
bool Transfer(uint32_t cmd, uint32_t numIn, uint32_t numOut, uint32_t vectorPtr, uint32_t callback,
uint32_t callbackArg, bool async, int32_t& result) {
Device& device = Dev();
Bluetooth::Controller& controller = ControllerFor(device);
const uint64_t nowUs = NowUs();
try {
if (cmd == kCtrlMsg && numIn == 6 && numOut == 1) {
const auto requestType = RuntimeHle::ReadIoVector(vectorPtr, 0);
const auto lengthVector = RuntimeHle::ReadIoVector(vectorPtr, 4);
const auto data = RuntimeHle::ReadIoVector(vectorPtr, 6);
const uint8_t bmRequestType = Memory::Read8(requestType.address);
// wLength is little-endian in the request block.
const uint16_t wLength = static_cast<uint16_t>(Memory::Read8(lengthVector.address) |
(Memory::Read8(lengthVector.address + 1) << 8));
if (bmRequestType != 0x20 || wLength > data.size) {
RT_LOG(RT_TAG_HLE) << "Bluetooth: unexpected control transfer type 0x" << std::hex
<< static_cast<int>(bmRequestType) << std::dec << std::endl;
result = kIpcEinval;
return true;
}
const std::vector<uint8_t> command = ReadGuest(data.address, wLength);
controller.ExecuteCommand(command.data(), command.size(), nowUs);
result = wLength;
return true;
}
if ((cmd == kBulkMsg || cmd == kIntrMsg) && numIn == 2 && numOut == 1) {
const auto endpointVector = RuntimeHle::ReadIoVector(vectorPtr, 0);
const auto lengthVector = RuntimeHle::ReadIoVector(vectorPtr, 1);
const auto data = RuntimeHle::ReadIoVector(vectorPtr, 2);
const uint8_t endpoint = Memory::Read8(endpointVector.address);
// Bulk / interrupt lengths are big-endian.
const uint16_t length = Memory::Read16(lengthVector.address);
if (length > data.size) {
result = kIpcEinval;
return true;
}
if (cmd == kBulkMsg && endpoint == kEndpointAclOut) {
const std::vector<uint8_t> packet = ReadGuest(data.address, length);
controller.ReceiveAcl(packet.data(), packet.size(), nowUs);
result = length;
return true;
}
if (async && cmd == kBulkMsg && endpoint == kEndpointAclIn) {
Hold(device.aclRead, data.address, length, callback, callbackArg);
Deliver(device, nowUs);
return false;
}
if (async && cmd == kIntrMsg && endpoint == kEndpointEvents) {
Hold(device.eventRead, data.address, length, callback, callbackArg);
Deliver(device, nowUs);
return false;
}
RT_LOG(RT_TAG_HLE) << "Bluetooth: unsupported transfer cmd " << cmd << " endpoint 0x" << std::hex
<< static_cast<int>(endpoint) << std::dec << (async ? "" : " (sync)") << std::endl;
result = kIpcEinval;
return true;
}
} catch (const Memory::AccessViolation& e) {
LogMemoryError(RT_TAG_HLE, "Bluetooth transfer", e);
result = kIpcEinval;
return true;
}
RT_LOG(RT_TAG_HLE) << "Bluetooth: unsupported ioctlv " << cmd << " (" << numIn << " in, " << numOut << " out)"
<< std::endl;
result = kIpcEinval;
return true;
}
} // namespace BluetoothUsb
int32_t Bluetooth_HLE_OpenDevice(const char* path) {
if (path == nullptr || std::strcmp(path, BluetoothUsb::kDevicePath) != 0) {
return 0;
}
BluetoothUsb::Device& device = BluetoothUsb::Dev();
if (device.open) {
RT_LOG(RT_TAG_HLE) << "Bluetooth: device opened twice" << std::endl;
}
device.open = true;
device.eventRead = {};
device.aclRead = {};
device.aclTokens = BluetoothUsb::kAclBurst;
device.lastRefillUs = BluetoothUsb::NowUs();
BluetoothUsb::ControllerFor(device).Open(device.lastRefillUs);
RT_LOG(RT_TAG_HLE) << "Bluetooth: " << BluetoothUsb::kDevicePath << " opened" << std::endl;
return BluetoothUsb::kDeviceFd;
}
bool Bluetooth_HLE_IsFd(uint32_t fd) {
return static_cast<int32_t>(fd) == BluetoothUsb::kDeviceFd;
}
int32_t Bluetooth_HLE_Close(uint32_t fd) {
if (!Bluetooth_HLE_IsFd(fd)) {
return BluetoothUsb::kIpcEinval;
}
BluetoothUsb::Device& device = BluetoothUsb::Dev();
// IOS cancels the transfers still queued on a device it closes.
for (BluetoothUsb::HeldRead* read : {&device.eventRead, &device.aclRead}) {
if (read->active) {
read->active = false;
NandQueueIosCallback(read->callback, BluetoothUsb::kIpcEinval, read->callbackArg);
}
}
BluetoothUsb::ControllerFor(device).Close();
device.open = false;
RT_LOG(RT_TAG_HLE) << "Bluetooth: " << BluetoothUsb::kDevicePath << " closed" << std::endl;
return BluetoothUsb::kIpcSuccess;
}
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) {
(void)fd;
if (!BluetoothUsb::Dev().open) {
result = BluetoothUsb::kIpcEinval;
return true;
}
return BluetoothUsb::Transfer(cmd, numIn, numOut, vectorPtr, callback, callbackArg, true, result);
}
int32_t Bluetooth_HLE_Ioctlv(uint32_t fd, uint32_t cmd, uint32_t numIn, uint32_t numOut, uint32_t vectorPtr) {
(void)fd;
if (!BluetoothUsb::Dev().open) {
return BluetoothUsb::kIpcEinval;
}
int32_t result = BluetoothUsb::kIpcEinval;
BluetoothUsb::Transfer(cmd, numIn, numOut, vectorPtr, 0, 0, false, result);
return result;
}
bool Bluetooth_HLE_ProcessCompletions(CpuContext* cpu) {
(void)cpu;
BluetoothUsb::Device& device = BluetoothUsb::Dev();
if (!device.open) {
return false;
}
const uint64_t nowUs = BluetoothUsb::NowUs();
try {
BluetoothUsb::ControllerFor(device).Tick(nowUs);
return BluetoothUsb::Deliver(device, nowUs);
} catch (const Memory::AccessViolation& e) {
LogMemoryError(RT_TAG_HLE, "Bluetooth delivery", e);
return false;
}
}
+32
View File
@@ -0,0 +1,32 @@
#include "hle/hollywood_gpio.h"
#include "runtime_log.h"
#include <atomic>
namespace HollywoodGpio {
constexpr uint32_t kSensorBarPower = 0x100;
std::atomic<uint32_t> g_gpioBOut{0};
} // namespace HollywoodGpio
bool HollywoodGpio_Read32(uint32_t address, uint32_t& value) {
if (address != kHollywoodGpioBOut) {
return false;
}
value = HollywoodGpio::g_gpioBOut.load(std::memory_order_relaxed);
return true;
}
bool HollywoodGpio_Write32(uint32_t address, uint32_t value) {
if (address != kHollywoodGpioBOut) {
return false;
}
const uint32_t previous = HollywoodGpio::g_gpioBOut.exchange(value, std::memory_order_relaxed);
if ((previous ^ value) & HollywoodGpio::kSensorBarPower) {
RT_LOG(RT_TAG_HLE) << "Sensor bar power " << ((value & HollywoodGpio::kSensorBarPower) ? "on" : "off") << std::endl;
}
return true;
}
-268
View File
@@ -1,268 +0,0 @@
#include "hle_stubs.h"
#include "input_bindings.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);
}
}
// Neutral sticks and no buttons while an overlay owns input; the remote stays connected.
bool ReadSample(uint32_t chan, WiiRemoteInput::KpadSample& sample) {
if (!WiiRemoteInput::ReadKpadSample(chan, sample)) {
return false;
}
if (InputBindings::InputBlocked()) {
sample.hold = 0;
sample.clHold = 0;
sample.stick[0] = sample.stick[1] = 0.0f;
sample.clLStick[0] = sample.clLStick[1] = 0.0f;
sample.clRStick[0] = sample.clRStick[1] = 0.0f;
sample.clLStickRaw[0] = sample.clLStickRaw[1] = 0;
sample.clRStickRaw[0] = sample.clRStickRaw[1] = 0;
sample.clTriggerL = sample.clTriggerR = 0;
}
return true;
}
} // 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)
{
if (chan >= g_channels.size() || statusPtr == 0 || count == 0) {
return 0;
}
WiiRemoteInput::KpadSample sample;
const bool have = ReadSample(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)
{
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 = ReadSample(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));
-7
View File
@@ -2,7 +2,6 @@
#include "memory.h"
#include "hle/controller_status_contract.h"
#include "input_bindings.h"
#include "wii_remote_input.h"
#include <algorithm>
#include <atomic>
@@ -113,13 +112,7 @@ 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);
FillTriggersHeldByButtons(statuses);
InputBindings::Apply(statuses);
-217
View File
@@ -1,217 +0,0 @@
#include "hle_stubs.h"
#include "memory.h"
#include "hle/controller_status_contract.h"
#include "wii_remote_input.h"
#include <cstdint>
#include <dolphin/pad.h>
extern "C" bool PAD_HLE_RumbleEnabled();
void NandQueueIosCallback(uint32_t callbackPtr, int32_t result, uint32_t callbackArg);
namespace {
constexpr uint32_t kDefaultWorkMemSize = 0x20000;
constexpr uint8_t kDefaultDpdSensitivity = 3;
constexpr int32_t kStatusOk = 0;
struct WpadStubState {
bool initSubRan = false;
bool simpleSyncActive = false;
uint32_t syncDeviceCallback = 0;
uint32_t workMemSize = kDefaultWorkMemSize;
uint8_t dpdSensitivity = kDefaultDpdSensitivity;
WpadContract::State contract{};
};
WpadStubState g_state{};
void InvokeWpadCallback(uint32_t callback, uint32_t chan, int32_t result)
{
if (callback == 0) {
return;
}
if (!TranslatedFunctionRegistry::FindByAddressPtr(callback)) {
return;
}
auto& cpu = GetPersistentCpuContext();
cpu.gpr[3] = chan;
cpu.gpr[4] = static_cast<uint32_t>(result);
InvokeIndirectCpu(callback, &cpu);
}
int32_t InitializeWpadLibrary()
{
g_state.contract.Initialize();
return kStatusOk;
}
// The async WPAD entry points all report their outcome to the guest callback and
// then return the same value.
int32_t CompleteWpadRequest(uint32_t chan, uint32_t callback, int32_t result)
{
InvokeWpadCallback(callback, chan, result);
return result;
}
} // namespace
extern "C" int32_t WPADGetStatus_HLE()
{
// RVL::WPADGetStatus takes no channel arg; it reports global WUD library state.
// Reading r3 here would leak a caller's stale register value into the result.
return g_state.contract.GetLibraryStatus();
}
PPC_NATIVE_OVERRIDE(801BF64C, WPADGetStatus_HLE, int32_t, (), ());
extern "C" uint32_t WPADGetDpdSensitivity_HLE()
{
return static_cast<uint32_t>(g_state.dpdSensitivity);
}
PPC_NATIVE_OVERRIDE(801C329C, WPADGetDpdSensitivity_HLE, uint32_t, (), ());
extern "C" int32_t WPADInitSub_HLE()
{
if (!g_state.initSubRan) {
g_state.initSubRan = true;
return InitializeWpadLibrary();
}
return kStatusOk;
}
PPC_NATIVE_OVERRIDE(801BF3B4, WPADInitSub_HLE, int32_t, (), ());
extern "C" int32_t WPADInit_HLE()
{
return InitializeWpadLibrary();
}
PPC_NATIVE_OVERRIDE(801BF5C4, WPADInit_HLE, int32_t, (), ());
extern "C" int32_t WUDGetStatus_HLE()
{
return WpadContract::kStatusReady;
}
PPC_NATIVE_OVERRIDE(801CDB84, WUDGetStatus_HLE, int32_t, (), ());
// WPADGetDataFormat: the format KPAD runs the attached extension in (WPAD_FMT_*_ACC_DPD).
// WPADClampStick keys off it. Can't reuse the translated SDK implementation: it dereferences
// Bluetooth control blocks that HLE'd WPADInit never constructs.
extern "C" int32_t WPADGetDataFormat_HLE(uint32_t chan)
{
const int32_t status = g_state.contract.GetDataFormat(chan);
if (status < 0) {
return status;
}
switch (WiiRemoteInput::EffectiveKind(chan)) {
case WiiRemoteInput::Kind::RemoteWithNunchuk: return 5;
case WiiRemoteInput::Kind::RemoteWithClassic: return 8;
default: return 2;
}
}
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);
}
return WpadContract::kErrorNoController;
}
PPC_NATIVE_OVERRIDE(801C0990, WPADProbe_HLE, int32_t, (uint32_t chan, uint32_t typePtr), (chan, typePtr));
extern "C" void WPADControlMotor_HLE(uint32_t chan, uint32_t command)
{
const bool on = command != 0 && PAD_HLE_RumbleEnabled();
PADControlMotor(chan, on ? PAD_MOTOR_RUMBLE : PAD_MOTOR_STOP_HARD);
}
PPC_NATIVE_OVERRIDE(801C0EC4, WPADControlMotor_HLE, void, (uint32_t chan, uint32_t command), (chan, command));
extern "C" int32_t WPADGetInfoAsync_HLE(uint32_t chan, uint32_t infoPtr, uint32_t callback)
{
if (chan >= WpadContract::kChannelCount) {
return CompleteWpadRequest(chan, callback, WpadContract::kErrorBadChannel);
}
(void)infoPtr;
return CompleteWpadRequest(chan, callback, WpadContract::kErrorNoController);
}
PPC_NATIVE_OVERRIDE(801C0CA4, WPADGetInfoAsync_HLE, int32_t,
(uint32_t chan, uint32_t infoPtr, uint32_t callback), (chan, infoPtr, callback));
extern "C" int32_t WPADControlLed_HLE(uint32_t chan, uint32_t ledMask, uint32_t callback)
{
if (chan >= WpadContract::kChannelCount) {
return CompleteWpadRequest(chan, callback, WpadContract::kErrorBadChannel);
}
(void)ledMask;
return CompleteWpadRequest(chan, callback, WpadContract::kErrorNoController);
}
PPC_NATIVE_OVERRIDE(801C0FF8, WPADControlLed_HLE, int32_t,
(uint32_t chan, uint32_t ledMask, uint32_t callback), (chan, ledMask, callback));
extern "C" int32_t WPADStartSimpleSync_HLE()
{
if (g_state.simpleSyncActive) {
return 0;
}
g_state.simpleSyncActive = true;
return 1;
}
PPC_NATIVE_OVERRIDE(801BF634, WPADStartSimpleSync_HLE, int32_t, (), ()); // WUDStartSyncSimple
PPC_NATIVE_OVERRIDE(801BF638, WPADStartSimpleSync_HLE, int32_t, (), ()); // WPADStartSimpleSync (HBM)
// due to multiplayer controller screen hle this to avoid startsyncdevice to return fail every frame
// causing you to get softlocked in the game
extern "C" int32_t WPADStopSimpleSync_HLE()
{
if (g_state.simpleSyncActive) {
g_state.simpleSyncActive = false;
const uint32_t callback = g_state.syncDeviceCallback;
if (callback != 0 && TranslatedFunctionRegistry::FindByAddressPtr(callback)) {
NandQueueIosCallback(callback, 1, 0); // callback(WUD_SYNC_DONE, devicesSynced=0)
}
}
return 1;
}
PPC_NATIVE_OVERRIDE(801BF63C, WPADStopSimpleSync_HLE, int32_t, (), ());
extern "C" uint32_t WPADSetSyncDeviceCallback_HLE(uint32_t callback)
{
const uint32_t previous = g_state.syncDeviceCallback;
g_state.syncDeviceCallback = callback;
return previous;
}
PPC_NATIVE_OVERRIDE(801BF640, WPADSetSyncDeviceCallback_HLE, uint32_t, (uint32_t callback), (callback));
+19
View File
@@ -4,6 +4,7 @@
#include "hle_stubs.h"
#include "memory.h"
#include "hle/bluetooth_hle.h"
#include "hle/net/network.h"
#include <cstdint>
@@ -120,6 +121,10 @@ extern "C" void IOS_OpenAsync_HLE(CpuContext* ctx) {
const uint32_t callbackArg = ctx->gpr[6];
const char* path = pathPtr ? (const char*)Memory::GetPointer(pathPtr) : "(null)";
if (pathPtr) {
if (const int32_t btFd = Bluetooth_HLE_OpenDevice(path)) {
CompleteAsync(ctx, callback, btFd, callbackArg);
return;
}
if (const int32_t netFd = Network_HLE_OpenDevice(path, mode)) {
CompleteAsync(ctx, callback, netFd, callbackArg);
return;
@@ -135,6 +140,11 @@ extern "C" void IOS_CloseAsync_HLE(CpuContext* ctx) {
const uint32_t fd = ctx->gpr[3];
const uint32_t callback = ctx->gpr[4];
const uint32_t callbackArg = ctx->gpr[5];
if (Bluetooth_HLE_IsFd(fd)) {
const int32_t result = Bluetooth_HLE_Close(fd);
CompleteAsync(ctx, callback, result, callbackArg);
return;
}
if (Network_HLE_IsFd(fd)) {
const int32_t result = Network_HLE_Close(fd);
CompleteAsync(ctx, callback, result, callbackArg);
@@ -179,6 +189,15 @@ extern "C" void IOS_IoctlvAsync_HLE(CpuContext* ctx)
const uint32_t vectorPtr = ctx->gpr[7];
const uint32_t callback = ctx->gpr[8];
const uint32_t callbackArg = ctx->gpr[9];
if (Bluetooth_HLE_IsFd(fd)) {
int32_t result = 0;
if (Bluetooth_HLE_IoctlvAsync(fd, cmd, numIn, numOut, vectorPtr, callback, callbackArg, result)) {
CompleteAsync(ctx, callback, result, callbackArg);
} else {
SetIosReturn(ctx, 0);
}
return;
}
if (TryDeferredNetworkAsync(
ctx, fd, callback, callbackArg,
[&] {
+5
View File
@@ -10,6 +10,7 @@
#include "hle_stubs.h"
#include "ppc_runtime.h"
#include "runtime_log.h"
#include "hle/bluetooth_hle.h"
#include "hle/net/network.h"
#include "guest_small_data.h"
#include "os_internal.h"
@@ -237,6 +238,10 @@ bool ProcessAlarmQueue(CpuContext* cpu, int maxToProcess)
handledAny = true;
completionNeedsReschedule = true;
}
if (Bluetooth_HLE_ProcessCompletions(cpu)) {
handledAny = true;
completionNeedsReschedule = true;
}
if (NandProcessPendingCallbacks(cpu, maxToProcess)) {
handledAny = true;
+7 -24
View File
@@ -80,33 +80,16 @@ extern "C" void OSFatal_HLE_801a4ec4(CpuContext* ctx) {
REGISTER_NATIVE_FUNCTION_AS(0x801A4EC4, OSFatal_HLE_801a4ec4, "OSFatal_HLE_801a4ec4");
extern "C" void GKI_delay_HLE_801301b4(CpuContext* ctx)
extern "C" void GKI_delay_HLE_801301b4()
{
const uint32_t delayMs = ctx ? static_cast<uint32_t>(ctx->gpr[3]) : 0;
const uint32_t sleepMs = delayMs == 0 ? 1u : std::min(delayMs, 10u);
std::this_thread::sleep_for(std::chrono::milliseconds(sleepMs));
// The guest's GKI_delay is empty: BTA_Init spins on it while the Bluetooth
// module answers through IPC interrupts and the 2 ms BTU alarm. Service both
// here and let the wall clock advance.
OS_HLE_ProcessAlarms(32);
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
extern "C" uint32_t BTM_IsDeviceUp_HLE_8013a300(CpuContext* ctx)
{
// Force Bluetooth stack to "up" to avoid endless polling loops while we lack
// real hardware bring-up.
constexpr uint32_t kBtmCbBase = 0x80336278u;
constexpr uint32_t kDevStateOffset = 0x64Eu;
try {
::Memory::Write8(kBtmCbBase + kDevStateOffset, 5u);
} catch (const ::Memory::AccessViolation&) {
// Ignore; best-effort write
}
if (ctx) {
ctx->gpr[3] = 1;
}
return 1;
}
PPC_NATIVE_OVERRIDE_VOID(801301B4, GKI_delay_HLE_801301b4, (CpuContext* ctx), (ctx));
PPC_NATIVE_OVERRIDE(8013A300, BTM_IsDeviceUp_HLE_8013a300, uint32_t, (CpuContext* ctx), (ctx));
PPC_NATIVE_OVERRIDE_VOID(801301B4, GKI_delay_HLE_801301b4, (), ());
// Serial Interface (SI) - GameCube controller ports; stubbed since we don't emulate the MMIO.
+1
View File
@@ -10,6 +10,7 @@
#include "memory.h"
#include "nand_path.h"
#include "nand_save_probe.h"
#include "hle/bluetooth_hle.h"
#include "hle/net/network.h"
#include "recomp_mod_loader.h"
#include "runtime_config.h"
+13
View File
@@ -382,6 +382,9 @@ extern "C" int32_t NAND_IOS_Open_HLE(uint32_t pathPtr, uint32_t mode) {
if (const int32_t netFd = Network_HLE_OpenDevice(path, mode)) {
return netFd;
}
if (const int32_t btFd = Bluetooth_HLE_OpenDevice(path)) {
return btFd;
}
if (std::strcmp(path, "/dev/dolphin") == 0) {
return DOLPHIN_DEV_FD;
}
@@ -452,6 +455,9 @@ extern "C" int32_t NAND_IOS_Close_HLE(uint32_t fd) {
if (Network_HLE_IsFd(fd)) {
return Network_HLE_Close(fd);
}
if (Bluetooth_HLE_IsFd(fd)) {
return Bluetooth_HLE_Close(fd);
}
auto* handle = GetHandle(fd);
if (!handle) {
@@ -566,6 +572,10 @@ extern "C" int32_t NAND_IOS_Ioctl_HLE(
LogNandWarning("IOS_Ioctl", "/dev/sha does not support scalar ioctl cmd=%u", cmd);
return ISFS_EINVAL;
}
if (Bluetooth_HLE_IsFd(fd)) {
LogNandWarning("IOS_Ioctl", "/dev/usb/oh1 does not support scalar ioctl cmd=%u", cmd);
return -4;
}
if (fd == DOLPHIN_DEV_FD) {
LogNandWarning("IOS_Ioctl", "/dev/dolphin does not support scalar ioctl cmd=%u", cmd);
return ISFS_EINVAL;
@@ -1032,6 +1042,9 @@ extern "C" int32_t NAND_IOS_Ioctlv_HLE(
if (Network_HLE_IsFd(fd)) {
return Network_HLE_Ioctlv(fd, cmd, numIn, numOut, vectorPtr);
}
if (Bluetooth_HLE_IsFd(fd)) {
return Bluetooth_HLE_Ioctlv(fd, cmd, numIn, numOut, vectorPtr);
}
if (GetShaHandle(static_cast<int32_t>(fd))) {
return HandleShaIoctlv(static_cast<int32_t>(fd), cmd, numIn, numOut, vectorPtr);
+8 -5
View File
@@ -63,7 +63,7 @@
#include "system_bridge.h"
#include "ppc_runtime.h"
#include "aurora_events.h"
#include "wii_remote_input.h"
#include "wiimote_host.h"
#include "discord_presence.h"
#include "fiber_manager.h"
#include "hle_stubs.h"
@@ -1430,10 +1430,9 @@ 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));
// SDL reads its device hints when the joystick subsystem starts, which
// aurora_initialize does.
Wiimote::ConfigureSdlHints(RuntimeConfigFile::WiiRemotesEnabled());
const AuroraInfo auroraInfo = aurora_initialize(0, nullptr, &auroraConfig);
if (auroraInfo.initializationStatus != AURORA_INITIALIZATION_SUCCESS) {
@@ -1450,6 +1449,7 @@ int RuntimeMain(int argc, char** argv) {
<< std::endl;
}
aurora_set_frame_worker_wait_callback(ServiceGuestTimingDuringAuroraFrameWait);
Wiimote::Initialize();
GxGuestWrite::InstallAuroraHooks();
UpdateMkwDynamicAspectSurface(auroraInfo.windowSize.native_fb_width,
auroraInfo.windowSize.native_fb_height);
@@ -1493,6 +1493,7 @@ int RuntimeMain(int argc, char** argv) {
// Shutdown fiber system
Fiber::GuestFiberManager::Shutdown();
WindowPlacementPersistence::Flush(true);
Wiimote::Shutdown();
aurora_shutdown();
DiscordPresence::Shutdown();
SetRuntimeExitCodeImpl(0);
@@ -1511,6 +1512,7 @@ int RuntimeMain(int argc, char** argv) {
SetRuntimeExitCodeImpl(1);
Fiber::GuestFiberManager::Shutdown();
WindowPlacementPersistence::Flush(true);
Wiimote::Shutdown();
aurora_shutdown();
DiscordPresence::Shutdown();
ShutdownProcessTranscript();
@@ -1523,6 +1525,7 @@ int RuntimeMain(int argc, char** argv) {
SetRuntimeExitCodeImpl(1);
Fiber::GuestFiberManager::Shutdown();
WindowPlacementPersistence::Flush(true);
Wiimote::Shutdown();
aurora_shutdown();
DiscordPresence::Shutdown();
ShutdownProcessTranscript();
+7
View File
@@ -1,4 +1,5 @@
#include "memory.h"
#include "hle/hollywood_gpio.h"
#include <algorithm>
#include <array>
@@ -643,6 +644,9 @@ uint16_t MemoryInline::Read16Slow(uint32_t addr) {
uint32_t MemoryInline::Read32Slow(uint32_t addr) {
if (IsMmioAddress(addr)) {
if (uint32_t value = 0; HollywoodGpio_Read32(addr, value)) {
return value;
}
ThrowMmioReadBlocked(addr, sizeof(uint32_t));
}
ResolveDeferredReads(addr, sizeof(uint32_t));
@@ -708,6 +712,9 @@ void MemoryInline::Write32Slow(uint32_t addr, uint32_t val) {
return;
}
if (IsMmioAddress(addr)) {
if (HollywoodGpio_Write32(addr, val)) {
return;
}
throw Memory::AccessViolation(addr, sizeof(val), "MMIO write blocked (non-GPU)");
}
WriteScalar(addr, val);
+96 -141
View File
@@ -8,7 +8,7 @@
#include "music_attenuation.h"
#include "runtime_config.h"
#include "runtime_log.h"
#include "wii_remote_input.h"
#include "wiimote_host.h"
#include <imgui.h>
#include <SDL3/SDL_events.h>
@@ -259,9 +259,9 @@ void ApplyConfiguredMappings() {
// 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) {
// the shared bindings on top swaps A/B and X/Y.
if (const char* name = PADGetNameForControllerIndex(static_cast<uint32_t>(controllerIndex));
name != nullptr && std::string_view(name).find("Wii U Pro") != std::string_view::npos) {
continue;
}
@@ -295,47 +295,11 @@ void ApplyConfiguredMappings() {
}
}
bool g_wiiRemotesEnabled = RuntimeConfigFile::WiiRemotesEnabled(true);
bool g_wiiContinuousScan = RuntimeConfigFile::WiiContinuousScanEnabled(false);
bool g_wiiRemotesEnabled = RuntimeConfigFile::WiiRemotesEnabled();
bool g_wiiAutoPair = RuntimeConfigFile::WiiAutoPairEnabled(true);
bool g_wiiSensorBarAbove = RuntimeConfigFile::WiiSensorBarAbove(false);
int g_wiiIrSensitivity = RuntimeConfigFile::WiiIrSensitivity(3);
// Accelerometer readout and zero-point calibration for a bare remote / remote + Nunchuk.
void DrawWiiRemoteAccelerometer(uint32_t port) {
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.");
}
// Residual per-axis bias left after SDL's factory-calibrated decode, measured
// here with the remote at rest.
if (WiiRemoteInput::IsAccelCalibrating(port)) {
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();
const auto stored = WiiRemoteInput::ReadAccelOffset(port);
const std::array<double, 3> offset = stored.value_or(std::array<double, 3>{});
const bool calibrated = offset[0] != 0.0 || offset[1] != 0.0 || offset[2] != 0.0;
ImGui::BeginDisabled(!calibrated || WiiRemoteInput::IsAccelCalibrating(port));
if (ImGui::Button("Clear calibration")) {
WiiRemoteInput::ClearAccelCalibration(port);
}
ImGui::EndDisabled();
if (const char* message = WiiRemoteInput::AccelCalibrationMessage(port)) {
ImGui::TextWrapped("%s", message);
} else if (calibrated) {
ImGui::TextDisabled("Stored offset: x %+.3f y %+.3f z %+.3f g", offset[0], offset[1], offset[2]);
} else {
ImGui::TextDisabled("No trim stored.");
}
}
constexpr float kControllerMenuWidth = 460.0f;
@@ -343,116 +307,101 @@ void PushMenuTextWrap() {
ImGui::PushTextWrapPos(0.0f);
}
// Driver switch, pairing help and scanning; applies to every port.
std::string WiiRemoteLabel(const Wiimote::SlotInfo& info) {
std::string label = info.model;
if (!info.extension.empty()) {
label += " + " + info.extension;
}
return label;
}
// Pairing, the console's Wii Remote settings and the driver switch; applies to every remote.
void DrawWiiRemoteSetup() {
if (!ImGui::CollapsingHeader("Wii Remote setup (Bluetooth)")) {
return;
}
ImGui::Indent();
PushMenuTextWrap();
#if defined(_WIN32)
ImGui::TextUnformatted("1. Windows Settings > Bluetooth > Add device.");
#elif defined(__APPLE__)
ImGui::TextUnformatted("1. System Settings > Bluetooth.");
#else
ImGui::TextUnformatted("1. Open your system's Bluetooth settings.");
#endif
ImGui::TextUnformatted("2. Press 1+2 (or the red SYNC button) on the remote. Leave the PIN empty.");
ImGui::TextDisabled("Paired before? Press 1+2 to turn it on. It then appears in the player list.");
const std::string notice = Wiimote::PlatformNotice();
if (!notice.empty()) {
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f), "%s", notice.c_str());
}
const Wiimote::PairingInfo pairing = Wiimote::Pairing();
if (pairing.supported) {
ImGui::TextUnformatted("Press the red SYNC button (or 1 and 2) on the Wii Remote, like on a Wii.");
ImGui::TextDisabled("Paired before? Just press any button on it.");
} else if (g_wiiRemotesEnabled) {
ImGui::TextUnformatted("Pair the Wii Remote in your system's Bluetooth settings, then press any button on it.");
}
ImGui::PopTextWrapPos();
// 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("Scan now")) {
WiiRemoteInput::RescanNow();
ImGui::BeginDisabled(!pairing.supported);
if (ImGui::Button("Pair a Wii Remote")) {
Wiimote::StartPairing();
}
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");
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Searches for a minute, like pressing SYNC on the console.");
}
if (ImGui::Checkbox("Keep scanning for Wii Remotes", &g_wiiContinuousScan)) {
RuntimeConfigFile::SetWiiContinuousScanEnabled(g_wiiContinuousScan);
ImGui::SameLine();
if (pairing.searching) {
ImGui::TextDisabled("Searching...");
} else if (!pairing.status.empty()) {
ImGui::TextDisabled("%s", pairing.status.c_str());
}
if (const size_t waiting = Wiimote::WaitingRemotes(); waiting != 0) {
ImGui::TextDisabled("%zu more Wii Remote%s connected; joins when a player leaves.", waiting,
waiting == 1 ? "" : "s");
}
if (ImGui::Checkbox("Look for Wii Remotes automatically", &g_wiiAutoPair)) {
RuntimeConfigFile::SetWiiAutoPairEnabled(g_wiiAutoPair);
}
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.");
ImGui::SetTooltip("While no Wii Remote is connected, keep searching for one in pairing mode.\n"
"Searching can briefly disturb Bluetooth headphones.");
}
if (ImGui::Checkbox("Use Wii Remotes / Wii U Pro Controllers", &g_wiiRemotesEnabled)) {
ImGui::SetNextItemWidth(160.0f);
int sensorBar = g_wiiSensorBarAbove ? 1 : 0;
if (ImGui::Combo("Sensor bar", &sensorBar, "Below TV\0Above TV\0")) {
g_wiiSensorBarAbove = sensorBar == 1;
RuntimeConfigFile::SetWiiSensorBarAbove(g_wiiSensorBarAbove);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Takes effect the next time the game starts.");
}
ImGui::SetNextItemWidth(160.0f);
if (ImGui::SliderInt("Pointer sensitivity", &g_wiiIrSensitivity, 1, 5, "%d", ImGuiSliderFlags_AlwaysClamp)) {
RuntimeConfigFile::SetWiiIrSensitivity(g_wiiIrSensitivity);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("The sensor bar sensitivity from the Wii settings.\nTakes effect the next time the game starts.");
}
if (ImGui::Checkbox("Use Wii Remotes", &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::SetTooltip("Takes effect on the next launch. Turn this off to use a Wii U Pro Controller as a gamepad.");
}
ImGui::Unindent();
}
void DrawWiiDiagnostics(uint32_t port, WiiRemoteInput::Kind kind) {
if (kind == WiiRemoteInput::Kind::RemoteWithClassic) {
WiiRemoteInput::KpadSample sample;
if (WiiRemoteInput::ReadKpadSample(port, 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 Wii Remote the game has on this player's channel.
void DrawWiiRemoteDetails(uint32_t port, const Wiimote::SlotInfo& info) {
ImGui::Text("%s", WiiRemoteLabel(info).c_str());
if (info.battery >= 0) {
ImGui::SameLine();
ImGui::TextDisabled("Battery %d/4", info.battery);
}
if (kind == WiiRemoteInput::Kind::WiiUPro) {
if (SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(port))) {
// 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. A button that never turns to capitals is not reaching SDL.");
}
}
}
// Selected port holds a Wii controller: no mapping, only motion calibration and diagnostics.
void DrawWiiPortDetails(uint32_t port, WiiRemoteInput::Kind kind) {
PushMenuTextWrap();
if (kind == WiiRemoteInput::Kind::WiiUPro) {
ImGui::TextUnformatted("Uses Nintendo's own layout, buttons work as labelled. Nothing to set up.");
} else {
if (info.asleep) {
ImGui::TextUnformatted("Disconnected by the game. Press any button on it to reconnect.");
} else if (info.link == Bluetooth::LinkState::Ready) {
ImGui::TextUnformatted("The game reads it like a real Wii Remote. Nothing to set up.");
} else {
ImGui::TextUnformatted("Connecting to the game...");
}
ImGui::PopTextWrapPos();
const bool remote = kind == WiiRemoteInput::Kind::Remote || kind == WiiRemoteInput::Kind::RemoteWithNunchuk ||
kind == WiiRemoteInput::Kind::RemoteWithClassic;
if (remote && ImGui::CollapsingHeader("Motion calibration")) {
ImGui::Indent();
DrawWiiRemoteAccelerometer(port);
ImGui::Unindent();
}
if ((kind == WiiRemoteInput::Kind::RemoteWithClassic || kind == WiiRemoteInput::Kind::WiiUPro) &&
ImGui::TreeNode("Diagnostics")) {
DrawWiiDiagnostics(port, kind);
ImGui::TreePop();
if (!info.asleep && ImGui::Button("Disconnect")) {
Wiimote::Disconnect(port);
}
}
@@ -946,6 +895,8 @@ void DrawPlayerRow(uint32_t port) {
preview = "Keyboard and mouse";
} else if (assigned >= 0) {
preview = ControllerLabel(static_cast<uint32_t>(assigned));
} else if (const Wiimote::SlotInfo remote = Wiimote::Slot(port); remote.attached) {
preview = WiiRemoteLabel(remote);
}
ImGui::PushID(static_cast<int>(port));
@@ -994,21 +945,29 @@ void DrawControllerSettings() {
for (uint32_t port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
DrawPlayerRow(port);
}
if (PADCount() == 0) {
bool anyRemote = false;
for (uint32_t player = 0; player < PAD_MAX_CONTROLLERS; ++player) {
anyRemote |= Wiimote::Slot(player).attached;
}
if (PADCount() == 0 && !anyRemote) {
ImGui::TextDisabled("No controller detected. Plug one in, or pair a Wii Remote below.");
}
const uint32_t port = static_cast<uint32_t>(g_controllerPort);
ImGui::SeparatorText(("Player " + std::to_string(port + 1)).c_str());
const WiiRemoteInput::Kind kind = WiiRemoteInput::KindForPort(port);
const Wiimote::SlotInfo remote = Wiimote::Slot(port);
if (remote.attached) {
DrawWiiRemoteDetails(port, remote);
if (KeyboardActive(port) || PADGetIndexForPort(port) >= 0) {
ImGui::Spacing();
}
}
if (KeyboardActive(port)) {
DrawKeyboardSettings(port);
} else if (PADGetIndexForPort(port) < 0) {
ImGui::TextDisabled("No device. Pick one in the list above.");
} else if (kind != WiiRemoteInput::Kind::NotWii || WiiRemoteInput::IsRemoteChannel(port)) {
ImGui::Text("%s", WiiRemoteInput::KindLabel(kind == WiiRemoteInput::Kind::NotWii ? WiiRemoteInput::Kind::Remote
: kind));
DrawWiiPortDetails(port, kind);
if (!remote.attached) {
ImGui::TextDisabled("No device. Pick one in the list above.");
}
} else {
ImGui::TextUnformatted("Ready to play.");
if (ImGui::CollapsingHeader("Customize buttons")) {
@@ -1606,6 +1565,7 @@ void ReleaseControllers() noexcept {
constexpr std::array<uint32_t, PAD_MAX_CONTROLLERS> stopAll{
PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD};
PADControlAllMotors(stopAll.data());
Wiimote::Shutdown();
// SDL hands LED and rumble reports to its own HIDAPI sender thread rather
// than writing them here, so without this the process dies before the
// controller ever receives them.
@@ -1616,11 +1576,6 @@ 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();
File diff suppressed because it is too large Load Diff
+260
View File
@@ -0,0 +1,260 @@
// Wii Remote extension encryption (first-party key schedule). Ported from
// Dolphin (Source/Core/Core/HW/WiimoteEmu/Encryption.cpp, GPL-2.0-or-later),
// based on Hector Martin's reverse engineering.
#include "wiimote_ext_crypto.h"
#include <algorithm>
#include <bit>
namespace Wiimote::ExtCrypto {
namespace Detail {
using SBox = std::array<uint8_t, 256>;
constexpr SBox kKeygenSbox = {
0x70, 0x51, 0x03, 0x86, 0x40, 0x0d, 0x4f, 0xeb, 0x3e, 0xcc, 0xd1, 0x87, 0x35, 0xbd, 0xf5, 0x0b,
0x5e, 0xd0, 0xf8, 0xf2, 0xd5, 0xe2, 0x6c, 0x31, 0x0c, 0xad, 0xfc, 0x21, 0xc3, 0x78, 0xc1, 0x06,
0xc2, 0x4c, 0x55, 0xe6, 0x4a, 0x34, 0x48, 0x11, 0x1e, 0xda, 0xe7, 0x1a, 0x84, 0xa0, 0x96, 0xa7,
0xe3, 0x7f, 0xaf, 0x63, 0x9c, 0xfa, 0x23, 0x5b, 0x79, 0xc8, 0x9e, 0xba, 0xb2, 0xc9, 0x22, 0x12,
0x4b, 0xb3, 0xa1, 0xb6, 0x32, 0x49, 0xa2, 0xe1, 0x89, 0x39, 0x10, 0x66, 0xc5, 0x07, 0x8f, 0x54,
0xea, 0x91, 0xca, 0x3f, 0xf9, 0x19, 0xf0, 0xd7, 0x46, 0xbc, 0x28, 0x1b, 0x61, 0xe8, 0x2f, 0x6a,
0xae, 0x9d, 0xf6, 0x4e, 0x09, 0x14, 0x77, 0x4d, 0xdb, 0x1f, 0x2e, 0x7b, 0x7c, 0xf1, 0x43, 0xa3,
0x00, 0xb8, 0x13, 0x8c, 0x85, 0xb9, 0x29, 0x75, 0x88, 0xfd, 0xd2, 0x56, 0x1c, 0x50, 0x97, 0x41,
0xe5, 0x3b, 0x60, 0xb5, 0xc0, 0x64, 0xee, 0x98, 0xd6, 0x2d, 0x25, 0xa4, 0xaa, 0xcd, 0x7d, 0xa8,
0x83, 0xc6, 0xab, 0xbe, 0x44, 0x99, 0x26, 0x3c, 0xce, 0x9f, 0xbf, 0xd3, 0xcb, 0x76, 0x7a, 0x7e,
0x82, 0x01, 0x8a, 0x9a, 0x80, 0x1d, 0x0e, 0xb0, 0x5c, 0xd4, 0x38, 0x62, 0xf4, 0x30, 0xe0, 0x8e,
0x53, 0xb7, 0x02, 0x57, 0xac, 0xa6, 0x52, 0x0a, 0x6d, 0x92, 0x65, 0x17, 0x24, 0x33, 0x45, 0x72,
0x74, 0xb1, 0xb4, 0xf7, 0x5d, 0xed, 0x2c, 0xff, 0x47, 0x37, 0x5a, 0x90, 0xbb, 0xdf, 0x2a, 0x16,
0x59, 0x95, 0xd9, 0xc4, 0x27, 0x67, 0x73, 0xc7, 0x68, 0xfe, 0xa5, 0xdd, 0x6b, 0x5f, 0x93, 0xd8,
0xec, 0x05, 0x3a, 0x8d, 0x6e, 0xfb, 0x3d, 0xa9, 0x69, 0x36, 0xf3, 0x94, 0xde, 0xef, 0x15, 0x6f,
0x8b, 0x9b, 0x08, 0x0f, 0xdc, 0x81, 0x18, 0x20, 0x04, 0xe4, 0x71, 0xcf, 0xe9, 0x2b, 0x42, 0x58,
};
constexpr std::array<SBox, 8> kSboxes = {{
{
0x01, 0xa0, 0xa9, 0x62, 0xd6, 0x3f, 0x85, 0xa7, 0xb6, 0xd4, 0xfa, 0x15, 0x66, 0x17, 0x09,
0xbd, 0x5d, 0x14, 0x34, 0x26, 0x59, 0x72, 0x91, 0x54, 0x06, 0x4f, 0xf8, 0xb0, 0x5b, 0x74,
0x93, 0x99, 0x8c, 0xf2, 0x45, 0xcd, 0xea, 0x4e, 0xad, 0x10, 0x4a, 0xe5, 0xca, 0xee, 0xdf,
0xc6, 0x6f, 0x9f, 0x88, 0x8e, 0x02, 0xcc, 0x08, 0xa8, 0x77, 0x94, 0x6d, 0x21, 0xb1, 0x28,
0xe4, 0x39, 0x79, 0x96, 0x60, 0x71, 0x81, 0x16, 0x2e, 0xe6, 0x78, 0xb9, 0xc4, 0x46, 0x9a,
0x42, 0xae, 0xb7, 0x7c, 0x43, 0xb3, 0x22, 0x1a, 0x86, 0xc2, 0x32, 0x3d, 0x2d, 0x9c, 0xd2,
0x29, 0xe9, 0x63, 0x9b, 0xd1, 0x31, 0x38, 0x5e, 0x1e, 0x36, 0x41, 0xbb, 0x03, 0x18, 0x2b,
0x3e, 0xbf, 0x68, 0x61, 0xfc, 0x52, 0xc0, 0xde, 0xe0, 0x0a, 0x58, 0x13, 0x5a, 0x00, 0xbe,
0x1c, 0x90, 0x0e, 0x53, 0x12, 0xfd, 0xe2, 0x6e, 0xba, 0xce, 0x24, 0x27, 0x44, 0x7f, 0x87,
0xa3, 0xa1, 0xd5, 0x50, 0x40, 0xe3, 0xf9, 0x83, 0xf7, 0xc7, 0xa2, 0x35, 0xc8, 0xdb, 0x19,
0xab, 0x2f, 0x11, 0x25, 0xed, 0x33, 0x9e, 0x55, 0xe1, 0x48, 0xaf, 0x73, 0x84, 0xda, 0x2a,
0xaa, 0x51, 0xeb, 0x9d, 0x95, 0xb2, 0xcb, 0xe7, 0x70, 0x80, 0xfe, 0x4c, 0x65, 0x04, 0xef,
0xc5, 0xf1, 0xc3, 0x3a, 0xb4, 0xf5, 0x5f, 0x23, 0x89, 0xdd, 0x30, 0xa5, 0x8b, 0xd3, 0xf6,
0xdc, 0x4d, 0x64, 0xd7, 0xf0, 0x8f, 0xec, 0x56, 0x37, 0x5c, 0xa4, 0x0d, 0x07, 0x76, 0x8a,
0x2c, 0x0b, 0xb5, 0xd8, 0xc1, 0x1f, 0xe8, 0x3b, 0xf4, 0x4b, 0x1b, 0x47, 0x6c, 0x49, 0x67,
0x7b, 0x92, 0xcf, 0x75, 0x7e, 0x20, 0xd9, 0x7d, 0x3c, 0x97, 0x7a, 0xd0, 0x05, 0x6b, 0x0f,
0x1d, 0xfb, 0x82, 0x98, 0x57, 0x8d, 0xf3, 0x6a, 0xbc, 0xac, 0xc9, 0xa6, 0xff, 0xb8, 0x69,
0x0c,
},
{
0x4c, 0x4d, 0x72, 0x07, 0x5a, 0x49, 0x33, 0x8d, 0xa2, 0xab, 0x46, 0x3d, 0x63, 0x0d, 0xa0,
0x97, 0xff, 0xf0, 0xf5, 0xfa, 0xc0, 0xe9, 0xdb, 0x62, 0xe4, 0xe1, 0x74, 0x43, 0xdc, 0x86,
0x18, 0x29, 0x37, 0xf4, 0x06, 0xe2, 0xed, 0x6f, 0x90, 0x48, 0x1e, 0x2d, 0x1d, 0xea, 0x73,
0x94, 0x54, 0xdf, 0x25, 0xf6, 0x47, 0x27, 0xd9, 0x11, 0x77, 0xc9, 0x84, 0x1c, 0x5b, 0x5c,
0x51, 0x81, 0xa6, 0x22, 0x3e, 0x24, 0x96, 0xc8, 0x8a, 0xec, 0x82, 0x7c, 0x09, 0xb8, 0x45,
0x4a, 0x57, 0xbb, 0x2f, 0x50, 0x75, 0x8e, 0x61, 0x70, 0x8c, 0x6c, 0xaf, 0xd0, 0xfd, 0xb4,
0x1b, 0xae, 0xde, 0xfe, 0x3b, 0xb5, 0x36, 0xbd, 0x55, 0x01, 0x0e, 0x9c, 0x41, 0x56, 0x5f,
0xb3, 0x26, 0x03, 0x83, 0xba, 0x13, 0x4b, 0xca, 0xc5, 0x0a, 0xf8, 0x60, 0xa5, 0xb9, 0xc7,
0xc3, 0x98, 0x32, 0xfb, 0x12, 0xf9, 0xa7, 0x92, 0xaa, 0x68, 0xf3, 0x78, 0x7e, 0x05, 0x20,
0x21, 0x02, 0xe8, 0xbf, 0xf2, 0xb0, 0x59, 0x8f, 0xd2, 0xcb, 0x87, 0x65, 0x15, 0xf1, 0x1a,
0xb2, 0x30, 0xad, 0xee, 0x58, 0xa3, 0x8b, 0x66, 0x1f, 0x2c, 0xd7, 0x5d, 0x19, 0x85, 0xa8,
0xe6, 0xd3, 0x6b, 0xa1, 0x0c, 0x91, 0x93, 0x6a, 0x5e, 0x0b, 0x79, 0xe3, 0xdd, 0x00, 0x4f,
0x3c, 0x89, 0x6e, 0x71, 0x69, 0xa9, 0xac, 0x40, 0xe5, 0x99, 0x28, 0xc6, 0x31, 0x4e, 0x7a,
0xcd, 0x08, 0x9e, 0x7d, 0xef, 0x17, 0xfc, 0x88, 0xd8, 0xa4, 0x6d, 0x44, 0x95, 0xd1, 0xb7,
0xd4, 0x9b, 0xbe, 0x2a, 0x34, 0x64, 0x2b, 0xcf, 0x2e, 0xeb, 0x38, 0xce, 0x23, 0xe0, 0x3a,
0x3f, 0xf7, 0x7b, 0x9f, 0x10, 0x53, 0xbc, 0x52, 0x67, 0x16, 0xe7, 0x80, 0x76, 0x04, 0xc4,
0xb6, 0xc1, 0xc2, 0x7f, 0x9a, 0xda, 0xd5, 0x39, 0x42, 0x14, 0x9d, 0xb1, 0x0f, 0x35, 0xd6,
0xcc,
},
{
0xb9, 0xda, 0x38, 0x0c, 0xa2, 0x9c, 0x09, 0x1f, 0x06, 0xb1, 0xb6, 0xfd, 0x1a, 0x69, 0x23,
0x30, 0xc4, 0xde, 0x01, 0xd1, 0xf4, 0x58, 0x29, 0x37, 0x1c, 0x7d, 0xd5, 0xbf, 0xff, 0xbd,
0xc8, 0xc9, 0xcf, 0x65, 0xbe, 0x7b, 0x78, 0x97, 0x98, 0x67, 0x08, 0xb3, 0x26, 0x57, 0xf7,
0xfa, 0x40, 0xad, 0x8e, 0x75, 0xa6, 0x7c, 0xdb, 0x91, 0x8b, 0x51, 0x99, 0xd4, 0x17, 0x7a,
0x90, 0x8d, 0xce, 0x63, 0xcb, 0x4e, 0xa0, 0xab, 0x18, 0x3a, 0x5b, 0x50, 0x7f, 0x21, 0x74,
0xc1, 0xbb, 0xb8, 0xb7, 0xba, 0x0b, 0x35, 0x95, 0x31, 0x59, 0x9a, 0x4d, 0x04, 0x07, 0x1e,
0x5a, 0x76, 0x13, 0xf3, 0x71, 0x83, 0xf0, 0x86, 0x03, 0xa8, 0x39, 0x42, 0xaa, 0x28, 0xe6,
0xe4, 0xd8, 0x5d, 0xd3, 0xd0, 0x6e, 0x6f, 0x96, 0xfb, 0x5e, 0xbc, 0x56, 0xc2, 0x5f, 0x85,
0x9b, 0xe7, 0xaf, 0xd2, 0x3b, 0x84, 0x6a, 0xa7, 0x53, 0xc5, 0x44, 0x49, 0xa5, 0xf9, 0x36,
0x72, 0x3d, 0x2c, 0xd9, 0x1b, 0xa1, 0xf5, 0x4f, 0x93, 0x9d, 0x68, 0x47, 0x41, 0x16, 0xca,
0x2a, 0x4c, 0xa3, 0x87, 0xd6, 0xe5, 0x19, 0x2e, 0x77, 0x15, 0x6d, 0x70, 0xc0, 0xdf, 0xb2,
0x00, 0x46, 0xed, 0xc6, 0x6c, 0x43, 0x60, 0x92, 0x2d, 0xa9, 0x22, 0x45, 0x8f, 0x34, 0x55,
0xae, 0xa4, 0x0a, 0x66, 0x32, 0xe0, 0xdc, 0x02, 0xac, 0xe8, 0x20, 0x8c, 0x89, 0x62, 0x4a,
0xfe, 0xee, 0xc3, 0xe3, 0x3c, 0xf1, 0x79, 0x05, 0xe9, 0xf6, 0x27, 0x33, 0xcc, 0xf2, 0x9e,
0x11, 0x81, 0x7e, 0x80, 0x10, 0x8a, 0x82, 0x9f, 0x48, 0x0d, 0xd7, 0xb4, 0xfc, 0x2f, 0xb5,
0xc7, 0xdd, 0x88, 0x14, 0x6b, 0x2b, 0x54, 0xea, 0x1d, 0x94, 0x5c, 0xb0, 0xef, 0x12, 0x24,
0xcd, 0xeb, 0xe1, 0xe2, 0x64, 0x73, 0x3f, 0x0e, 0x52, 0x61, 0x25, 0x3e, 0xf8, 0x0f, 0x4b,
0xec,
},
{
0xc0, 0x00, 0x30, 0xf6, 0x02, 0x49, 0x3d, 0x10, 0x6e, 0x20, 0xc9, 0xa6, 0x2f, 0xfe, 0x2c,
0x2b, 0x75, 0x2e, 0x45, 0x26, 0xab, 0x48, 0xa9, 0x80, 0xfc, 0x04, 0xcc, 0xd3, 0xb5, 0xba,
0xa3, 0x38, 0x31, 0x7d, 0x01, 0xd9, 0xa7, 0x7b, 0x96, 0xb6, 0x63, 0x69, 0x4e, 0xf7, 0xde,
0xe0, 0x78, 0xca, 0x50, 0xaa, 0x41, 0x91, 0x65, 0x88, 0xe4, 0x21, 0x85, 0xda, 0x3a, 0x27,
0xbe, 0x1c, 0x3e, 0x42, 0x5e, 0x17, 0x52, 0x7f, 0x1f, 0x89, 0x24, 0x6f, 0x8f, 0x5c, 0x67,
0x74, 0x0e, 0x12, 0x87, 0x8d, 0xe9, 0x34, 0xed, 0x73, 0xc4, 0xf8, 0x61, 0x5b, 0x05, 0xdf,
0x59, 0x4c, 0x97, 0x79, 0x83, 0x18, 0xa4, 0x55, 0x95, 0xeb, 0xbd, 0x53, 0xf5, 0xf1, 0x57,
0x66, 0x46, 0x9f, 0xb2, 0x81, 0x09, 0x51, 0x86, 0x22, 0x16, 0xdd, 0x23, 0x93, 0x76, 0x29,
0xc2, 0xd7, 0x1d, 0xd4, 0xbf, 0x36, 0x3f, 0xea, 0x4b, 0x11, 0x32, 0xb9, 0x62, 0x54, 0x60,
0xd6, 0x6d, 0x43, 0x9a, 0x0d, 0x92, 0x9c, 0xb0, 0xef, 0x58, 0x6c, 0x9d, 0x77, 0x2d, 0x70,
0xfa, 0xf3, 0xb3, 0x0b, 0xe2, 0x40, 0x7e, 0xf4, 0x8a, 0xe5, 0x8c, 0x3c, 0x56, 0x71, 0xd1,
0x64, 0xe1, 0x82, 0x0a, 0xcb, 0x13, 0x15, 0x90, 0xec, 0x03, 0x99, 0xaf, 0x14, 0x5d, 0x0f,
0x33, 0x4a, 0x94, 0xa5, 0xa8, 0x35, 0x1b, 0xe3, 0x6a, 0xc6, 0x28, 0xff, 0x4d, 0xe7, 0x25,
0x84, 0xac, 0x08, 0xae, 0xc5, 0xa2, 0x2a, 0xb8, 0x37, 0x0c, 0x7a, 0xa0, 0xc3, 0xce, 0xad,
0x06, 0x1a, 0x9e, 0x8b, 0xfb, 0xd5, 0xf0, 0xc1, 0x1e, 0xd0, 0xb4, 0x9b, 0xb1, 0x44, 0xf2,
0x47, 0xc7, 0x68, 0xcf, 0x72, 0xbb, 0x4f, 0x5a, 0xf9, 0xdc, 0x6b, 0xdb, 0xd2, 0xe8, 0x7c,
0xc8, 0xee, 0x98, 0xa1, 0xe6, 0xd8, 0x39, 0x07, 0x5f, 0xfd, 0x8e, 0x19, 0xb7, 0x3b, 0xbc,
0xcd,
},
{
0x7c, 0xe3, 0x81, 0x73, 0xb2, 0x11, 0xbf, 0x6f, 0x20, 0x98, 0xfe, 0x75, 0x96, 0xef, 0x6c,
0xda, 0x50, 0xe1, 0x09, 0x72, 0x54, 0x45, 0xba, 0x34, 0x80, 0x5b, 0xed, 0x3e, 0x53, 0x2c,
0x87, 0xa4, 0x57, 0xf3, 0x33, 0x3f, 0x3c, 0xb7, 0x67, 0xb4, 0xa3, 0x25, 0x60, 0x4f, 0x07,
0x6b, 0x1b, 0x47, 0x15, 0x0f, 0xe4, 0x0a, 0xea, 0xd1, 0x32, 0x78, 0x36, 0x49, 0x8d, 0x4b,
0xd2, 0xbc, 0xa5, 0xdc, 0x1d, 0x0d, 0x4d, 0xcd, 0x9a, 0x82, 0x5f, 0xfc, 0x94, 0x65, 0xbe,
0xe2, 0xf4, 0xc9, 0x1e, 0x44, 0xcb, 0x9e, 0x0c, 0x64, 0x71, 0x26, 0x63, 0xb3, 0x14, 0xe8,
0x40, 0x70, 0x8a, 0x0e, 0x19, 0x42, 0x6d, 0xac, 0x88, 0x10, 0x5c, 0xdf, 0x41, 0xa9, 0xad,
0xe5, 0xfb, 0x74, 0xcc, 0xd5, 0x06, 0x8e, 0x59, 0x86, 0xce, 0x1f, 0x3d, 0x76, 0xe0, 0x8f,
0xb9, 0x77, 0x27, 0x7b, 0xa6, 0xd8, 0x29, 0xd3, 0xec, 0xb8, 0x13, 0xf7, 0xfa, 0xc3, 0x51,
0x6a, 0xde, 0x4a, 0x5a, 0xeb, 0xc2, 0x8b, 0x23, 0x48, 0x92, 0xcf, 0x62, 0xa8, 0x99, 0xf8,
0xd0, 0x2e, 0x85, 0x61, 0x43, 0xc8, 0xbd, 0xf0, 0x05, 0x93, 0xca, 0x4e, 0xf1, 0x7d, 0x30,
0xfd, 0xc4, 0x69, 0x66, 0x2f, 0x08, 0xb1, 0x52, 0xf9, 0x21, 0xe6, 0x7a, 0x2b, 0xdd, 0x39,
0x84, 0xff, 0xc0, 0x91, 0xd6, 0x37, 0xd4, 0x7f, 0x2d, 0x9b, 0x5d, 0xa1, 0x3b, 0x6e, 0xb5,
0xc5, 0x46, 0x04, 0xf5, 0x90, 0xee, 0x7e, 0x83, 0x1c, 0x03, 0x56, 0xb6, 0xaa, 0x00, 0x17,
0x01, 0x35, 0x55, 0x79, 0x0b, 0x12, 0xbb, 0x1a, 0x31, 0xe7, 0x02, 0x28, 0x16, 0xc1, 0xf6,
0xa2, 0xdb, 0x18, 0x9c, 0x89, 0x68, 0x38, 0x97, 0xab, 0xc7, 0x2a, 0xd7, 0x3a, 0xf2, 0xc6,
0x24, 0x4c, 0xb0, 0x58, 0xa0, 0x22, 0x5e, 0x9d, 0xd9, 0xa7, 0xe9, 0xae, 0xaf, 0x8c, 0x95,
0x9f,
},
{
0x28, 0xb7, 0x20, 0xd7, 0xb0, 0x30, 0xc3, 0x09, 0x19, 0xc0, 0x67, 0xd6, 0x00, 0x3c, 0x7e,
0xe7, 0xe9, 0xf4, 0x08, 0x5a, 0xf8, 0xb8, 0x2e, 0x05, 0xa6, 0x25, 0x9e, 0x5c, 0xd8, 0x15,
0x0d, 0xe1, 0xf6, 0x11, 0x54, 0x6b, 0xcd, 0x21, 0x46, 0x66, 0x5e, 0x84, 0xad, 0x06, 0x38,
0x29, 0x44, 0xc5, 0xa2, 0xce, 0xf1, 0xaa, 0xc1, 0x40, 0x71, 0x86, 0xb5, 0xef, 0xfc, 0x36,
0xa8, 0xcb, 0x0a, 0x48, 0x27, 0x45, 0x64, 0xa3, 0xaf, 0x8c, 0xb2, 0xc6, 0x9f, 0x07, 0x89,
0xdc, 0x17, 0xd3, 0x49, 0x79, 0xfb, 0xfe, 0x1d, 0xd0, 0xb9, 0x88, 0x43, 0x52, 0xbc, 0x01,
0x78, 0x2b, 0x7d, 0x94, 0xc7, 0x0e, 0xde, 0xa5, 0xd5, 0x9b, 0xcc, 0xf7, 0x61, 0x7a, 0xc2,
0x74, 0x81, 0x39, 0x03, 0xab, 0x96, 0xa0, 0x37, 0xbd, 0x2d, 0x72, 0x75, 0x3f, 0xc9, 0xd4,
0x8e, 0x6f, 0xf9, 0x8d, 0xed, 0x62, 0xdb, 0x1c, 0xdf, 0x04, 0xac, 0x1b, 0x6c, 0x14, 0x4b,
0x63, 0xf0, 0xbf, 0xb4, 0x82, 0xec, 0x7b, 0x1a, 0x59, 0x92, 0xd2, 0x10, 0x60, 0xb6, 0x3d,
0x5f, 0xe6, 0x80, 0x6e, 0x70, 0xc4, 0xf2, 0x35, 0xd9, 0x7c, 0xee, 0xe5, 0x41, 0xa4, 0x5b,
0x50, 0xdd, 0xbb, 0x4c, 0xf3, 0x1f, 0x9d, 0x5d, 0x57, 0x55, 0x51, 0x97, 0xe3, 0x58, 0x42,
0x4d, 0x9c, 0x73, 0xba, 0xc8, 0x77, 0x31, 0x69, 0x26, 0xae, 0xea, 0x8a, 0xda, 0x22, 0xb3,
0x87, 0x56, 0xfa, 0x93, 0x0b, 0x34, 0x16, 0x33, 0xe8, 0xe4, 0x53, 0xbe, 0xa9, 0xb1, 0x3a,
0x3e, 0xf5, 0x90, 0x6a, 0xcf, 0x3b, 0x12, 0xfd, 0x8f, 0x9a, 0xa7, 0x47, 0x91, 0x99, 0xeb,
0x0f, 0x24, 0xff, 0x23, 0x18, 0x85, 0x4e, 0x7f, 0x0c, 0xe0, 0xa1, 0xe2, 0xd1, 0x2c, 0x2a,
0x4a, 0x02, 0x4f, 0x1e, 0x95, 0x68, 0x8b, 0x98, 0x83, 0x6d, 0x76, 0xca, 0x65, 0x32, 0x13,
0x2f,
},
{
0xc3, 0x82, 0x9a, 0xa4, 0xba, 0x81, 0x60, 0x37, 0x34, 0x35, 0xfc, 0x80, 0xa8, 0x51, 0x65,
0x67, 0xed, 0x30, 0x5f, 0x10, 0xd3, 0x4a, 0x27, 0x2f, 0x13, 0xb9, 0x2a, 0xd2, 0xcc, 0xe1,
0xef, 0xae, 0xeb, 0xbe, 0xf4, 0xbd, 0xcf, 0x43, 0xb3, 0xc5, 0x88, 0x84, 0xb7, 0xdd, 0x39,
0x40, 0xce, 0x48, 0x6d, 0x9b, 0x72, 0x61, 0x7e, 0xe7, 0xa1, 0x4e, 0x53, 0x2e, 0x77, 0x3b,
0xe2, 0xc9, 0x36, 0x22, 0x1b, 0x6e, 0x73, 0xb1, 0x03, 0xb2, 0x4c, 0x87, 0xa9, 0xd4, 0x4d,
0x0f, 0xd8, 0x15, 0x6c, 0xaa, 0x18, 0xf6, 0x49, 0x57, 0x5d, 0xfb, 0x7a, 0x14, 0x94, 0x63,
0xa0, 0x11, 0xb0, 0x9e, 0xde, 0x05, 0x46, 0xc8, 0xee, 0x47, 0xdb, 0xdc, 0x24, 0x89, 0x9c,
0x91, 0x97, 0x29, 0xe9, 0x7b, 0xc1, 0x07, 0x1e, 0xb8, 0xfd, 0xfe, 0xac, 0xc6, 0x62, 0x98,
0x4f, 0xf1, 0x79, 0xe0, 0xe8, 0x6b, 0x78, 0x56, 0xb6, 0x8d, 0x04, 0x50, 0x86, 0xca, 0x6f,
0x20, 0xe6, 0xea, 0xe5, 0x76, 0x17, 0x1c, 0x74, 0x7f, 0xbc, 0x0d, 0x2c, 0x85, 0xf7, 0x66,
0x96, 0xe4, 0x8b, 0x75, 0x3f, 0x4b, 0xd9, 0x38, 0xaf, 0x7c, 0xda, 0x0b, 0x83, 0x2d, 0x31,
0x32, 0xa2, 0xf5, 0x1d, 0x59, 0x41, 0x45, 0xbf, 0x3c, 0x1f, 0xf8, 0xf9, 0x8a, 0xd0, 0x16,
0x25, 0x69, 0x12, 0x99, 0x9d, 0x21, 0x95, 0xab, 0x01, 0xa6, 0xd7, 0xb5, 0xc0, 0x7d, 0xff,
0x58, 0x0e, 0x3a, 0x92, 0xd1, 0x55, 0xe3, 0x08, 0x9f, 0xd6, 0x3e, 0x52, 0x8e, 0xfa, 0xa3,
0xc7, 0x02, 0xcd, 0xdf, 0x8f, 0x64, 0x19, 0x8c, 0xf3, 0xa7, 0x0c, 0x5e, 0x0a, 0x6a, 0x09,
0xf0, 0x93, 0x5b, 0x42, 0xc2, 0x06, 0x23, 0xec, 0x71, 0xad, 0xb4, 0xcb, 0xbb, 0x70, 0x28,
0xd5, 0x1a, 0x5c, 0x33, 0x68, 0x5a, 0x00, 0x44, 0x90, 0xa5, 0xc4, 0x26, 0x3d, 0x2b, 0xf2,
0x54,
},
{
0x96, 0xad, 0xda, 0x1f, 0xed, 0x33, 0xe1, 0x81, 0x69, 0x08, 0x0d, 0x0a, 0xdb, 0x35, 0x77,
0x9a, 0x64, 0xd1, 0xfc, 0x78, 0xaa, 0x1b, 0xd0, 0x67, 0xa0, 0xdd, 0xfa, 0x6c, 0x63, 0x71,
0x05, 0x84, 0x17, 0x6a, 0x89, 0x4f, 0x66, 0x7f, 0xc6, 0x50, 0x55, 0x92, 0x6f, 0xbd, 0xe7,
0xd2, 0x40, 0x72, 0x8d, 0xbb, 0xec, 0x06, 0x42, 0x8a, 0xe4, 0x88, 0x9d, 0x7e, 0x7a, 0x82,
0x27, 0x13, 0x41, 0x1a, 0xaf, 0xc8, 0xa4, 0x76, 0xb4, 0xc2, 0xfe, 0x6d, 0x1c, 0xd9, 0x61,
0x30, 0xb3, 0x7c, 0xea, 0xf7, 0x29, 0x0f, 0xf2, 0x3b, 0x51, 0xc1, 0xde, 0x5f, 0xe5, 0x2a,
0x2f, 0x99, 0x0b, 0x5d, 0xa3, 0x2b, 0x4a, 0xab, 0x95, 0xa5, 0xd3, 0x58, 0x56, 0xee, 0x28,
0x31, 0x00, 0xcc, 0x15, 0x46, 0xca, 0xe6, 0x86, 0x38, 0x3c, 0x65, 0xf5, 0xe3, 0x9f, 0xd6,
0x5b, 0x09, 0x49, 0x83, 0x70, 0x2d, 0x53, 0xa9, 0x7d, 0xe2, 0xc4, 0xac, 0x8e, 0x5e, 0xb8,
0x25, 0xf4, 0xb9, 0x57, 0xf3, 0xf1, 0x68, 0x47, 0xb2, 0xa2, 0x59, 0x20, 0xce, 0x34, 0x79,
0x5c, 0x90, 0x0e, 0x1e, 0xbe, 0xd5, 0x22, 0x23, 0xb1, 0xc9, 0x18, 0x62, 0x16, 0x2e, 0x91,
0x3e, 0x07, 0x8f, 0xd8, 0x3f, 0x93, 0x3d, 0xd4, 0x9b, 0xdf, 0x85, 0x21, 0xfb, 0x11, 0x74,
0x97, 0xc7, 0xd7, 0xdc, 0x4c, 0x19, 0x45, 0x98, 0xe9, 0x43, 0x02, 0x4b, 0xbc, 0xc3, 0x04,
0x9c, 0x6b, 0xf0, 0x75, 0x52, 0xa7, 0x26, 0xf6, 0xc5, 0xba, 0xcf, 0xb0, 0xb7, 0xae, 0x5a,
0xa1, 0xbf, 0x03, 0x8b, 0x80, 0x12, 0x6e, 0x0c, 0xeb, 0xf9, 0xc0, 0x44, 0x24, 0xef, 0x10,
0xf8, 0xa8, 0x8c, 0xe8, 0x7b, 0xff, 0x9e, 0x2c, 0xcd, 0x60, 0x36, 0x87, 0xb5, 0x94, 0xa6,
0x54, 0x73, 0x3a, 0x14, 0x4e, 0x01, 0x1d, 0xb6, 0xfd, 0x37, 0x48, 0x4d, 0x39, 0xcb, 0xe0,
0x32,
},
}};
constexpr uint8_t kAnswers[7][6] = {
{0xa8, 0x77, 0xa6, 0xe0, 0xf7, 0x43}, {0x5a, 0x35, 0x85, 0xe2, 0x72, 0x97},
{0x8f, 0xb7, 0x1a, 0x62, 0x87, 0x38}, {0x0d, 0x67, 0xc7, 0xbe, 0x4f, 0x3e},
{0x20, 0x76, 0x37, 0x8f, 0x68, 0xb7}, {0xa9, 0x26, 0x3f, 0x2b, 0x10, 0xe3},
{0x30, 0x7e, 0x90, 0x0e, 0x85, 0x0a},
};
using Rand = std::array<uint8_t, 10>;
using Key = std::array<uint8_t, 6>;
Key KeyFor(const Rand& rand, size_t idx) {
const uint8_t* ans = kAnswers[idx];
Rand t{};
for (size_t i = 0; i < t.size(); ++i) t[i] = kKeygenSbox[rand[i]];
return {
static_cast<uint8_t>((std::rotr<uint8_t>(ans[0] ^ t[5], t[2]) - t[9]) ^ t[4]),
static_cast<uint8_t>((std::rotr<uint8_t>(ans[1] ^ t[1], t[0]) - t[5]) ^ t[7]),
static_cast<uint8_t>((std::rotr<uint8_t>(ans[2] ^ t[6], t[8]) - t[2]) ^ t[0]),
static_cast<uint8_t>((std::rotr<uint8_t>(ans[3] ^ t[4], t[7]) - t[3]) ^ t[2]),
static_cast<uint8_t>((std::rotr<uint8_t>(ans[4] ^ t[1], t[6]) - t[3]) ^ t[4]),
static_cast<uint8_t>((std::rotr<uint8_t>(ans[5] ^ t[7], t[8]) - t[5]) ^ t[9]),
};
}
} // namespace Detail
std::optional<Tables> FromKey(const std::array<uint8_t, 16>& keyData) {
using namespace Detail;
Rand rand{};
std::copy_n(keyData.begin(), rand.size(), rand.rbegin());
Key key{};
std::copy_n(keyData.begin() + rand.size(), key.size(), key.rbegin());
// The key was derived from "rand" and an index the extension is not told.
for (size_t idx = 0; idx < 7; ++idx) {
if (KeyFor(rand, idx) != key) continue;
const SBox& a = kSboxes[idx];
const SBox& b = kSboxes[idx + 1];
Tables t;
t.ft = {static_cast<uint8_t>(a[key[4]] ^ b[rand[3]]), static_cast<uint8_t>(a[key[2]] ^ b[rand[5]]),
static_cast<uint8_t>(a[key[5]] ^ b[rand[7]]), static_cast<uint8_t>(a[key[0]] ^ b[rand[2]]),
static_cast<uint8_t>(a[key[1]] ^ b[rand[4]]), static_cast<uint8_t>(a[key[3]] ^ b[rand[9]]),
static_cast<uint8_t>(a[rand[0]] ^ b[rand[6]]), static_cast<uint8_t>(a[rand[1]] ^ b[rand[8]])};
t.sb = {static_cast<uint8_t>(a[key[0]] ^ b[rand[1]]), static_cast<uint8_t>(a[key[5]] ^ b[rand[4]]),
static_cast<uint8_t>(a[key[3]] ^ b[rand[0]]), static_cast<uint8_t>(a[key[2]] ^ b[rand[9]]),
static_cast<uint8_t>(a[key[4]] ^ b[rand[7]]), static_cast<uint8_t>(a[key[1]] ^ b[rand[8]]),
static_cast<uint8_t>(a[rand[3]] ^ b[rand[5]]), static_cast<uint8_t>(a[rand[2]] ^ b[rand[6]])};
return t;
}
return std::nullopt;
}
void Tables::Encrypt(uint8_t* data, uint32_t address, size_t length) const {
for (size_t i = 0; i < length; ++i, ++address) {
data[i] = static_cast<uint8_t>((data[i] - ft[address % 8]) ^ sb[address % 8]);
}
}
void Tables::Decrypt(uint8_t* data, uint32_t address, size_t length) const {
for (size_t i = 0; i < length; ++i, ++address) {
data[i] = static_cast<uint8_t>((data[i] ^ sb[address % 8]) + ft[address % 8]);
}
}
} // namespace Wiimote::ExtCrypto
+146
View File
@@ -0,0 +1,146 @@
#include "wiimote_extension.h"
#include <algorithm>
namespace Wiimote {
namespace ExtensionDetail {
constexpr uint8_t kReportStatus = 0x20;
constexpr uint8_t kReportReadReply = 0x21;
constexpr uint8_t kReportWriteMemory = 0x16;
constexpr uint8_t kReportReadMemory = 0x17;
constexpr uint32_t kExtensionRegisters = 0xA40000;
constexpr uint8_t kRegisterCalibration = 0x20;
constexpr uint8_t kRegisterKey = 0x40;
constexpr uint8_t kRegisterEncryption = 0xF0;
constexpr uint8_t kRegisterIdentifier = 0xFA;
constexpr uint8_t kEncryptionOn = 0xAA;
constexpr uint8_t kEncryptionOff = 0x55;
struct ExtensionSpan {
size_t offset;
size_t length;
};
// Where the extension bytes sit in each data report.
std::optional<ExtensionSpan> SpanFor(uint8_t id) {
switch (id) {
case 0x32: return ExtensionSpan{3, 8};
case 0x34: return ExtensionSpan{3, 19};
case 0x35: return ExtensionSpan{6, 16};
case 0x36: return ExtensionSpan{13, 9};
case 0x37: return ExtensionSpan{16, 6};
case 0x3D: return ExtensionSpan{1, 21};
default: return std::nullopt;
}
}
bool InExtensionSpace(uint32_t address) {
return (address & 0xFF0000) == kExtensionRegisters;
}
} // namespace ExtensionDetail
void ExtensionState::Reset() {
m_type = Type::Unknown;
m_format = 0;
m_encrypted = false;
m_tables.reset();
m_haveCalibration = false;
}
void ExtensionState::ObserveOutput(const uint8_t* report, size_t size) {
using namespace ExtensionDetail;
if (size < 6) return;
const bool registerSpace = (report[1] & 0x04) != 0;
const uint32_t address = (report[2] << 16) | (report[3] << 8) | report[4];
if (report[0] == kReportReadMemory) {
m_readAddress = registerSpace ? address : 0;
return;
}
if (!registerSpace || report[0] != kReportWriteMemory || !InExtensionSpace(address)) return;
const size_t length = std::min<size_t>(report[5], size - 6);
bool keyChanged = false;
for (size_t i = 0; i < length; ++i) {
const size_t reg = (address & 0xFF) + i;
const uint8_t value = report[6 + i];
if (reg >= kRegisterKey && reg < kRegisterKey + m_key.size()) {
m_key[reg - kRegisterKey] = value;
keyChanged = true;
} else if (reg == kRegisterEncryption) {
if (value == kEncryptionOn) m_encrypted = true;
if (value == kEncryptionOff) Reset();
}
}
if (keyChanged) {
m_tables = ExtCrypto::FromKey(m_key);
}
}
void ExtensionState::ObserveInput(const uint8_t* report, size_t size) {
using namespace ExtensionDetail;
if (size >= 4 && report[0] == kReportStatus) {
if ((report[3] & 0x02) == 0) Reset();
return;
}
if (size < 7 || report[0] != kReportReadReply || (report[3] & 0x0F) != 0 || !InExtensionSpace(m_readAddress)) {
return;
}
const size_t length = std::min<size_t>((report[3] >> 4) + 1, size - 6);
const uint32_t first = ((report[4] << 8) | report[5]) & 0xFF;
for (size_t i = 0; i < length; ++i) {
const uint32_t reg = first + static_cast<uint32_t>(i);
uint8_t value = report[6 + i];
if (reg >= kRegisterIdentifier) {
// The identifier is read before the game turns encryption on.
if (reg == 0xFE) m_format = value;
if (reg == 0xFF) m_type = value == 0x00 ? Type::Nunchuk : value == 0x01 ? Type::Classic : Type::Unknown;
} else if (reg >= kRegisterCalibration && reg < kRegisterCalibration + m_calibration.size()) {
if (m_encrypted) {
if (!m_tables) continue;
m_tables->Decrypt(&value, reg, 1);
}
m_calibration[reg - kRegisterCalibration] = value;
m_haveCalibration = true;
}
}
}
uint8_t ExtensionState::Calibration(size_t index, uint8_t fallback) const {
return m_haveCalibration ? m_calibration[index] : fallback;
}
void ExtensionState::Neutralize(std::vector<uint8_t>& report) const {
using namespace ExtensionDetail;
if (report.empty() || m_type == Type::Unknown || (m_encrypted && !m_tables)) return;
const auto span = SpanFor(report[0]);
if (!span || report.size() < span->offset + 6) return;
uint8_t* data = report.data() + span->offset;
std::array<uint8_t, 6> plain{};
std::copy_n(data, plain.size(), plain.begin());
if (m_encrypted) m_tables->Decrypt(plain.data(), 0, plain.size());
if (m_type == Type::Nunchuk) {
plain[0] = Calibration(10, 0x80);
plain[1] = Calibration(13, 0x80);
plain[5] |= 0x03; // C and Z are active low
} else if (m_format == 0x01) {
const uint8_t lx = Calibration(2, 0x80) >> 2;
const uint8_t ly = Calibration(5, 0x80) >> 2;
const uint8_t rx = Calibration(8, 0x80) >> 3;
const uint8_t ry = Calibration(11, 0x80) >> 3;
plain[0] = static_cast<uint8_t>(((rx >> 3) & 0x03) << 6 | (lx & 0x3F));
plain[1] = static_cast<uint8_t>(((rx >> 1) & 0x03) << 6 | (ly & 0x3F));
plain[2] = static_cast<uint8_t>((rx & 0x01) << 7 | (ry & 0x1F));
plain[3] = 0x00;
plain[4] = 0xFF; // buttons are active low
plain[5] = 0xFF;
} else {
return;
}
if (m_encrypted) m_tables->Encrypt(plain.data(), 0, plain.size());
std::copy(plain.begin(), plain.end(), data);
}
} // namespace Wiimote
+828
View File
@@ -0,0 +1,828 @@
#include "wiimote_host.h"
#include "input_bindings.h"
#include "runtime_config.h"
#include "runtime_log.h"
#include "wiimote_extension.h"
#include "wiimote_platform.h"
#include <SDL3/SDL_hidapi.h>
#include <SDL3/SDL_hints.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstring>
#include <deque>
#include <map>
#include <memory>
#include <mutex>
#include <set>
#include <string>
#include <thread>
#include <vector>
extern "C" bool PAD_HLE_RumbleEnabled();
namespace Wiimote {
namespace HostDetail {
using Clock = std::chrono::steady_clock;
using Report = std::vector<uint8_t>;
constexpr uint16_t kNintendoVendor = 0x057E;
constexpr uint16_t kProductRemote = 0x0306;
constexpr uint16_t kProductRemotePlus = 0x0330;
constexpr auto kScanInterval = std::chrono::milliseconds(500);
constexpr auto kEnumerateInterval = std::chrono::seconds(1);
constexpr auto kProbeCooldown = std::chrono::seconds(3);
constexpr auto kKeepAlive = std::chrono::seconds(1);
constexpr auto kBlinkInterval = std::chrono::milliseconds(500);
constexpr auto kPairingWindow = std::chrono::seconds(60);
constexpr auto kGuestSearchTail = std::chrono::seconds(10);
constexpr auto kAutoPairPause = std::chrono::seconds(4);
constexpr int kReadTimeoutMs = 200;
constexpr size_t kInputQueueLimit = 32;
constexpr uint16_t kButtonMask = 0x9F1F;
constexpr uint8_t kReportSpeakerData = 0x18;
enum class Phase : uint8_t {
Waiting, // connected to the PC, offered to the game
Linked, // the game's HID channels are open
Asleep, // the game let it go and it could not be switched off
PoweringOff,
};
// Input report sizes including the id: Windows always reads the longest report.
size_t ReportSize(uint8_t id, size_t read) {
size_t size = 0;
switch (id) {
case 0x20: size = 7; break;
case 0x21: size = 22; break;
case 0x22: size = 5; break;
case 0x30: size = 3; break;
case 0x31: size = 6; break;
case 0x32: size = 11; break;
case 0x33: size = 18; break;
case 0x34: case 0x35: case 0x36: case 0x37: case 0x3D: case 0x3E: case 0x3F: size = 22; break;
default: return 0;
}
return size <= read ? size : 0;
}
uint16_t Buttons(const Report& report) {
if (report.size() < 3 || report[0] < 0x20 || report[0] == 0x3D) return 0;
return static_cast<uint16_t>(((report[1] << 8) | report[2]) & kButtonMask);
}
// The game's own battery bars (WPAD's status report thresholds).
int BatteryBars(uint8_t level) {
if (level < 0x03) return 0;
if (level < 0x33) return 1;
if (level < 0x44) return 2;
if (level < 0x55) return 3;
return 4;
}
struct Remote {
std::string path;
std::string address;
std::string name;
bool plus = false;
SDL_hid_device* device = nullptr;
std::thread reader;
std::thread writer;
std::atomic<bool> alive{true};
std::atomic<Phase> phase{Phase::Waiting};
std::atomic<bool> wake{true};
std::atomic<int> battery{-1};
std::atomic<int> extension{-1}; // -1 none, else the extension id byte
std::atomic<bool> extensionAttached{false};
// Rumble bit of the game's last report, before the vibration setting.
std::atomic<bool> gameRumble{false};
std::mutex writeMutex;
std::condition_variable writeCv;
std::deque<Report> writes;
std::mutex inputMutex;
std::deque<Report> input;
// Emulation thread only.
ExtensionState extensionState;
};
class Manager final : public Bluetooth::RemoteHost {
public:
void Start();
void Stop();
bool IsAttached(size_t slot) const override;
bool ConsumeWake(size_t slot) override;
bool PopInputReport(size_t slot, std::vector<uint8_t>& report) override;
void SendOutputReport(size_t slot, const uint8_t* report, size_t size) override;
void OnLinkStateChanged(size_t slot, Bluetooth::LinkState state) override;
void OnGuestDisconnect(size_t slot) override;
void OnGuestInquiry(bool active) override;
const char* DeviceName(size_t slot) const override;
SlotInfo Slot(size_t slot);
PairingInfo Pairing();
void StartPairing();
void Disconnect(size_t slot);
size_t Waiting();
private:
std::shared_ptr<Remote> At(size_t slot) const;
void ScanLoop();
void DiscoveryLoop();
void Enumerate();
void Reap();
void AssignWaiting();
void Open(const SDL_hid_device_info& info);
void SetPhase(const std::shared_ptr<Remote>& remote, Phase phase);
void PowerOff(const std::shared_ptr<Remote>& remote);
static void ReadLoop(std::shared_ptr<Remote> remote);
static void WriteLoop(std::shared_ptr<Remote> remote);
static void Queue(Remote& remote, Report report);
mutable std::mutex m_mutex;
std::array<std::shared_ptr<Remote>, Bluetooth::kRemoteSlots> m_slots;
std::array<Bluetooth::LinkState, Bluetooth::kRemoteSlots> m_links{};
std::vector<std::shared_ptr<Remote>> m_waiting;
std::map<std::string, size_t> m_lastSlot;
std::map<std::string, Clock::time_point> m_cooldown;
std::set<std::string> m_refusedLogged;
// Remotes the OS knows but that were off when last seen, by path.
std::map<std::string, std::string> m_dormant;
Clock::time_point m_lastEnumerate{};
uint32_t m_deviceChanges = 0;
std::atomic<bool> m_running{false};
std::thread m_scanner;
std::mutex m_scanMutex;
std::condition_variable m_scanCv;
std::thread m_discovery;
std::mutex m_discoveryMutex;
std::condition_variable m_discoveryCv;
std::atomic<bool> m_cancelDiscovery{false};
std::atomic<bool> m_searching{false};
std::atomic<bool> m_guestSearching{false};
Clock::time_point m_guestSearchUntil{};
Clock::time_point m_pairingUntil{};
std::string m_pairingStatus;
};
// Never destroyed: its threads may outlive static destruction at exit.
Manager& TheManager() {
static Manager* manager = new Manager();
return *manager;
}
std::shared_ptr<Remote> Manager::At(size_t slot) const {
std::lock_guard<std::mutex> lock(m_mutex);
return slot < m_slots.size() ? m_slots[slot] : nullptr;
}
bool Manager::IsAttached(size_t slot) const {
const auto remote = At(slot);
if (!remote || !remote->alive) return false;
const Phase phase = remote->phase;
return phase == Phase::Waiting || phase == Phase::Linked;
}
bool Manager::ConsumeWake(size_t slot) {
const auto remote = At(slot);
return remote && remote->phase == Phase::Waiting && remote->wake.exchange(false);
}
bool Manager::PopInputReport(size_t slot, std::vector<uint8_t>& report) {
const auto remote = At(slot);
if (!remote) return false;
{
std::lock_guard<std::mutex> lock(remote->inputMutex);
if (remote->input.empty()) return false;
report = std::move(remote->input.front());
remote->input.pop_front();
}
remote->extensionState.ObserveInput(report.data(), report.size());
if (InputBindings::InputBlocked()) {
// The overlay owns input: the game sees the controller at rest.
if (Buttons(report) != 0) {
report[1] &= static_cast<uint8_t>(~(kButtonMask >> 8));
report[2] &= static_cast<uint8_t>(~kButtonMask);
}
remote->extensionState.Neutralize(report);
}
return true;
}
void Manager::Queue(Remote& remote, Report report) {
{
std::lock_guard<std::mutex> lock(remote.writeMutex);
remote.writes.push_back(std::move(report));
}
remote.writeCv.notify_one();
}
void Manager::SendOutputReport(size_t slot, const uint8_t* report, size_t size) {
const auto remote = At(slot);
if (!remote || remote->phase != Phase::Linked || size == 0) return;
remote->extensionState.ObserveOutput(report, size);
const bool rumble = size >= 2 && (report[1] & 0x01) != 0;
const bool rumbleChanged = size >= 2 && remote->gameRumble.exchange(rumble) != rumble;
// PC Bluetooth cannot feed the speaker smoothly enough to sound right, but
// WPAD may carry a rumble change only on speaker data.
if (report[0] == kReportSpeakerData) {
if (rumbleChanged) Queue(*remote, {0x10, static_cast<uint8_t>(rumble ? 0x01 : 0x00)});
return;
}
Queue(*remote, Report(report, report + size));
}
void Manager::OnLinkStateChanged(size_t slot, Bluetooth::LinkState state) {
std::shared_ptr<Remote> remote;
{
std::lock_guard<std::mutex> lock(m_mutex);
m_links[slot] = state;
remote = m_slots[slot];
}
if (!remote) return;
if (state == Bluetooth::LinkState::Ready) {
if (remote->phase == Phase::PoweringOff) return;
{
std::lock_guard<std::mutex> lock(remote->inputMutex);
remote->input.clear();
}
SetPhase(remote, Phase::Linked);
RT_LOG(RT_TAG_HLE) << "Wii Remote " << (slot + 1) << " connected to the game" << std::endl;
} else if (state == Bluetooth::LinkState::Down && remote->phase == Phase::Linked) {
// Lost without the game asking (e.g. the controller was reset): offer it again.
SetPhase(remote, Phase::Waiting);
remote->wake = true;
}
}
void Manager::OnGuestDisconnect(size_t slot) {
const auto remote = At(slot);
if (!remote) return;
RT_LOG(RT_TAG_HLE) << "Wii Remote " << (slot + 1) << " disconnected by the game" << std::endl;
PowerOff(remote);
}
void Manager::PowerOff(const std::shared_ptr<Remote>& remote) {
remote->wake = false;
if (Platform::Query().powerOff && !remote->address.empty()) {
SetPhase(remote, Phase::PoweringOff);
std::thread([remote] {
const bool requested = Platform::PowerOff(remote->address);
// The HID device goes away once the remote is off; if it does not,
// keep it asleep so a button press brings it back.
const auto deadline = Clock::now() + std::chrono::seconds(5);
while (requested && remote->alive && Clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
if (remote->alive && remote->phase == Phase::PoweringOff) {
remote->phase = Phase::Asleep;
remote->writeCv.notify_one();
}
}).detach();
} else {
SetPhase(remote, Phase::Asleep);
}
}
void Manager::SetPhase(const std::shared_ptr<Remote>& remote, Phase phase) {
remote->phase = phase;
remote->gameRumble = false;
if (phase != Phase::Linked) {
std::lock_guard<std::mutex> lock(remote->writeMutex);
remote->writes.clear();
}
remote->writeCv.notify_one();
}
void Manager::OnGuestInquiry(bool active) {
if (active) {
std::lock_guard<std::mutex> lock(m_discoveryMutex);
m_guestSearching = true;
} else {
std::lock_guard<std::mutex> lock(m_discoveryMutex);
m_guestSearching = false;
m_guestSearchUntil = Clock::now() + kGuestSearchTail;
}
m_discoveryCv.notify_all();
}
const char* Manager::DeviceName(size_t slot) const {
const auto remote = At(slot);
return remote && remote->plus ? "Nintendo RVL-CNT-01-TR" : "Nintendo RVL-CNT-01";
}
void Manager::Start() {
if (m_running.exchange(true)) return;
if (SDL_hid_init() != 0) {
RT_LOG(RT_TAG_HLE) << "Wii Remotes: HID access unavailable: " << SDL_GetError() << std::endl;
m_running = false;
return;
}
m_scanner = std::thread([this] { ScanLoop(); });
if (Platform::Query().pairing) {
m_discovery = std::thread([this] { DiscoveryLoop(); });
}
const std::string notice = Platform::Notice();
if (!notice.empty()) {
RT_LOG(RT_TAG_HLE) << "Wii Remotes: " << notice << std::endl;
}
}
void Manager::Stop() {
if (!m_running.exchange(false)) return;
m_cancelDiscovery = true;
m_scanCv.notify_all();
m_discoveryCv.notify_all();
if (m_scanner.joinable()) m_scanner.join();
// An inquiry in progress cannot be interrupted; let it finish on its own.
if (m_discovery.joinable()) m_discovery.detach();
std::vector<std::shared_ptr<Remote>> remotes;
{
std::lock_guard<std::mutex> lock(m_mutex);
for (auto& remote : m_slots) {
if (remote) remotes.push_back(remote);
remote.reset();
}
remotes.insert(remotes.end(), m_waiting.begin(), m_waiting.end());
m_waiting.clear();
}
for (const auto& remote : remotes) {
remote->alive = false;
remote->writeCv.notify_all();
}
// A console switches its remotes off when it shuts down.
for (const auto& remote : remotes) {
if (remote->writer.joinable()) remote->writer.join();
if (remote->reader.joinable()) remote->reader.join();
if (remote->device != nullptr) {
const uint8_t rumbleOff[] = {0x10, 0x00};
const uint8_t ledsOff[] = {0x11, 0x00};
SDL_hid_write(remote->device, rumbleOff, sizeof(rumbleOff));
SDL_hid_write(remote->device, ledsOff, sizeof(ledsOff));
SDL_hid_close(remote->device);
remote->device = nullptr;
}
if (Platform::Query().powerOff && !remote->address.empty()) {
Platform::PowerOff(remote->address);
}
}
}
void Manager::ScanLoop() {
while (m_running) {
Reap();
Enumerate();
AssignWaiting();
std::unique_lock<std::mutex> lock(m_scanMutex);
m_scanCv.wait_for(lock, kScanInterval, [this] { return !m_running; });
}
}
void Manager::Reap() {
std::vector<std::shared_ptr<Remote>> dead;
{
std::lock_guard<std::mutex> lock(m_mutex);
for (auto& remote : m_slots) {
if (remote && !remote->alive) {
dead.push_back(remote);
remote.reset();
}
}
for (auto it = m_waiting.begin(); it != m_waiting.end();) {
if (!(*it)->alive) {
dead.push_back(*it);
it = m_waiting.erase(it);
} else {
++it;
}
}
}
for (const auto& remote : dead) {
remote->writeCv.notify_all();
if (remote->writer.joinable()) remote->writer.join();
if (remote->reader.joinable()) remote->reader.join();
if (remote->device != nullptr) SDL_hid_close(remote->device);
remote->device = nullptr;
RT_LOG(RT_TAG_HLE) << "Wii Remote " << (remote->address.empty() ? remote->path : remote->address)
<< " disconnected from the PC" << std::endl;
m_cooldown[remote->path] = Clock::now() + std::chrono::seconds(1);
if (!remote->address.empty()) m_dormant[remote->path] = remote->address;
}
}
void Manager::Enumerate() {
const Uint32 changes = SDL_hid_device_change_count();
const auto now = Clock::now();
bool fullSlots = false;
bool anyRemote = false;
{
std::lock_guard<std::mutex> lock(m_mutex);
fullSlots = std::all_of(m_slots.begin(), m_slots.end(), [](const auto& remote) { return remote != nullptr; });
anyRemote = !m_waiting.empty() ||
std::any_of(m_slots.begin(), m_slots.end(), [](const auto& remote) { return remote != nullptr; });
}
bool pairing = false;
{
std::lock_guard<std::mutex> lock(m_discoveryMutex);
pairing = m_guestSearching || now < m_guestSearchUntil || now < m_pairingUntil;
}
if (fullSlots) return;
if (changes == m_deviceChanges) {
if (now - m_lastEnumerate < kEnumerateInterval) return;
// Once a remote is connected, skip HID enumeration unless pairing or a known remote came back.
if (anyRemote && !pairing &&
std::none_of(m_dormant.begin(), m_dormant.end(),
[](const auto& entry) { return Platform::MaybeConnected(entry.second); })) {
m_lastEnumerate = now;
return;
}
}
m_deviceChanges = changes;
m_lastEnumerate = now;
std::set<std::string> open;
{
std::lock_guard<std::mutex> lock(m_mutex);
for (const auto& remote : m_slots) {
if (remote) open.insert(remote->path);
}
for (const auto& remote : m_waiting) open.insert(remote->path);
}
std::set<std::string> listed;
SDL_hid_device_info* list = SDL_hid_enumerate(kNintendoVendor, 0);
for (const SDL_hid_device_info* info = list; info != nullptr; info = info->next) {
if (info->path == nullptr || (info->product_id != kProductRemote && info->product_id != kProductRemotePlus)) {
continue;
}
listed.insert(info->path);
if (open.count(info->path) != 0) continue;
const auto cooldown = m_cooldown.find(info->path);
if (cooldown != m_cooldown.end() && now < cooldown->second) continue;
open.insert(info->path);
Open(*info);
}
SDL_hid_free_enumeration(list);
for (auto it = m_dormant.begin(); it != m_dormant.end();) {
it = listed.count(it->first) != 0 ? std::next(it) : m_dormant.erase(it);
}
}
void Manager::Open(const SDL_hid_device_info& info) {
const std::string path = info.path;
const std::string address = Platform::AddressFor(info);
const std::string name = Platform::NameFor(address, info);
// Wii U Pro Controllers share the Wii Remote Plus id; balance boards are not used by the game.
if (name.find("-UC") != std::string::npos || name.find("RVL-WBC") != std::string::npos) {
m_cooldown[path] = Clock::time_point::max();
return;
}
if (!address.empty() && !Platform::MaybeConnected(address)) {
// Known to the OS but switched off: check again soon, a button press
// reconnects it.
m_cooldown[path] = Clock::now() + kScanInterval;
m_dormant[path] = address;
return;
}
if (!Platform::Usable(info)) {
if (m_refusedLogged.insert(path).second) {
RT_LOG(RT_TAG_HLE) << "Wii Remote " << address << " is held by the system driver: " << Platform::Notice()
<< std::endl;
}
m_cooldown[path] = Clock::now() + std::chrono::seconds(10);
return;
}
SDL_hid_device* device = SDL_hid_open_path(path.c_str());
if (device == nullptr) {
m_cooldown[path] = Clock::now() + kProbeCooldown;
return;
}
// A remote the OS remembers but that is switched off still has a HID device;
// only a write tells.
const uint8_t status[] = {0x15, 0x00};
if (SDL_hid_write(device, status, sizeof(status)) < 0) {
SDL_hid_close(device);
m_cooldown[path] = Clock::now() + kProbeCooldown;
if (!address.empty()) m_dormant[path] = address;
return;
}
m_dormant.erase(path);
auto remote = std::make_shared<Remote>();
remote->path = path;
remote->address = address;
remote->name = name;
remote->plus = info.product_id == kProductRemotePlus;
remote->device = device;
remote->reader = std::thread(ReadLoop, remote);
remote->writer = std::thread(WriteLoop, remote);
RT_LOG(RT_TAG_HLE) << (remote->plus ? "Wii Remote Plus " : "Wii Remote ")
<< (address.empty() ? path : address) << " connected to the PC" << std::endl;
{
std::lock_guard<std::mutex> lock(m_mutex);
m_waiting.push_back(remote);
}
AssignWaiting();
}
void Manager::AssignWaiting() {
std::lock_guard<std::mutex> lock(m_mutex);
for (auto it = m_waiting.begin(); it != m_waiting.end();) {
const std::shared_ptr<Remote>& remote = *it;
const std::string key = remote->address.empty() ? remote->path : remote->address;
size_t slot = Bluetooth::kRemoteSlots;
const auto last = m_lastSlot.find(key);
// A slot is free once the game has seen its previous remote go.
const auto free = [this](size_t i) { return !m_slots[i] && m_links[i] == Bluetooth::LinkState::Down; };
if (last != m_lastSlot.end() && free(last->second)) {
slot = last->second;
} else {
for (size_t i = 0; i < m_slots.size(); ++i) {
if (free(i)) {
slot = i;
break;
}
}
}
if (slot == Bluetooth::kRemoteSlots) return;
m_slots[slot] = remote;
m_lastSlot[key] = slot;
it = m_waiting.erase(it);
}
}
void Manager::ReadLoop(std::shared_ptr<Remote> remote) {
std::array<uint8_t, 32> buffer{};
while (remote->alive) {
const int read = SDL_hid_read_timeout(remote->device, buffer.data(), buffer.size(), kReadTimeoutMs);
if (read < 0) {
remote->alive = false;
remote->writeCv.notify_all();
break;
}
if (read == 0) continue;
const size_t size = ReportSize(buffer[0], static_cast<size_t>(read));
if (size == 0) continue;
Report report(buffer.begin(), buffer.begin() + static_cast<std::ptrdiff_t>(size));
if (report[0] == 0x20) {
remote->battery = BatteryBars(report[6]);
const bool attached = (report[3] & 0x02) != 0;
remote->extensionAttached = attached;
if (!attached) remote->extension = -1;
} else if (report[0] == 0x21 && (report[3] & 0x0F) == 0 && report[4] == 0x00 && report[5] == 0xFA &&
(report[3] >> 4) >= 5) {
// The game reading the extension id at 0xA400FA (before encryption).
remote->extension = report[11];
}
const Phase phase = remote->phase;
if (phase == Phase::Linked) {
std::lock_guard<std::mutex> lock(remote->inputMutex);
if (remote->input.size() >= kInputQueueLimit) {
auto stale = std::find_if(remote->input.begin(), remote->input.end(),
[](const Report& r) { return r[0] >= 0x30; });
remote->input.erase(stale != remote->input.end() ? stale : remote->input.begin());
}
remote->input.push_back(std::move(report));
} else if (Buttons(report) != 0) {
if (phase == Phase::Asleep) {
remote->phase = Phase::Waiting;
remote->writeCv.notify_one();
}
if (phase == Phase::Asleep || phase == Phase::Waiting) {
remote->wake = true;
}
}
}
}
void Manager::WriteLoop(std::shared_ptr<Remote> remote) {
auto lastWrite = Clock::now();
auto lastBlink = Clock::time_point{};
bool blinkOn = false;
bool appliedRumble = false;
Phase lastPhase = Phase::Linked; // forces the quiet setup on the first pass
const auto write = [&](const Report& report) {
if (SDL_hid_write(remote->device, report.data(), report.size()) < 0) {
remote->alive = false;
return false;
}
lastWrite = Clock::now();
return true;
};
while (remote->alive) {
std::deque<Report> batch;
{
std::unique_lock<std::mutex> lock(remote->writeMutex);
remote->writeCv.wait_for(lock, std::chrono::milliseconds(100),
[&] { return !remote->alive || !remote->writes.empty(); });
batch.swap(remote->writes);
}
const bool rumbleAllowed = PAD_HLE_RumbleEnabled();
const Phase phase = remote->phase;
// Queued just before the game let go of the remote.
if (phase != Phase::Linked) batch.clear();
for (Report& report : batch) {
if (report.size() >= 2 && !rumbleAllowed) report[1] &= 0xFE;
if (!write(report)) break;
if (report.size() >= 2) appliedRumble = (report[1] & 0x01) != 0;
}
if (!remote->alive) break;
const auto now = Clock::now();
if (phase != lastPhase && phase != Phase::Linked && phase != Phase::PoweringOff) {
// Stop streaming and rumbling while the game does not own the remote.
if (!write({0x12, 0x00, 0x30}) || !write({0x11, 0x00})) break;
appliedRumble = false;
blinkOn = false;
lastBlink = now;
}
lastPhase = phase;
if (phase == Phase::Waiting && now - lastBlink >= kBlinkInterval) {
// Like a remote looking for its console.
blinkOn = !blinkOn;
lastBlink = now;
if (!write({0x11, static_cast<uint8_t>(blinkOn ? 0xF0 : 0x00)})) break;
} else if (phase == Phase::Linked) {
const bool wanted = remote->gameRumble && rumbleAllowed;
if (wanted != appliedRumble || now - lastWrite >= kKeepAlive) {
// Also how Windows notices a remote that went away.
if (!write({0x10, static_cast<uint8_t>(wanted ? 0x01 : 0x00)})) break;
appliedRumble = wanted;
}
} else if (phase == Phase::Asleep && now - lastWrite >= kKeepAlive) {
if (!write({0x10, 0x00})) break;
}
}
remote->writeCv.notify_all();
}
void Manager::DiscoveryLoop() {
while (m_running) {
bool search = false;
bool automatic = false;
{
std::unique_lock<std::mutex> lock(m_discoveryMutex);
const auto now = Clock::now();
bool anyRemote = false;
{
std::lock_guard<std::mutex> slotLock(m_mutex);
for (const auto& remote : m_slots) anyRemote |= remote != nullptr;
anyRemote |= !m_waiting.empty();
}
const bool requested = m_guestSearching || now < m_guestSearchUntil || now < m_pairingUntil;
automatic = !requested && !anyRemote && RuntimeConfigFile::WiiAutoPairEnabled();
search = requested || automatic;
if (!search) {
m_searching = false;
m_discoveryCv.wait_for(lock, std::chrono::seconds(1));
continue;
}
}
m_searching = true;
std::string status;
const int connected = Platform::DiscoverAndPair(m_cancelDiscovery, status);
{
std::lock_guard<std::mutex> lock(m_discoveryMutex);
if (!status.empty()) m_pairingStatus = status;
if (connected > 0) {
m_scanCv.notify_all();
}
}
if (automatic && connected == 0) {
std::unique_lock<std::mutex> lock(m_discoveryMutex);
m_discoveryCv.wait_for(lock, kAutoPairPause, [this] {
return !m_running || m_guestSearching || Clock::now() < m_pairingUntil;
});
}
}
m_searching = false;
}
SlotInfo Manager::Slot(size_t slot) {
SlotInfo info;
std::shared_ptr<Remote> remote;
{
std::lock_guard<std::mutex> lock(m_mutex);
if (slot >= m_slots.size()) return info;
remote = m_slots[slot];
info.link = m_links[slot];
}
if (!remote || !remote->alive || remote->phase == Phase::PoweringOff) {
info.link = Bluetooth::LinkState::Down;
return info;
}
info.attached = true;
info.asleep = remote->phase == Phase::Asleep;
info.model = remote->plus ? "Wii Remote Plus" : "Wii Remote";
if (remote->extensionAttached) {
switch (remote->extension.load()) {
case 0x00: info.extension = "Nunchuk"; break;
case 0x01: info.extension = "Classic Controller"; break;
case 0x03: info.extension = "Classic Controller Pro"; break;
default: info.extension = "Extension"; break;
}
}
info.battery = remote->battery;
return info;
}
PairingInfo Manager::Pairing() {
PairingInfo info;
info.supported = Platform::Query().pairing && m_running;
info.searching = m_searching;
std::lock_guard<std::mutex> lock(m_discoveryMutex);
info.status = m_pairingStatus;
return info;
}
void Manager::StartPairing() {
{
std::lock_guard<std::mutex> lock(m_discoveryMutex);
m_pairingUntil = Clock::now() + kPairingWindow;
m_pairingStatus = "Press the red SYNC button (or 1 and 2) on the Wii Remote.";
}
m_discoveryCv.notify_all();
}
void Manager::Disconnect(size_t slot) {
if (const auto remote = At(slot)) {
PowerOff(remote);
}
}
size_t Manager::Waiting() {
std::lock_guard<std::mutex> lock(m_mutex);
return m_waiting.size();
}
bool g_enabled = false;
} // namespace HostDetail
using namespace HostDetail;
void ConfigureSdlHints(bool enabled) {
g_enabled = enabled;
// The remotes belong to the game's Bluetooth stack: SDL must not open them
// as gamepads (its own Wii driver, or the Linux kernel driver's evdev nodes).
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_WII, enabled ? "0" : "1");
if (enabled) {
std::string ignored = "0x057e/0x0306,0x057e/0x0330";
if (const char* existing = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES); existing && *existing) {
ignored = std::string(existing) + "," + ignored;
}
SDL_SetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES, ignored.c_str());
}
}
void Initialize() {
if (g_enabled) {
TheManager().Start();
}
}
void Shutdown() {
TheManager().Stop();
}
Bluetooth::RemoteHost& Host() {
return TheManager();
}
SlotInfo Slot(size_t slot) {
return TheManager().Slot(slot);
}
PairingInfo Pairing() {
return TheManager().Pairing();
}
void StartPairing() {
TheManager().StartPairing();
}
void Disconnect(size_t slot) {
TheManager().Disconnect(slot);
}
std::string PlatformNotice() {
return g_enabled ? Platform::Notice() : std::string();
}
size_t WaitingRemotes() {
return TheManager().Waiting();
}
} // namespace Wiimote
@@ -0,0 +1,496 @@
#if !defined(_WIN32)
#include "wiimote_platform.h"
#include "runtime_log.h"
#include <SDL3/SDL_hidapi.h>
#include <algorithm>
#include <atomic>
#include <cctype>
#include <chrono>
#include <cstring>
#include <mutex>
#include <string>
#include <thread>
#include <type_traits>
#include <vector>
#if defined(__linux__)
#include <dlfcn.h>
#include <unistd.h>
#include <filesystem>
#endif
namespace Wiimote::Platform {
namespace Detail {
std::string Narrow(const wchar_t* text) {
std::string out;
if (text != nullptr) {
for (const wchar_t* c = text; *c != L'\0'; ++c) out += static_cast<char>(*c < 128 ? *c : '?');
}
return out;
}
std::string HexDigits(const std::string& text) {
std::string hex;
for (const char c : text) {
if (std::isxdigit(static_cast<unsigned char>(c))) hex += static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}
return hex;
}
std::mutex g_noticeMutex;
std::string g_notice;
void SetNotice(const std::string& notice) {
std::lock_guard<std::mutex> lock(g_noticeMutex);
g_notice = notice;
}
#if defined(__linux__)
// BlueZ over the system bus. libdbus-1 is loaded at run time so a system
// without it still runs (pairing then falls back to the desktop's tools).
extern "C" {
struct BzConnection;
struct BzMessage;
struct BzError {
const char* name;
const char* message;
unsigned int dummy1 : 1;
unsigned int dummy2 : 1;
unsigned int dummy3 : 1;
unsigned int dummy4 : 1;
unsigned int dummy5 : 1;
void* padding1;
};
// Mirrors DBusMessageIter.
struct BzIter {
void* dummy1;
void* dummy2;
uint32_t dummy3;
int dummy4;
int dummy5;
int dummy6;
int dummy7;
int dummy8;
int dummy9;
int dummy10;
int dummy11;
int pad1;
int pad2;
void* pad3;
};
}
constexpr int kBusSystem = 1;
constexpr int kTypeInvalid = 0;
constexpr int kTypeString = 's';
constexpr int kTypeObjectPath = 'o';
constexpr int kTypeBoolean = 'b';
constexpr int kTypeInt16 = 'n';
constexpr int kTypeArray = 'a';
constexpr int kTypeVariant = 'v';
constexpr int kTypeDictEntry = 'e';
constexpr int kShortTimeoutMs = 2000;
constexpr int kPairTimeoutMs = 30000;
constexpr auto kInquiryTime = std::chrono::milliseconds(3840);
struct Device {
std::string path;
std::string address;
std::string name;
bool paired = false;
bool connected = false;
bool trusted = false;
bool discovered = false; // has an RSSI: answered the current inquiry
};
class BlueZ {
public:
bool Load() {
if (m_loaded) return m_conn != nullptr;
m_loaded = true;
m_handle = dlopen("libdbus-1.so.3", RTLD_NOW | RTLD_LOCAL);
if (m_handle == nullptr) return false;
const bool ok = Bind(bus_get_private, "dbus_bus_get_private") &&
Bind(connection_set_exit_on_disconnect, "dbus_connection_set_exit_on_disconnect") &&
Bind(error_init, "dbus_error_init") && Bind(error_free, "dbus_error_free") &&
Bind(message_new_method_call, "dbus_message_new_method_call") &&
Bind(message_append_args, "dbus_message_append_args") &&
Bind(send_with_reply_and_block, "dbus_connection_send_with_reply_and_block") &&
Bind(message_unref, "dbus_message_unref") &&
Bind(message_iter_init, "dbus_message_iter_init") &&
Bind(message_iter_init_append, "dbus_message_iter_init_append") &&
Bind(message_iter_open_container, "dbus_message_iter_open_container") &&
Bind(message_iter_close_container, "dbus_message_iter_close_container") &&
Bind(message_iter_append_basic, "dbus_message_iter_append_basic") &&
Bind(message_iter_next, "dbus_message_iter_next") &&
Bind(message_iter_recurse, "dbus_message_iter_recurse") &&
Bind(message_iter_get_arg_type, "dbus_message_iter_get_arg_type") &&
Bind(message_iter_get_basic, "dbus_message_iter_get_basic");
if (!ok) return false;
BzError error;
error_init(&error);
m_conn = bus_get_private(kBusSystem, &error);
error_free(&error);
if (m_conn != nullptr) connection_set_exit_on_disconnect(m_conn, 0);
return m_conn != nullptr;
}
// org.freedesktop.DBus.ObjectManager.GetManagedObjects on org.bluez.
bool Objects(std::string& adapter, std::vector<Device>& devices) {
BzMessage* reply = Call("/", "org.freedesktop.DBus.ObjectManager", "GetManagedObjects", kShortTimeoutMs);
if (reply == nullptr) return false;
BzIter root;
BzIter objects;
if (message_iter_init(reply, &root) && message_iter_get_arg_type(&root) == kTypeArray) {
message_iter_recurse(&root, &objects);
while (message_iter_get_arg_type(&objects) == kTypeDictEntry) {
BzIter entry;
message_iter_recurse(&objects, &entry);
const char* path = nullptr;
message_iter_get_basic(&entry, &path);
message_iter_next(&entry);
ReadObject(path != nullptr ? path : "", entry, adapter, devices);
message_iter_next(&objects);
}
}
message_unref(reply);
return true;
}
bool Simple(const std::string& path, const char* interface, const char* method, int timeoutMs,
std::string* error = nullptr) {
BzMessage* reply = Call(path.c_str(), interface, method, timeoutMs, nullptr, error);
if (reply == nullptr) return false;
message_unref(reply);
return true;
}
bool RemoveDevice(const std::string& adapter, const std::string& device) {
const char* path = device.c_str();
BzMessage* reply = Call(adapter.c_str(), "org.bluez.Adapter1", "RemoveDevice", kShortTimeoutMs,
[&](BzMessage* message) {
return message_append_args(message, kTypeObjectPath, &path, kTypeInvalid) != 0;
});
if (reply == nullptr) return false;
message_unref(reply);
return true;
}
bool SetTrusted(const std::string& device) {
BzMessage* reply = Call(device.c_str(), "org.freedesktop.DBus.Properties", "Set", kShortTimeoutMs,
[&](BzMessage* message) {
const char* interface = "org.bluez.Device1";
const char* property = "Trusted";
if (!message_append_args(message, kTypeString, &interface, kTypeString, &property,
kTypeInvalid)) {
return false;
}
BzIter args;
BzIter variant;
message_iter_init_append(message, &args);
if (!message_iter_open_container(&args, kTypeVariant, "b", &variant)) return false;
const uint32_t value = 1;
message_iter_append_basic(&variant, kTypeBoolean, &value);
return message_iter_close_container(&args, &variant) != 0;
});
if (reply == nullptr) return false;
message_unref(reply);
return true;
}
private:
template <typename Fn>
bool Bind(Fn& fn, const char* symbol) {
fn = reinterpret_cast<Fn>(dlsym(m_handle, symbol));
return fn != nullptr;
}
template <typename Append = std::nullptr_t>
BzMessage* Call(const char* path, const char* interface, const char* method, int timeoutMs,
Append append = nullptr, std::string* errorText = nullptr) {
BzMessage* message = message_new_method_call("org.bluez", path, interface, method);
if (message == nullptr) return nullptr;
if constexpr (!std::is_same_v<Append, std::nullptr_t>) {
if (!append(message)) {
message_unref(message);
return nullptr;
}
}
BzError error;
error_init(&error);
BzMessage* reply = send_with_reply_and_block(m_conn, message, timeoutMs, &error);
message_unref(message);
if (reply == nullptr && errorText != nullptr && error.name != nullptr) {
*errorText = error.name;
}
error_free(&error);
return reply;
}
// One object's a{sa{sv}} interface map.
void ReadObject(const std::string& path, BzIter& interfaces, std::string& adapter, std::vector<Device>& devices) {
if (message_iter_get_arg_type(&interfaces) != kTypeArray) return;
BzIter list;
message_iter_recurse(&interfaces, &list);
while (message_iter_get_arg_type(&list) == kTypeDictEntry) {
BzIter entry;
message_iter_recurse(&list, &entry);
const char* interface = nullptr;
message_iter_get_basic(&entry, &interface);
message_iter_next(&entry);
const std::string name = interface != nullptr ? interface : "";
if (name == "org.bluez.Adapter1" && adapter.empty()) {
adapter = path;
} else if (name == "org.bluez.Device1") {
Device device;
device.path = path;
ReadDevice(entry, device);
devices.push_back(device);
}
message_iter_next(&list);
}
}
void ReadDevice(BzIter& properties, Device& device) {
if (message_iter_get_arg_type(&properties) != kTypeArray) return;
BzIter list;
message_iter_recurse(&properties, &list);
while (message_iter_get_arg_type(&list) == kTypeDictEntry) {
BzIter entry;
message_iter_recurse(&list, &entry);
const char* key = nullptr;
message_iter_get_basic(&entry, &key);
message_iter_next(&entry);
BzIter value;
message_iter_recurse(&entry, &value);
const std::string name = key != nullptr ? key : "";
const int type = message_iter_get_arg_type(&value);
if (type == kTypeString && (name == "Address" || name == "Name")) {
const char* text = nullptr;
message_iter_get_basic(&value, &text);
(name == "Address" ? device.address : device.name) = text != nullptr ? text : "";
} else if (type == kTypeBoolean) {
uint32_t flag = 0;
message_iter_get_basic(&value, &flag);
if (name == "Paired") device.paired = flag != 0;
if (name == "Connected") device.connected = flag != 0;
if (name == "Trusted") device.trusted = flag != 0;
} else if (type == kTypeInt16 && name == "RSSI") {
device.discovered = true;
}
message_iter_next(&list);
}
}
bool m_loaded = false;
void* m_handle = nullptr;
BzConnection* m_conn = nullptr;
BzConnection* (*bus_get_private)(int, BzError*) = nullptr;
void (*connection_set_exit_on_disconnect)(BzConnection*, unsigned int) = nullptr;
void (*error_init)(BzError*) = nullptr;
void (*error_free)(BzError*) = nullptr;
BzMessage* (*message_new_method_call)(const char*, const char*, const char*, const char*) = nullptr;
unsigned int (*message_append_args)(BzMessage*, int, ...) = nullptr;
BzMessage* (*send_with_reply_and_block)(BzConnection*, BzMessage*, int, BzError*) = nullptr;
void (*message_unref)(BzMessage*) = nullptr;
unsigned int (*message_iter_init)(BzMessage*, BzIter*) = nullptr;
void (*message_iter_init_append)(BzMessage*, BzIter*) = nullptr;
unsigned int (*message_iter_open_container)(BzIter*, int, const char*, BzIter*) = nullptr;
unsigned int (*message_iter_close_container)(BzIter*, BzIter*) = nullptr;
unsigned int (*message_iter_append_basic)(BzIter*, int, const void*) = nullptr;
unsigned int (*message_iter_next)(BzIter*) = nullptr;
void (*message_iter_recurse)(BzIter*, BzIter*) = nullptr;
int (*message_iter_get_arg_type)(BzIter*) = nullptr;
void (*message_iter_get_basic)(BzIter*, void*) = nullptr;
};
// Every call happens on the discovery thread or the power-off worker.
std::timed_mutex g_busMutex;
BlueZ* Bus() {
static BlueZ bus;
static const bool ok = bus.Load();
return ok ? &bus : nullptr;
}
// A remote that was just paired needs a few seconds to connect; until then it
// must not be taken for a stale pairing.
std::vector<std::pair<std::string, std::chrono::steady_clock::time_point>> g_recent;
bool RecentlyPaired(const std::string& path) {
const auto now = std::chrono::steady_clock::now();
g_recent.erase(std::remove_if(g_recent.begin(), g_recent.end(),
[&](const auto& entry) { return now - entry.second > std::chrono::seconds(30); }),
g_recent.end());
return std::any_of(g_recent.begin(), g_recent.end(), [&](const auto& entry) { return entry.first == path; });
}
bool IsRemoteName(const std::string& name) {
return name.rfind("Nintendo RVL-CNT-01", 0) == 0 && name.find("-UC") == std::string::npos;
}
std::string DriverOf(const std::string& hidrawPath) {
const std::filesystem::path node(hidrawPath);
std::error_code error;
const auto driver =
std::filesystem::read_symlink(std::filesystem::path("/sys/class/hidraw") / node.filename() / "device" / "driver",
error);
return error ? std::string() : driver.filename().string();
}
#endif
} // namespace Detail
using namespace Detail;
Capabilities Query() {
#if defined(__linux__)
// Cached: adapters rarely change and this is asked often.
static std::mutex mutex;
static Capabilities cached;
static std::chrono::steady_clock::time_point checked{};
std::lock_guard<std::mutex> cacheLock(mutex);
const auto now = std::chrono::steady_clock::now();
if (checked == std::chrono::steady_clock::time_point{} || now - checked > std::chrono::seconds(10)) {
// A search holds the bus for seconds; keep the cached answer rather than wait.
std::unique_lock<std::timed_mutex> lock(g_busMutex, std::try_to_lock);
if (!lock.owns_lock()) return cached;
checked = now;
std::string adapter;
std::vector<Device> devices;
BlueZ* bus = Bus();
cached.pairing = cached.powerOff = bus != nullptr && bus->Objects(adapter, devices) && !adapter.empty();
}
return cached;
#else
return {};
#endif
}
int DiscoverAndPair(const std::atomic<bool>& cancel, std::string& status) {
#if defined(__linux__)
std::lock_guard<std::timed_mutex> lock(g_busMutex);
BlueZ* bus = Bus();
std::string adapter;
std::vector<Device> devices;
if (bus == nullptr || !bus->Objects(adapter, devices) || adapter.empty()) {
status = "No Bluetooth adapter found.";
return 0;
}
bus->Simple(adapter, "org.bluez.Adapter1", "StartDiscovery", kShortTimeoutMs);
int connected = 0;
const auto deadline = std::chrono::steady_clock::now() + kInquiryTime;
std::vector<std::string> handled;
while (!cancel && std::chrono::steady_clock::now() < deadline) {
devices.clear();
bus->Objects(adapter, devices);
for (const Device& device : devices) {
if (!IsRemoteName(device.name) || device.connected || !device.discovered) continue;
if (std::find(handled.begin(), handled.end(), device.path) != handled.end() || RecentlyPaired(device.path)) {
continue;
}
if (device.paired) {
// In pairing mode while paired: the remote forgot this PC.
bus->RemoveDevice(adapter, device.path);
continue;
}
handled.push_back(device.path);
// BlueZ's wiimote plugin answers the PIN (SYNC or 1+2).
std::string error;
if (bus->Simple(device.path, "org.bluez.Device1", "Pair", kPairTimeoutMs, &error)) {
bus->SetTrusted(device.path);
g_recent.emplace_back(device.path, std::chrono::steady_clock::now());
if (bus->Simple(device.path, "org.bluez.Device1", "Connect", kPairTimeoutMs, &error)) {
++connected;
status = "Wii Remote paired. It reconnects with any button from now on.";
RT_LOG(RT_TAG_HLE) << "Wii Remote " << device.address << " paired" << std::endl;
continue;
}
}
status = "Could not pair the Wii Remote. Press its red SYNC button and try again.";
RT_LOG(RT_TAG_HLE) << "Wii Remote " << device.address << ": pairing failed (" << error << ")" << std::endl;
}
std::this_thread::sleep_for(std::chrono::milliseconds(250));
}
bus->Simple(adapter, "org.bluez.Adapter1", "StopDiscovery", kShortTimeoutMs);
return connected;
#else
(void)cancel;
status = "Pair the Wii Remote in the system's Bluetooth settings.";
return 0;
#endif
}
bool PowerOff(const std::string& address) {
#if defined(__linux__)
std::unique_lock<std::timed_mutex> lock(g_busMutex, std::chrono::milliseconds(kShortTimeoutMs));
if (!lock.owns_lock()) return false;
BlueZ* bus = Bus();
std::string adapter;
std::vector<Device> devices;
if (bus == nullptr || !bus->Objects(adapter, devices)) return false;
for (const Device& device : devices) {
if (HexDigits(device.address) == address) {
return bus->Simple(device.path, "org.bluez.Device1", "Disconnect", kShortTimeoutMs);
}
}
#else
(void)address;
#endif
return false;
}
std::string AddressFor(const SDL_hid_device_info& info) {
const std::string hex = HexDigits(Narrow(info.serial_number));
return hex.size() == 12 && hex != "000000000000" ? hex : std::string();
}
std::string NameFor(const std::string& address, const SDL_hid_device_info& info) {
(void)address;
return Narrow(info.product_string);
}
bool MaybeConnected(const std::string& address) {
(void)address;
return true;
}
bool Usable(const SDL_hid_device_info& info) {
#if defined(__linux__)
if (info.path == nullptr) return false;
if (DriverOf(info.path) == "wiimote") {
SetNotice("The Linux hid-wiimote driver holds the Wii Remote, so the game cannot use it. Run "
"\"sudo modprobe -r hid_wiimote\" and add \"blacklist hid_wiimote\" to /etc/modprobe.d/wiimote.conf.");
return false;
}
if (access(info.path, R_OK | W_OK) != 0) {
SetNotice("No permission to open the Wii Remote. Add the udev rule "
"KERNEL==\"hidraw*\", KERNELS==\"0005:057E:*\", MODE=\"0666\" and reconnect it.");
return false;
}
SetNotice({});
#else
(void)info;
#endif
return true;
}
std::string Notice() {
std::lock_guard<std::mutex> lock(g_noticeMutex);
#if defined(__APPLE__)
if (g_notice.empty()) return "Pair Wii Remotes in System Settings > Bluetooth, then press a button on them.";
#endif
return g_notice;
}
} // namespace Wiimote::Platform
#endif
@@ -0,0 +1,300 @@
#if defined(_WIN32)
#include "wiimote_platform.h"
#include "runtime_log.h"
#include <SDL3/SDL_hidapi.h>
#include <windows.h>
#include <bluetoothapis.h>
#include <algorithm>
#include <cctype>
#include <chrono>
#include <cstdio>
#include <mutex>
#include <string>
#include <vector>
namespace Wiimote::Platform {
namespace Detail {
// bthioctl.h IOCTL_BTH_DISCONNECT_DEVICE: CTL_CODE(FILE_DEVICE_BLUETOOTH, 0x03, METHOD_BUFFERED, FILE_ANY_ACCESS).
constexpr DWORD kIoctlBthDisconnectDevice = 0x0041000C;
const GUID kHidServiceClass = {0x00001124, 0x0000, 0x1000, {0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB}};
// Each inquiry lasts this many 1.28 s units.
constexpr UCHAR kInquiryUnits = 3;
struct Radio {
HANDLE handle = nullptr;
BLUETOOTH_RADIO_INFO info{};
};
class Radios {
public:
Radios() {
BLUETOOTH_FIND_RADIO_PARAMS params{sizeof(params)};
HANDLE radio = nullptr;
HBLUETOOTH_RADIO_FIND find = BluetoothFindFirstRadio(&params, &radio);
if (find == nullptr) return;
do {
Radio entry;
entry.handle = radio;
entry.info.dwSize = sizeof(entry.info);
if (BluetoothGetRadioInfo(radio, &entry.info) == ERROR_SUCCESS) {
m_radios.push_back(entry);
} else {
CloseHandle(radio);
}
} while (BluetoothFindNextRadio(find, &radio));
BluetoothFindRadioClose(find);
}
~Radios() {
for (const Radio& radio : m_radios) CloseHandle(radio.handle);
}
Radios(const Radios&) = delete;
Radios& operator=(const Radios&) = delete;
const std::vector<Radio>& All() const { return m_radios; }
private:
std::vector<Radio> m_radios;
};
bool IsRemoteName(const WCHAR* name) {
const std::wstring text = name;
return text.rfind(L"Nintendo RVL-CNT-01", 0) == 0 && text.find(L"-UC") == std::wstring::npos;
}
std::string Hex(const BLUETOOTH_ADDRESS& address) {
char text[13];
std::snprintf(text, sizeof(text), "%012llx", static_cast<unsigned long long>(address.ullLong & 0xFFFFFFFFFFFFull));
return text;
}
bool ParseHex(const std::string& text, BLUETOOTH_ADDRESS& address) {
if (text.size() != 12) return false;
address.ullLong = std::stoull(text, nullptr, 16);
return true;
}
ULONGLONG FileTimeOf(const SYSTEMTIME& time) {
FILETIME file{};
if (!SystemTimeToFileTime(&time, &file)) return 0;
return (static_cast<ULONGLONG>(file.dwHighDateTime) << 32) | file.dwLowDateTime;
}
ULONGLONG Now() {
SYSTEMTIME now{};
GetSystemTime(&now);
return FileTimeOf(now);
}
// The remote takes the host's address as PIN after its SYNC button, its own
// address after 1+2; both as raw bytes, least significant first.
DWORD Authenticate(const Radio& radio, BLUETOOTH_DEVICE_INFO& device, const BLUETOOTH_ADDRESS& pinAddress) {
WCHAR pin[6];
for (int i = 0; i < 6; ++i) pin[i] = pinAddress.rgBytes[i];
return BluetoothAuthenticateDevice(nullptr, radio.handle, &device, pin, 6);
}
// A remote that was just paired needs a few seconds to connect its HID service;
// until then it must not be taken for a stale pairing.
constexpr auto kPairingSettleTime = std::chrono::seconds(30);
std::mutex g_recentMutex;
std::vector<std::pair<std::string, std::chrono::steady_clock::time_point>> g_recent;
bool RecentlyPaired(const std::string& address) {
std::lock_guard<std::mutex> lock(g_recentMutex);
const auto now = std::chrono::steady_clock::now();
g_recent.erase(std::remove_if(g_recent.begin(), g_recent.end(),
[&](const auto& entry) { return now - entry.second > kPairingSettleTime; }),
g_recent.end());
return std::any_of(g_recent.begin(), g_recent.end(), [&](const auto& entry) { return entry.first == address; });
}
void MarkPaired(const std::string& address) {
std::lock_guard<std::mutex> lock(g_recentMutex);
g_recent.emplace_back(address, std::chrono::steady_clock::now());
}
std::vector<BLUETOOTH_DEVICE_INFO> Devices(const Radio& radio, bool inquiry) {
std::vector<BLUETOOTH_DEVICE_INFO> devices;
BLUETOOTH_DEVICE_SEARCH_PARAMS params{sizeof(params)};
params.fReturnAuthenticated = TRUE;
params.fReturnRemembered = TRUE;
params.fReturnUnknown = TRUE;
params.fReturnConnected = TRUE;
params.fIssueInquiry = inquiry ? TRUE : FALSE;
params.cTimeoutMultiplier = inquiry ? kInquiryUnits : 0;
params.hRadio = radio.handle;
BLUETOOTH_DEVICE_INFO device{sizeof(device)};
HBLUETOOTH_DEVICE_FIND find = BluetoothFindFirstDevice(&params, &device);
if (find == nullptr) return devices;
do {
devices.push_back(device);
device = BLUETOOTH_DEVICE_INFO{sizeof(device)};
} while (BluetoothFindNextDevice(find, &device));
BluetoothFindDeviceClose(find);
return devices;
}
} // namespace Detail
using namespace Detail;
Capabilities Query() {
// Cached: radios rarely change and this is asked often.
static std::mutex mutex;
static Capabilities cached;
static std::chrono::steady_clock::time_point checked{};
std::lock_guard<std::mutex> lock(mutex);
const auto now = std::chrono::steady_clock::now();
if (checked == std::chrono::steady_clock::time_point{} || now - checked > std::chrono::seconds(10)) {
checked = now;
const Radios radios;
cached.pairing = cached.powerOff = !radios.All().empty();
}
return cached;
}
int DiscoverAndPair(const std::atomic<bool>& cancel, std::string& status) {
const Radios radios;
if (radios.All().empty()) {
status = "No Bluetooth adapter found.";
return 0;
}
int connected = 0;
for (const Radio& radio : radios.All()) {
// A remote Windows remembers without a bond can never reconnect.
for (BLUETOOTH_DEVICE_INFO& device : Devices(radio, false)) {
if (IsRemoteName(device.szName) && device.fRemembered && !device.fConnected && !device.fAuthenticated) {
BluetoothRemoveDevice(&device.Address);
}
}
if (cancel) return connected;
const ULONGLONG started = Now();
std::vector<ULONGLONG> seen;
for (BLUETOOTH_DEVICE_INFO& found : Devices(radio, true)) {
if (cancel) return connected;
if (!IsRemoteName(found.szName) || std::find(seen.begin(), seen.end(), found.Address.ullLong) != seen.end()) {
continue;
}
seen.push_back(found.Address.ullLong);
const std::string address = Hex(found.Address);
if (RecentlyPaired(address)) continue;
// The inquiry results can be seconds old: act on the current state.
BLUETOOTH_DEVICE_INFO device = found;
BluetoothGetDeviceInfo(radio.handle, &device);
if (device.fConnected) continue;
// Only a remote in pairing mode answers an inquiry; remembered ones
// that did not answer keep their old timestamp.
const bool answered = !device.fRemembered || FileTimeOf(found.stLastSeen) >= started;
if (!answered) continue;
if (device.fRemembered) {
// Paired before, but in pairing mode now: it forgot this PC.
BluetoothRemoveDevice(&device.Address);
device.fAuthenticated = FALSE;
device.fRemembered = FALSE;
}
DWORD result = Authenticate(radio, device, radio.info.address);
const bool permanent = result == ERROR_SUCCESS;
if (!permanent) {
result = Authenticate(radio, device, device.Address);
}
if (result == ERROR_SUCCESS) {
// Windows only keeps the pairing once the services were read.
DWORD count = 0;
BluetoothEnumerateInstalledServices(radio.handle, &device, &count, nullptr);
}
const DWORD service =
BluetoothSetServiceState(radio.handle, &device, const_cast<GUID*>(&kHidServiceClass), BLUETOOTH_SERVICE_ENABLE);
if (result == ERROR_SUCCESS || service == ERROR_SUCCESS) {
++connected;
MarkPaired(address);
status = permanent ? "Wii Remote paired. It reconnects with any button from now on."
: "Wii Remote connected for this session (press SYNC to pair it for good).";
RT_LOG(RT_TAG_HLE) << "Wii Remote " << address << (permanent ? " paired (SYNC)" : " connected (1+2)")
<< std::endl;
} else {
status = "Could not pair the Wii Remote. Press its red SYNC button and try again.";
RT_LOG(RT_TAG_HLE) << "Wii Remote " << address << ": pairing failed (" << result << ", " << service
<< ")" << std::endl;
}
}
}
return connected;
}
bool PowerOff(const std::string& address) {
BLUETOOTH_ADDRESS target{};
if (!ParseHex(address, target)) return false;
const Radios radios;
for (const Radio& radio : radios.All()) {
ULONGLONG raw = target.ullLong;
DWORD returned = 0;
if (DeviceIoControl(radio.handle, kIoctlBthDisconnectDevice, &raw, sizeof(raw), nullptr, 0, &returned,
nullptr)) {
return true;
}
}
return false;
}
std::string AddressFor(const SDL_hid_device_info& info) {
// Bluetooth HID devices report the remote's address as their serial number.
std::string hex;
if (info.serial_number != nullptr) {
for (const wchar_t* c = info.serial_number; *c != L'\0'; ++c) {
if (*c < 128 && std::isxdigit(static_cast<int>(*c))) hex += static_cast<char>(std::tolower(*c));
}
}
return hex.size() == 12 && hex != "000000000000" ? hex : std::string();
}
std::string NameFor(const std::string& address, const SDL_hid_device_info& info) {
BLUETOOTH_ADDRESS target{};
if (ParseHex(address, target)) {
const Radios radios;
for (const Radio& radio : radios.All()) {
BLUETOOTH_DEVICE_INFO device{sizeof(device)};
device.Address = target;
if (BluetoothGetDeviceInfo(radio.handle, &device) == ERROR_SUCCESS && device.szName[0] != L'\0') {
std::string name;
for (const WCHAR* c = device.szName; *c != L'\0'; ++c) name += static_cast<char>(*c < 128 ? *c : '?');
return name;
}
}
}
std::string name;
if (info.product_string != nullptr) {
for (const wchar_t* c = info.product_string; *c != L'\0'; ++c) name += static_cast<char>(*c < 128 ? *c : '?');
}
return name;
}
bool MaybeConnected(const std::string& address) {
BLUETOOTH_ADDRESS target{};
if (!ParseHex(address, target)) return true;
const Radios radios;
for (const Radio& radio : radios.All()) {
BLUETOOTH_DEVICE_INFO device{sizeof(device)};
device.Address = target;
if (BluetoothGetDeviceInfo(radio.handle, &device) == ERROR_SUCCESS) {
return device.fConnected != FALSE;
}
}
return true;
}
bool Usable(const SDL_hid_device_info&) {
return true;
}
std::string Notice() {
return Query().pairing ? std::string() : std::string("No Bluetooth adapter found.");
}
} // namespace Wiimote::Platform
#endif
@@ -0,0 +1,368 @@
#include "bluetooth/bt_controller.h"
#include "bluetooth/bt_sdp.h"
#include <array>
#include <cstdio>
#include <cstdlib>
#include <deque>
#include <vector>
namespace {
using Bytes = std::vector<uint8_t>;
using Bluetooth::BdAddr;
using Bluetooth::Controller;
using Bluetooth::LinkState;
int g_failures = 0;
#define CHECK(cond) \
do { \
if (!(cond)) { \
std::fprintf(stderr, "%s:%d: CHECK failed: %s\n", __FILE__, __LINE__, #cond); \
++g_failures; \
} \
} while (0)
struct FakeHost : Bluetooth::RemoteHost {
std::array<bool, Bluetooth::kRemoteSlots> attached{};
std::array<bool, Bluetooth::kRemoteSlots> wake{};
std::array<std::deque<Bytes>, Bluetooth::kRemoteSlots> input;
std::vector<std::pair<size_t, Bytes>> output;
std::array<LinkState, Bluetooth::kRemoteSlots> states{};
std::vector<size_t> guestDisconnects;
bool inquiry = false;
bool IsAttached(size_t slot) const override { return attached[slot]; }
bool ConsumeWake(size_t slot) override {
const bool w = wake[slot];
wake[slot] = false;
return w;
}
bool PopInputReport(size_t slot, Bytes& report) override {
if (input[slot].empty()) return false;
report = input[slot].front();
input[slot].pop_front();
return true;
}
void SendOutputReport(size_t slot, const uint8_t* report, size_t size) override {
output.emplace_back(slot, Bytes(report, report + size));
}
void OnLinkStateChanged(size_t slot, LinkState state) override { states[slot] = state; }
void OnGuestDisconnect(size_t slot) override { guestDisconnects.push_back(slot); }
void OnGuestInquiry(bool active) override { inquiry = active; }
const char* DeviceName(size_t) const override { return "Nintendo RVL-CNT-01"; }
};
uint16_t Le16(const Bytes& b, size_t i) {
return static_cast<uint16_t>(b[i] | (b[i + 1] << 8));
}
void PutLe16(Bytes& b, uint16_t v) {
b.push_back(static_cast<uint8_t>(v));
b.push_back(static_cast<uint8_t>(v >> 8));
}
Bytes Command(uint16_t opcode, Bytes params) {
Bytes packet;
PutLe16(packet, opcode);
packet.push_back(static_cast<uint8_t>(params.size()));
packet.insert(packet.end(), params.begin(), params.end());
return packet;
}
struct Guest {
Controller& controller;
uint64_t now = 1000;
std::vector<Bytes> Run(uint16_t opcode, Bytes params = {}) {
const Bytes packet = Command(opcode, std::move(params));
controller.ExecuteCommand(packet.data(), packet.size(), now);
return Events();
}
std::vector<Bytes> Events() {
std::vector<Bytes> events;
Bytes event;
while (controller.PopEvent(event)) events.push_back(event);
return events;
}
std::vector<Bytes> Acl() {
std::vector<Bytes> packets;
Bytes packet;
while (controller.PopAcl(packet)) packets.push_back(packet);
return packets;
}
void Tick() {
now += 1000;
controller.Tick(now);
}
void SendL2cap(uint16_t handle, uint16_t cid, const Bytes& payload) {
Bytes packet;
PutLe16(packet, static_cast<uint16_t>(handle | 0x2000));
PutLe16(packet, static_cast<uint16_t>(payload.size() + 4));
PutLe16(packet, static_cast<uint16_t>(payload.size()));
PutLe16(packet, cid);
packet.insert(packet.end(), payload.begin(), payload.end());
controller.ReceiveAcl(packet.data(), packet.size(), now);
}
void Signal(uint16_t handle, uint8_t code, uint8_t ident, const Bytes& data) {
Bytes payload = {code, ident};
PutLe16(payload, static_cast<uint16_t>(data.size()));
payload.insert(payload.end(), data.begin(), data.end());
SendL2cap(handle, 0x0001, payload);
}
};
bool IsCommandComplete(const Bytes& e, uint16_t opcode) {
return e.size() >= 6 && e[0] == 0x0E && Le16(e, 3) == opcode && e[5] == 0x00;
}
bool IsCommandStatusOk(const Bytes& e, uint16_t opcode) {
return e.size() == 6 && e[0] == 0x0F && e[2] == 0x00 && Le16(e, 4) == opcode;
}
// Splits a signalling ACL packet from the remote into (code, ident, data).
struct Signal {
uint8_t code = 0;
uint8_t ident = 0;
Bytes data;
};
std::vector<Signal> Signals(const std::vector<Bytes>& acl) {
std::vector<Signal> out;
for (const Bytes& packet : acl) {
if (packet.size() < 8 || Le16(packet, 6) != 0x0001) continue;
size_t i = 8;
while (i + 4 <= packet.size()) {
Signal s;
s.code = packet[i];
s.ident = packet[i + 1];
const uint16_t length = Le16(packet, i + 2);
s.data.assign(packet.begin() + static_cast<std::ptrdiff_t>(i + 4),
packet.begin() + static_cast<std::ptrdiff_t>(i + 4 + length));
out.push_back(s);
i += 4 + length;
}
}
return out;
}
// Opens one HID channel the remote requested and completes both configurations.
uint16_t AcceptRemoteChannel(Guest& guest, uint16_t handle, uint16_t psm, uint16_t guestCid) {
guest.Events();
const auto requests = Signals(guest.Acl());
CHECK(requests.size() == 1);
if (requests.size() != 1) return 0;
CHECK(requests[0].code == 0x02 && Le16(requests[0].data, 0) == psm);
const uint16_t remoteCid = Le16(requests[0].data, 2);
Bytes rsp;
PutLe16(rsp, guestCid);
PutLe16(rsp, remoteCid);
PutLe16(rsp, 0);
PutLe16(rsp, 0);
guest.Signal(handle, 0x03, requests[0].ident, rsp);
const auto config = Signals(guest.Acl());
CHECK(config.size() == 1 && config[0].code == 0x04 && Le16(config[0].data, 0) == guestCid);
Bytes cfg;
PutLe16(cfg, remoteCid);
PutLe16(cfg, 0);
cfg.insert(cfg.end(), {0x01, 0x02, 0xA0, 0x02});
guest.Signal(handle, 0x04, 0x40, cfg);
const auto cfgRsp = Signals(guest.Acl());
CHECK(cfgRsp.size() == 1 && cfgRsp[0].code == 0x05 && cfgRsp[0].ident == 0x40 &&
Le16(cfgRsp[0].data, 0) == guestCid && Le16(cfgRsp[0].data, 4) == 0);
Bytes ourCfgRsp;
PutLe16(ourCfgRsp, remoteCid);
PutLe16(ourCfgRsp, 0);
PutLe16(ourCfgRsp, 0);
if (!config.empty()) guest.Signal(handle, 0x05, config[0].ident, ourCfgRsp);
return remoteCid;
}
void TestBootAndConnection() {
FakeHost host;
Controller controller(host);
Guest guest{controller};
controller.Open(guest.now);
auto e = guest.Run(0x0C03);
CHECK(e.size() == 1 && IsCommandComplete(e[0], 0x0C03));
e = guest.Run(0x1005);
CHECK(e.size() == 1 && IsCommandComplete(e[0], 0x1005) && Le16(e[0], 6) == 339 && Le16(e[0], 9) == 10);
e = guest.Run(0x1009);
CHECK(e.size() == 1 && IsCommandComplete(e[0], 0x1009) && e[0][11] == 0xFF);
e = guest.Run(0x1001);
CHECK(e.size() == 1 && IsCommandComplete(e[0], 0x1001) && Le16(e[0], 7) == 0x40A7);
e = guest.Run(0x1003);
CHECK(e.size() == 1 && IsCommandComplete(e[0], 0x1003));
e = guest.Run(0x0C0D, {0, 0, 0, 0, 0, 0, 1});
CHECK(e.size() == 2 && e[0][0] == 0x15 && e[0][2] == 4 && IsCommandComplete(e[1], 0x0C0D) &&
Le16(e[1], 8) == 4);
Bytes slot0Key;
if (!e.empty() && e[0].size() >= 25) slot0Key.assign(e[0].begin() + 9, e[0].begin() + 25);
e = guest.Run(0xFC4F, {0x00});
CHECK(e.size() == 1 && IsCommandComplete(e[0], 0xFC4F));
// Attached remote, page scan off: nothing yet.
host.attached[0] = true;
host.wake[0] = true;
guest.Tick();
CHECK(guest.Events().empty());
e = guest.Run(0x0C1A, {0x02});
CHECK(e.size() == 1 && IsCommandComplete(e[0], 0x0C1A));
guest.Tick();
e = guest.Events();
CHECK(e.size() == 1 && e[0][0] == 0x04 && e[0][7] == 0x00);
const BdAddr addr = Controller::RemoteAddress(0);
Bytes accept(addr.begin(), addr.end());
accept.push_back(0x00);
e = guest.Run(0x0409, accept);
CHECK(e.size() == 3 && IsCommandStatusOk(e[0], 0x0409) && e[1][0] == 0x12 && e[2][0] == 0x03 &&
e[2][2] == 0x00 && Le16(e[2], 3) == 0x0100);
CHECK(host.states[0] == LinkState::Connecting);
// Authentication with the stored key.
e = guest.Run(0x0411, {0x00, 0x01});
CHECK(e.size() == 2 && IsCommandStatusOk(e[0], 0x0411) && e[1][0] == 0x17);
Bytes reply(addr.begin(), addr.end());
reply.insert(reply.end(), slot0Key.begin(), slot0Key.end());
e = guest.Run(0x040B, reply);
CHECK(e.size() == 2 && IsCommandComplete(e[0], 0x040B) && e[1][0] == 0x06 && e[1][2] == 0x00);
guest.Tick();
const uint16_t controlCid = AcceptRemoteChannel(guest, 0x0100, 0x0011, 0x0041);
CHECK(host.states[0] == LinkState::Connecting);
guest.Tick();
const uint16_t interruptCid = AcceptRemoteChannel(guest, 0x0100, 0x0013, 0x0042);
CHECK(controlCid != 0 && interruptCid != 0);
CHECK(host.states[0] == LinkState::Ready);
// Input passthrough.
host.input[0].push_back({0x30, 0x00, 0x08});
guest.Tick();
guest.Events();
auto acl = guest.Acl();
CHECK(acl.size() == 1);
if (acl.size() == 1) {
const Bytes& p = acl[0];
CHECK(Le16(p, 0) == 0x2100 && Le16(p, 6) == 0x0042 && p[8] == 0xA1 && p[9] == 0x30 && p[11] == 0x08);
}
// Output passthrough on the interrupt channel, and a SET_REPORT on control.
guest.SendL2cap(0x0100, interruptCid, {0xA2, 0x11, 0x10});
guest.SendL2cap(0x0100, controlCid, {0x52, 0x15, 0x00});
CHECK(host.output.size() == 2 && host.output[0].second == Bytes({0x11, 0x10}) &&
host.output[1].second == Bytes({0x15, 0x00}));
acl = guest.Acl();
CHECK(acl.size() == 1 && acl[0].size() == 9 && acl[0][8] == 0x00);
guest.Tick();
e = guest.Events();
CHECK(e.size() == 1 && e[0][0] == 0x13 && e[0][2] == 1 && Le16(e[0], 3) == 0x0100 && Le16(e[0], 5) == 2);
// The game drops the remote: a real one powers off.
e = guest.Run(0x0406, {0x00, 0x01, 0x13});
CHECK(e.size() == 2 && IsCommandStatusOk(e[0], 0x0406) && e[1][0] == 0x05 && e[1][5] == 0x16);
CHECK(host.guestDisconnects.size() == 1 && host.states[0] == LinkState::Down);
guest.Tick();
CHECK(guest.Events().empty());
}
void TestInquiryAndPaging() {
FakeHost host;
Controller controller(host);
Guest guest{controller};
controller.Open(guest.now);
guest.Run(0x0C45, {0x01});
host.attached[1] = true;
host.wake[1] = true;
guest.Tick(); // consumes the wake; page scan is off
auto e = guest.Run(0x0401, {0x00, 0x8B, 0x9E, 0x03, 0x01});
CHECK(e.size() == 1 && IsCommandStatusOk(e[0], 0x0401) && host.inquiry);
guest.Tick();
e = guest.Events();
CHECK(e.size() == 1 && e[0][0] == 0x22 && e[0][8] == 0x01);
guest.Tick();
e = guest.Events();
CHECK(e.size() == 1 && e[0][0] == 0x01);
CHECK(!host.inquiry);
Bytes create;
const BdAddr addr = Controller::RemoteAddress(1);
create.insert(create.end(), addr.begin(), addr.end());
create.insert(create.end(), {0x18, 0xCC, 0x01, 0x00, 0x00, 0x00, 0x00});
e = guest.Run(0x0405, create);
CHECK(e.size() == 2 && IsCommandStatusOk(e[0], 0x0405) && e[1][0] == 0x03 && e[1][2] == 0x00 &&
Le16(e[1], 3) == 0x0101);
// Paging a remote that is not there times out.
const BdAddr missing = Controller::RemoteAddress(2);
Bytes create2(missing.begin(), missing.end());
create2.insert(create2.end(), {0x18, 0xCC, 0x01, 0x00, 0x00, 0x00, 0x00});
e = guest.Run(0x0405, create2);
CHECK(e.size() == 1);
guest.now += 6'000'000;
guest.Tick();
e = guest.Events();
CHECK(e.size() == 1 && e[0][0] == 0x03 && e[0][2] == 0x04);
// The guest opens SDP itself on a paged remote.
Bytes sdpReq;
PutLe16(sdpReq, 0x0001);
PutLe16(sdpReq, 0x0050);
guest.Signal(0x0101, 0x02, 0x07, sdpReq);
const auto rsp = Signals(guest.Acl());
CHECK(rsp.size() == 2 && rsp[0].code == 0x03 && Le16(rsp[0].data, 4) == 0 && rsp[1].code == 0x04);
}
void TestSdp() {
// ServiceAttributeRequest for the whole HID record.
const Bytes request = {0x04, 0x00, 0x01, 0x00, 0x0E, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xFF,
0x35, 0x05, 0x0A, 0x00, 0x00, 0xFF, 0xFF, 0x00};
const Bytes response = Bluetooth::Sdp::HandleRequest(request.data(), request.size(), 4096);
CHECK(response.size() > 10 && response[0] == 0x05);
if (response.size() > 10) {
const uint16_t count = static_cast<uint16_t>((response[5] << 8) | response[6]);
CHECK(count == 0x01CC + 3 && response[7] == 0x36 && response[8] == 0x01 && response[9] == 0xCC);
CHECK(response.back() == 0x00);
}
CHECK(Bluetooth::Sdp::WiiRemoteHidDescriptor().size() == 0xD9);
// Continuation with a small MTU reassembles to the same list.
Bytes assembled;
Bytes cont = {0x00};
for (int guard = 0; guard < 64; ++guard) {
Bytes req = {0x04, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x40,
0x35, 0x05, 0x0A, 0x00, 0x00, 0xFF, 0xFF};
req.insert(req.end(), cont.begin(), cont.end());
req[4] = static_cast<uint8_t>(req.size() - 5);
const Bytes part = Bluetooth::Sdp::HandleRequest(req.data(), req.size(), 48);
CHECK(part.size() <= 48 && part[0] == 0x05);
const uint16_t count = static_cast<uint16_t>((part[5] << 8) | part[6]);
assembled.insert(assembled.end(), part.begin() + 7, part.begin() + 7 + count);
cont.assign(part.begin() + 7 + count, part.end());
if (cont.size() == 1 && cont[0] == 0) break;
}
CHECK(assembled.size() == 0x01CC + 3);
// Device ID record via ServiceSearchAttribute.
const Bytes search = {0x06, 0x00, 0x03, 0x00, 0x0F, 0x35, 0x03, 0x19, 0x12, 0x00, 0xFF, 0xFF,
0x35, 0x05, 0x0A, 0x00, 0x00, 0xFF, 0xFF, 0x00};
const Bytes found = Bluetooth::Sdp::HandleRequest(search.data(), search.size(), 4096);
CHECK(found.size() > 12 && found[0] == 0x07 && found[7] == 0x35 && found[9] == 0x36 && found[10] == 0x00 &&
found[11] == 0x5A);
}
} // namespace
int main() {
TestBootAndConnection();
TestInquiryAndPaging();
TestSdp();
if (g_failures != 0) {
std::fprintf(stderr, "%d check(s) failed\n", g_failures);
return EXIT_FAILURE;
}
std::printf("bluetooth controller tests passed\n");
return EXIT_SUCCESS;
}
@@ -93,6 +93,13 @@ public sealed partial class CxxLinearCodeGenerator
{
sb.AppendLine($"{pad}{dest} = {GuestLoadHelper(8, isFloat: true)}({RemapAddress(load.Address, types, localConstants, linkedAddressRemap)});");
}
else if (TryGetKnownEffectiveAddress(load.Address, knownConstants, out var knownLoadAddress) &&
FlatWriteNeedsPolicy(knownLoadAddress))
{
// The flat view maps the MMIO window no-access, so a hardware register read would fault
// without knowing its width. The checked read reaches the runtime's register HLE.
sb.AppendLine($"{pad}{dest} = {CheckedMmioLoadHelper(load.SizeBytes)}({RemapAddress(load.Address, types, localConstants, linkedAddressRemap)});");
}
else
{
sb.AppendLine($"{pad}{dest} = {GuestLoadHelper(load.SizeBytes, isFloat: false)}({RemapAddress(load.Address, types, localConstants, linkedAddressRemap)});");
@@ -84,6 +84,14 @@ public sealed partial class CxxLinearCodeGenerator
private static string GuestLoadHelper(int sizeBytes, bool isFloat) =>
FlatReadHelper(Bits(sizeBytes), isFloat);
private static string CheckedMmioLoadHelper(int sizeBytes) => Bits(sizeBytes) switch
{
8 => "Memory::Read8",
16 => "Memory::Read16",
64 => "Memory::Read64",
_ => "Memory::Read32"
};
private static string GuestStoreHelper(int sizeBytes, bool isFloat) =>
FlatWriteHelper(Bits(sizeBytes), isFloat);
@@ -480,6 +480,41 @@ public class CodeGenTests
Assert.Contains("MemoryInline::FlatWrite8((ctx->gpr[6] + -32768), static_cast<uint32_t>(ctx->gpr[3]));", code, StringComparison.Ordinal);
}
[Fact]
public void CodeGenerator_ReadsKnownMmioAddressesThroughCheckedHelper()
{
var function = new IrFunction(
"known_mmio_load",
"entry",
new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrAssign("r4", IrValue.Imm(unchecked((int)0xCD000000))),
new IrLoad("r0", new IrAddress("r4", 0xC0), 4),
new IrLoad("r5", new IrAddress("r3", 8), 4),
new IrReturn(null)
})
});
var types = new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>
{
["r0"] = ValueRepresentation.UInt32,
["r3"] = ValueRepresentation.UInt32,
["r4"] = ValueRepresentation.UInt32,
["r5"] = ValueRepresentation.UInt32
});
var signature = new FunctionAbiClassification(
"known_mmio_load",
ValueRepresentation.Void);
var ssa = new SsaTransformer().Convert(function);
var code = new CxxLinearCodeGenerator().Emit(0x80008020, ssa, signature, types);
Assert.Contains("= Memory::Read32(", code, StringComparison.Ordinal);
Assert.Contains("= MemoryInline::FlatRead32((ctx->gpr[3] + 8));", code, StringComparison.Ordinal);
}
[Fact]
public void CodeGenerator_ReusesFctiwzStackLowWordForMatchingLoad()
{