diff --git a/runtime/cmake/PlatformTests.cmake b/runtime/cmake/PlatformTests.cmake index 246501d..a1a073f 100644 --- a/runtime/cmake/PlatformTests.cmake +++ b/runtime/cmake/PlatformTests.cmake @@ -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. diff --git a/runtime/cmake/PublicProducts.cmake b/runtime/cmake/PublicProducts.cmake index f129ba2..da92fe7 100644 --- a/runtime/cmake/PublicProducts.cmake +++ b/runtime/cmake/PublicProducts.cmake @@ -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) diff --git a/runtime/cmake/RuntimeLibrary.cmake b/runtime/cmake/RuntimeLibrary.cmake index 6efb455..8d6160a 100644 --- a/runtime/cmake/RuntimeLibrary.cmake +++ b/runtime/cmake/RuntimeLibrary.cmake @@ -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. diff --git a/runtime/include/bluetooth/bt_bytes.h b/runtime/include/bluetooth/bt_bytes.h new file mode 100644 index 0000000..5af9c71 --- /dev/null +++ b/runtime/include/bluetooth/bt_bytes.h @@ -0,0 +1,19 @@ +#pragma once + +// HCI and L2CAP fields are little-endian. + +#include +#include + +namespace Bluetooth { + +inline uint16_t Le16(const uint8_t* p) { + return static_cast(p[0] | (p[1] << 8)); +} + +inline void PutLe16(std::vector& out, uint16_t v) { + out.push_back(static_cast(v)); + out.push_back(static_cast(v >> 8)); +} + +} // namespace Bluetooth diff --git a/runtime/include/bluetooth/bt_controller.h b/runtime/include/bluetooth/bt_controller.h new file mode 100644 index 0000000..5dd5995 --- /dev/null +++ b/runtime/include/bluetooth/bt_controller.h @@ -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 +#include +#include +#include +#include +#include + +namespace Bluetooth { + +// Bluetooth device address in HCI (least significant byte first) order. +using BdAddr = std::array; + +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& 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& 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& 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 event); + void QueueAcl(uint16_t handle, const std::vector& l2capFrame); + RemoteHost& Host() { return m_host; } + uint32_t PageTimeoutUs() const { return static_cast(m_pageTimeoutSlots) * 625u; } + +private: + struct StoredKey { + BdAddr addr{}; + std::array key{}; + }; + struct Inquiry { + bool active = false; + uint64_t endUs = 0; + uint8_t maxResponses = 0; + uint8_t reported = 0; + std::array slotReported{}; + }; + struct Reassembly { + std::vector 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 event; + }; + + void ResetState(bool keepStoredKeys); + void CommandComplete(uint16_t opcode, const std::vector& 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 m_classOfDevice{}; + std::array m_localName{}; + Inquiry m_inquiry; + bool m_hostInquiryNotified = false; + std::vector m_storedKeys; + std::array, kRemoteSlots> m_links; + std::array m_completedPackets{}; + std::array m_reassembly; + std::array m_authPending{}; + std::vector m_pending; + std::deque> m_events; + std::deque> m_acl; +}; + +} // namespace Bluetooth diff --git a/runtime/include/bluetooth/bt_remote_link.h b/runtime/include/bluetooth/bt_remote_link.h new file mode 100644 index 0000000..4beea04 --- /dev/null +++ b/runtime/include/bluetooth/bt_remote_link.h @@ -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 +#include +#include + +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 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& data); + void SendData(const Channel& channel, const std::vector& 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& 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 m_channels; + std::vector m_report; +}; + +} // namespace Bluetooth diff --git a/runtime/include/bluetooth/bt_sdp.h b/runtime/include/bluetooth/bt_sdp.h new file mode 100644 index 0000000..4cd2d7f --- /dev/null +++ b/runtime/include/bluetooth/bt_sdp.h @@ -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 +#include +#include + +namespace Bluetooth::Sdp { + +// The Wii Remote's HID report descriptor, as the game also embeds it. +const std::vector& WiiRemoteHidDescriptor(); + +// Answers one SDP request PDU. `maxResponse` bounds the PDU returned (the +// guest's L2CAP MTU on the SDP channel). +std::vector HandleRequest(const uint8_t* pdu, size_t size, size_t maxResponse); + +} // namespace Bluetooth::Sdp diff --git a/runtime/include/hle/bluetooth_hle.h b/runtime/include/hle/bluetooth_hle.h new file mode 100644 index 0000000..b9944b2 --- /dev/null +++ b/runtime/include/hle/bluetooth_hle.h @@ -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 + +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); diff --git a/runtime/include/hle/controller_status_contract.h b/runtime/include/hle/controller_status_contract.h index e948e10..8ff7318 100644 --- a/runtime/include/hle/controller_status_contract.h +++ b/runtime/include/hle/controller_status_contract.h @@ -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 diff --git a/runtime/include/hle/hollywood_gpio.h b/runtime/include/hle/hollywood_gpio.h new file mode 100644 index 0000000..a383fe1 --- /dev/null +++ b/runtime/include/hle/hollywood_gpio.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +// 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); diff --git a/runtime/include/runtime_config.h b/runtime/include/runtime_config.h index 99d6c2b..055cbec 100644 --- a/runtime/include/runtime_config.h +++ b/runtime/include/runtime_config.h @@ -57,26 +57,16 @@ struct RuntimeUserConfig { std::optional audioMuted; std::optional audioMixWorker; std::optional 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 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 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> 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 wiiAccelTrace; + // Look for remotes in pairing mode (1+2 / SYNC) while none is connected. + std::optional wiiAutoPair; + // The console's Wii Remote settings (SYSCONF BT.BAR, BT.SENS, BT.SPKV, BT.MOT). + std::optional wiiSensorBarAbove; + std::optional wiiIrSensitivity; + std::optional wiiSpeakerVolume; + std::optional wiiRumble; std::optional networkEnabled; std::optional 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> ParseAccelOffset(const toml::value& value) { - if (!value.is_array() || value.as_array().size() != 3) { - return std::nullopt; - } - std::array 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(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(document, "video", "widescreen"); @@ -494,11 +455,17 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) { config.attenuateMusicWhenMediaPlays = FindConfigValue(document, "audio", "attenuate_music_when_media_plays"); config.wiiRemotes = FindConfigValue(document, "controller", "wii_remotes"); - config.wiiContinuousScan = FindConfigValue(document, "controller", "wii_continuous_scan"); - for (const char* key : {"wii_accel_offset_x", "wii_accel_offset_y", "wii_accel_offset_z"}) { - config.hasLegacyWiiAccelOffset |= FindConfigValue(document, "controller", key).value_or(0.0) != 0.0; + config.wiiAutoPair = FindConfigValue(document, "controller", "wii_auto_pair"); + if (const auto bar = FindConfigValue(document, "controller", "wii_sensor_bar")) { + if (*bar == "above" || *bar == "below") config.wiiSensorBarAbove = *bar == "above"; } - config.wiiAccelTrace = FindConfigValue(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(document, "controller", "wii_rumble"); config.networkEnabled = FindConfigValue(document, "network", "enabled"); config.discordPresenceEnabled = FindConfigValue(document, "discord", "enabled"); config.discordClientId = FindConfigValue(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> 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& 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. diff --git a/runtime/include/wii_remote_input.h b/runtime/include/wii_remote_input.h deleted file mode 100644 index d6780cd..0000000 --- a/runtime/include/wii_remote_input.h +++ /dev/null @@ -1,111 +0,0 @@ -#pragma once - -#include -#include -#include - -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> 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 diff --git a/runtime/include/wiimote_ext_crypto.h b/runtime/include/wiimote_ext_crypto.h new file mode 100644 index 0000000..b92293c --- /dev/null +++ b/runtime/include/wiimote_ext_crypto.h @@ -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 +#include +#include +#include + +namespace Wiimote::ExtCrypto { + +struct Tables { + std::array ft{}; + std::array 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 FromKey(const std::array& keyData); + +} // namespace Wiimote::ExtCrypto diff --git a/runtime/include/wiimote_extension.h b/runtime/include/wiimote_extension.h new file mode 100644 index 0000000..d1c492c --- /dev/null +++ b/runtime/include/wiimote_extension.h @@ -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 +#include +#include +#include +#include + +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& 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 m_key{}; + std::optional m_tables; + std::array m_calibration{}; + bool m_haveCalibration = false; + uint32_t m_readAddress = 0; +}; + +} // namespace Wiimote diff --git a/runtime/include/wiimote_host.h b/runtime/include/wiimote_host.h new file mode 100644 index 0000000..1975252 --- /dev/null +++ b/runtime/include/wiimote_host.h @@ -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 +#include + +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 diff --git a/runtime/include/wiimote_platform.h b/runtime/include/wiimote_platform.h new file mode 100644 index 0000000..5c89f9f --- /dev/null +++ b/runtime/include/wiimote_platform.h @@ -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 +#include + +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& 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 diff --git a/runtime/src/hle/bluetooth/bt_controller.cpp b/runtime/src/hle/bluetooth/bt_controller.cpp new file mode 100644 index 0000000..923aa00 --- /dev/null +++ b/runtime/src/hle/bluetooth/bt_controller.cpp @@ -0,0 +1,951 @@ +#include "bluetooth/bt_controller.h" + +#include "bluetooth/bt_bytes.h" +#include "bluetooth/bt_remote_link.h" + +#include +#include +#include + +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 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 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& 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 Event(uint8_t code, const std::vector& params) { + std::vector event; + event.reserve(2 + params.size()); + event.push_back(code); + event.push_back(static_cast(params.size())); + event.insert(event.end(), params.begin(), params.end()); + return event; +} + +std::array SlotLinkKey(size_t slot) { + std::array key{}; + for (size_t i = 0; i < key.size(); ++i) { + key[i] = static_cast(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(*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(slot)}; +} + +uint16_t Controller::ConnectionHandle(size_t slot) { + return static_cast(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(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(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 event) { + m_events.push_back(std::move(event)); +} + +void Controller::QueueAcl(uint16_t handle, const std::vector& l2capFrame) { + std::vector packet; + packet.reserve(4 + l2capFrame.size()); + // Packet boundary 0x2: first (and only) fragment, automatically flushable. + PutLe16(packet, static_cast((handle & 0x0FFF) | 0x2000)); + PutLe16(packet, static_cast(l2capFrame.size())); + packet.insert(packet.end(), l2capFrame.begin(), l2capFrame.end()); + m_acl.push_back(std::move(packet)); +} + +bool Controller::PopEvent(std::vector& event) { + if (m_events.empty()) return false; + event = std::move(m_events.front()); + m_events.pop_front(); + return true; +} + +bool Controller::PopAcl(std::vector& 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& params) { + std::vector 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 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(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 body = {kStatusSuccess}; + PutLe16(body, ConnectionHandle(slot)); + PutAddr(body, addr); + body.insert(body.end(), {0x01, 0x00}); + QueueEvent(Event(kEventConnectionComplete, body)); + } else { + std::vector 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 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 body = {static_cast(it == m_pending.end() ? kStatusUnknownConnection + : kStatusSuccess)}; + PutAddr(body, addr); + CommandComplete(opcode, body); + if (it != m_pending.end()) { + std::vector 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 change = {kStatusSuccess}; + PutAddr(change, addr); + change.push_back(0x00); + QueueEvent(Event(kEventRoleChange, change)); + } + std::vector 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 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 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 auth = {static_cast(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 body = {kStatusSuccess}; + PutAddr(body, addr); + CommandComplete(opcode, body); + const int slot = SlotForAddress(addr); + if (slot >= 0 && m_authPending[slot]) { + std::vector 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 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 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 auth = {static_cast(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 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 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 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 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 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 body = {static_cast(reachable ? kStatusSuccess : kStatusPageTimeout)}; + PutAddr(body, addr); + std::array 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 body = {static_cast(it == m_pending.end() ? kStatusUnknownConnection + : kStatusSuccess)}; + PutAddr(body, addr); + CommandComplete(opcode, body); + if (it != m_pending.end()) { + std::vector 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 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 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 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 body = {kStatusSuccess}; + PutLe16(body, ConnectionHandle(static_cast(slot))); + body.push_back(mode); + PutLe16(body, interval); + QueueEvent(Event(kEventModeChange, body)); + }; + const auto handleReply = [&](std::vector extra) { + std::vector body = {static_cast(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 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 extra) { + const int slot = n >= 2 ? SlotForHandle(Le16(p)) : -1; + std::vector body = {static_cast(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 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(kKeysPerEvent, keys.size() - start); + std::vector body = {static_cast(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 body = {kStatusSuccess}; + PutLe16(body, kMaxStoredKeys); + PutLe16(body, static_cast(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 body = {kStatusSuccess}; + PutLe16(body, static_cast(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 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 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 body = {kStatusSuccess}; + body.insert(body.end(), kLocalFeatures.begin(), kLocalFeatures.end()); + CommandComplete(opcode, body); + return; + } + case 0x0005: { // Read Buffer Size + std::vector 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 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 body = {static_cast(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((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(Le16(data)) + 4 : 0; + } + if (r.expected != 0 && r.frame.size() >= r.expected) { + const std::vector frame(r.frame.begin(), r.frame.begin() + static_cast(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(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 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 eir{}; + const char* name = m_host.DeviceName(slot); + const size_t nameLength = std::min(std::strlen(name), eir.size() - 2); + eir[0] = static_cast(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 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 diff --git a/runtime/src/hle/bluetooth/bt_remote_link.cpp b/runtime/src/hle/bluetooth/bt_remote_link.cpp new file mode 100644 index 0000000..f12938e --- /dev/null +++ b/runtime/src/hle/bluetooth/bt_remote_link.cpp @@ -0,0 +1,567 @@ +#include "bluetooth/bt_remote_link.h" + +#include "bluetooth/bt_bytes.h" +#include "bluetooth/bt_sdp.h" + +#include +#include + +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 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& report) { + std::vector 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(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 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(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& data) { + std::vector frame; + frame.reserve(8 + data.size()); + PutLe16(frame, static_cast(4 + data.size())); + PutLe16(frame, kSignallingCid); + frame.push_back(code); + frame.push_back(ident); + PutLe16(frame, static_cast(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& payload) { + std::vector frame; + frame.reserve(4 + payload.size()); + PutLe16(frame, static_cast(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 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 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(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(p, p + length)); break; + case kInformationRequest: { + if (length >= 2) { + std::vector 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(ident) << std::endl; + break; + case kEchoResponse: + case kInformationResponse: + break; + default: { + std::vector 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 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 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 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& 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(i), + options.begin() + static_cast(i + 4)); + } else if (type == kOptionFlushTimeout && length == 2) { + reply.insert(reply.end(), options.begin() + static_cast(i), + options.begin() + static_cast(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 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 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 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 diff --git a/runtime/src/hle/bluetooth/bt_sdp.cpp b/runtime/src/hle/bluetooth/bt_sdp.cpp new file mode 100644 index 0000000..21ac26d --- /dev/null +++ b/runtime/src/hle/bluetooth/bt_sdp.cpp @@ -0,0 +1,438 @@ +#include "bluetooth/bt_sdp.h" + +#include +#include +#include +#include +#include +#include + +namespace Bluetooth::Sdp { +namespace { + +using Bytes = std::vector; + +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(v >> 8)); + out.push_back(static_cast(v)); +} + +void PutBe32(Bytes& out, uint32_t v) { + PutBe16(out, static_cast(v >> 16)); + PutBe16(out, static_cast(v)); +} + +uint16_t Be16(const uint8_t* p) { + return static_cast((p[0] << 8) | p[1]); +} + +uint32_t Be32(const uint8_t* p) { + return (static_cast(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(v ? 1 : 0)}; } + +Bytes WithLength(uint8_t type, size_t length, bool wide = false) { + if (length <= 0xFF && !wide) { + return {static_cast((type << 3) | 5), static_cast(length)}; + } + Bytes b{static_cast((type << 3) | 6)}; + PutBe16(b, static_cast(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 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>; + +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 kRecords = {{ + {kHidRecordHandle, HidRecord}, + {kDeviceIdRecordHandle, DeviceIdRecord}, +}}; + +using Uuid = std::array; + +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(value >> 24); + u[1] = static_cast(value >> 16); + u[2] = static_cast(value >> 8); + u[3] = static_cast(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 ParseHeader(const uint8_t* p, size_t n) { + if (n == 0) return std::nullopt; + ElementHeader h; + h.type = static_cast(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 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& 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& pattern) { + std::vector 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> 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 body{p + h->headerSize, h->dataSize}; + p += h->headerSize + h->dataSize; + n -= h->headerSize + h->dataSize; + return body; +} + +std::optional> ParsePattern(const uint8_t*& p, size_t& n) { + const auto body = TakeSequence(p, n); + if (!body) return std::nullopt; + std::vector 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; + +std::optional> ParseAttributeIds(const uint8_t*& p, size_t& n) { + const auto body = TakeSequence(p, n); + if (!body) return std::nullopt; + std::vector 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 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& 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(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(1, std::min(maxBytes, room)); + const size_t count = std::min(limit, full.size() - offset); + Bytes params; + PutBe16(params, static_cast(count)); + params.insert(params.end(), full.begin() + static_cast(offset), + full.begin() + static_cast(offset + count)); + const size_t next = offset + count; + if (next < full.size()) { + params.push_back(2); + PutBe16(params, static_cast(next)); + } else { + params.push_back(0); + } + return Pdu(responseId, tid, params); +} + +} // namespace + +const std::vector& WiiRemoteHidDescriptor() { + return kHidDescriptor; +} + +std::vector 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 diff --git a/runtime/src/hle/bluetooth/bt_sysconf.cpp b/runtime/src/hle/bluetooth/bt_sysconf.cpp new file mode 100644 index 0000000..1c3f01f --- /dev/null +++ b/runtime/src/hle/bluetooth/bt_sysconf.cpp @@ -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 +#include +#include + +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(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(Bluetooth::kRemoteSlots)); + for (size_t slot = 0; slot < Bluetooth::kRemoteSlots; ++slot) { + BluetoothSysconf::WriteEntry(buffer, static_cast(slot), slot); + BluetoothSysconf::WriteEntry(buffer, BluetoothSysconf::kActiveEntriesStart + static_cast(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(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(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(static_cast(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)); diff --git a/runtime/src/hle/bluetooth/bt_usb_device.cpp b/runtime/src/hle/bluetooth/bt_usb_device.cpp new file mode 100644 index 0000000..f0f5c05 --- /dev/null +++ b/runtime/src/hle/bluetooth/bt_usb_device.cpp @@ -0,0 +1,269 @@ +#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 +#include +#include +#include +#include + +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 = 0x30001; +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 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(duration_cast(steady_clock::now().time_since_epoch()).count()); +} + +Bluetooth::Controller& ControllerFor(Device& device) { + if (!device.controller) { + device.controller = std::make_unique(Wiimote::Host()); + } + return *device.controller; +} + +std::vector ReadGuest(uint32_t address, uint32_t length) { + std::vector data(length); + if (length != 0) { + std::memcpy(data.data(), Memory::GetPointer(address, length), length); + } + return data; +} + +void Complete(HeldRead& read, const std::vector& data) { + const uint32_t size = static_cast(std::min(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(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 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; + } + 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(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(bmRequestType) << std::dec << std::endl; + result = kIpcEinval; + return true; + } + const std::vector 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 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(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(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; + } +} diff --git a/runtime/src/hle/hollywood_gpio.cpp b/runtime/src/hle/hollywood_gpio.cpp new file mode 100644 index 0000000..ff774bd --- /dev/null +++ b/runtime/src/hle/hollywood_gpio.cpp @@ -0,0 +1,32 @@ +#include "hle/hollywood_gpio.h" + +#include "runtime_log.h" + +#include + +namespace HollywoodGpio { + +constexpr uint32_t kSensorBarPower = 0x100; + +std::atomic 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; +} diff --git a/runtime/src/hle/input/kpad.cpp b/runtime/src/hle/input/kpad.cpp deleted file mode 100644 index 5246d63..0000000 --- a/runtime/src/hle/input/kpad.cpp +++ /dev/null @@ -1,268 +0,0 @@ -#include "hle_stubs.h" -#include "input_bindings.h" -#include "memory.h" -#include "wii_remote_input.h" - -#include -#include -#include -#include - -// 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 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(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(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(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(kWpadErrNoController)); - Memory::Write8(addr + kUFmt, kFmtCoreAccDpd); - return; - } - Memory::Write16(addr + kUButton, static_cast(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(kWpadErrNone)); - if (sample->hasClassic) { - Memory::Write8(addr + kUDev, kDevClassic); - Memory::Write16(addr + kUClButton, static_cast(sample->clHold & 0xFFFF)); - Memory::Write16(addr + kUClLStickX, static_cast(sample->clLStickRaw[0])); - Memory::Write16(addr + kUClLStickY, static_cast(sample->clLStickRaw[1])); - Memory::Write16(addr + kUClRStickX, static_cast(sample->clRStickRaw[0])); - Memory::Write16(addr + kUClRStickY, static_cast(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(std::clamp(128.0f + sample->stick[0] * 100.0f, 0.0f, 255.0f))); - Memory::Write8(addr + kUFsStickY, - static_cast(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)); diff --git a/runtime/src/hle/input/pad.cpp b/runtime/src/hle/input/pad.cpp index c0bbfdb..24ad29f 100644 --- a/runtime/src/hle/input/pad.cpp +++ b/runtime/src/hle/input/pad.cpp @@ -2,7 +2,6 @@ #include "memory.h" #include "hle/controller_status_contract.h" #include "input_bindings.h" -#include "wii_remote_input.h" #include #include @@ -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); diff --git a/runtime/src/hle/input/wpad.cpp b/runtime/src/hle/input/wpad.cpp deleted file mode 100644 index 5c53953..0000000 --- a/runtime/src/hle/input/wpad.cpp +++ /dev/null @@ -1,217 +0,0 @@ -#include "hle_stubs.h" -#include "memory.h" -#include "hle/controller_status_contract.h" -#include "wii_remote_input.h" - -#include - -#include - -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(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(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)); diff --git a/runtime/src/hle/ios.cpp b/runtime/src/hle/ios.cpp index bdeab3b..9de2c3c 100644 --- a/runtime/src/hle/ios.cpp +++ b/runtime/src/hle/ios.cpp @@ -4,6 +4,7 @@ #include "hle_stubs.h" #include "memory.h" +#include "hle/bluetooth_hle.h" #include "hle/net/network.h" #include @@ -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, [&] { diff --git a/runtime/src/hle/os/os_alarm.cpp b/runtime/src/hle/os/os_alarm.cpp index a0bf0c0..13d0b26 100644 --- a/runtime/src/hle/os/os_alarm.cpp +++ b/runtime/src/hle/os/os_alarm.cpp @@ -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; diff --git a/runtime/src/hle/os/os_init.cpp b/runtime/src/hle/os/os_init.cpp index 08472a8..2f5fc8f 100644 --- a/runtime/src/hle/os/os_init.cpp +++ b/runtime/src/hle/os/os_init.cpp @@ -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(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. diff --git a/runtime/src/hle/storage/nand_internal.h b/runtime/src/hle/storage/nand_internal.h index 346be4f..c143a41 100644 --- a/runtime/src/hle/storage/nand_internal.h +++ b/runtime/src/hle/storage/nand_internal.h @@ -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" diff --git a/runtime/src/hle/storage/nand_isfs.cpp b/runtime/src/hle/storage/nand_isfs.cpp index f084226..0741989 100644 --- a/runtime/src/hle/storage/nand_isfs.cpp +++ b/runtime/src/hle/storage/nand_isfs.cpp @@ -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(fd))) { return HandleShaIoctlv(static_cast(fd), cmd, numIn, numOut, vectorPtr); diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 5f37a54..a111433 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -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); diff --git a/runtime/src/memory.cpp b/runtime/src/memory.cpp index 88a484e..7225f4b 100644 --- a/runtime/src/memory.cpp +++ b/runtime/src/memory.cpp @@ -1,4 +1,5 @@ #include "memory.h" +#include "hle/hollywood_gpio.h" #include #include @@ -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); diff --git a/runtime/src/settings_overlay.cpp b/runtime/src/settings_overlay.cpp index 923d0ac..99d0915 100644 --- a/runtime/src/settings_overlay.cpp +++ b/runtime/src/settings_overlay.cpp @@ -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 #include @@ -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(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 offset = stored.value_or(std::array{}); - 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)) { + 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(sample.clLStickRaw[0]), - static_cast(sample.clLStickRaw[1]), sample.clRStick[0], sample.clRStick[1], - static_cast(sample.clRStickRaw[0]), static_cast(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(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(assigned)); + } else if (const Wiimote::SlotInfo remote = Wiimote::Slot(port); remote.attached) { + preview = WiiRemoteLabel(remote); } ImGui::PushID(static_cast(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(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 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(); diff --git a/runtime/src/wii_remote_input.cpp b/runtime/src/wii_remote_input.cpp deleted file mode 100644 index a219c40..0000000 --- a/runtime/src/wii_remote_input.cpp +++ /dev/null @@ -1,1069 +0,0 @@ -#include "wii_remote_input.h" - -#include "runtime_config.h" -#include "runtime_log.h" - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace WiiRemoteInput { -namespace { - -// Dolphin's continuous scanning polls Bluetooth about once a second; SDL's -// enumeration walks every HID device on the main thread, so stay a bit lazier. -constexpr uint64_t kScanIntervalMs = 2000; -// Right after a remote drops it is almost certainly still there, but the first -// re-open attempts tend to fail on timed-out reads, so retry quickly for a while. -constexpr uint64_t kFastScanIntervalMs = 500; -constexpr uint64_t kFastScanWindowMs = 15000; -// How long the Wii driver hint stays at "0" during a rescan; SDL applies hint -// changes on its next joystick update, once per pumped frame. -constexpr uint64_t kRescanDriverOffMs = 100; - -constexpr float kStandardGravity = 9.80665f; - -// SDL's Wii driver posts the remote's own buttons as raw joystick buttons -// starting at SDL_GAMEPAD_BUTTON_MISC1, in this order (SDL_hidapi_wii.c, -// EWiiButtons), whatever the extension. -enum RawWiiButton : int { - kRawA = SDL_GAMEPAD_BUTTON_MISC1, - kRawB, - kRawOne, - kRawTwo, - kRawPlus, - kRawMinus, - kRawHome, - kRawDpadUp, - kRawDpadDown, - kRawDpadLeft, - kRawDpadRight, -}; - -// WPAD_BUTTON_* bits as the game reads them from KPADStatus.hold. -constexpr uint32_t kWpadLeft = 0x0001, kWpadRight = 0x0002, kWpadDown = 0x0004, kWpadUp = 0x0008, - kWpadPlus = 0x0010, kWpadTwo = 0x0100, kWpadOne = 0x0200, kWpadB = 0x0400, kWpadA = 0x0800, - kWpadMinus = 0x1000, kWpadZ = 0x2000, kWpadC = 0x4000, kWpadHome = 0x8000; - -// WPAD_CL_BUTTON_* bits (WPADCLStatus.clButton / KPADStatus.ex_status.cl.hold): -// the Classic Controller's two button bytes, inverted, high byte first. -constexpr uint32_t kClUp = 0x0001, kClLeft = 0x0002, kClZR = 0x0004, kClX = 0x0008, kClA = 0x0010, kClY = 0x0020, - kClB = 0x0040, kClZL = 0x0080, kClR = 0x0200, kClPlus = 0x0400, kClHome = 0x0800, - kClMinus = 0x1000, kClL = 0x2000, kClDown = 0x4000, kClRight = 0x8000; - -constexpr Uint16 kNintendoVendor = 0x057e; - -// How long a vanished remote keeps its channel alive with neutral input. SDL's -// in-place reconnect after an extension change takes well under a second; a -// remote that is really gone shows up as disconnected after this. -constexpr uint64_t kExtensionSwapGraceMs = 3000; -// Rescanning closes and re-opens the Bluetooth HID handle, which some Windows -// stacks answer by dropping the link; leave SDL's own reconnect this long first. -constexpr uint64_t kScanStartDelayMs = 3000; - -// Per-port memory of the last Wii controller seen there, for EffectiveKind. -struct PortMemory { - Kind lastKind = Kind::NotWii; - uint64_t lastSeenMs = 0; -}; -std::array g_ports{}; - -bool g_wiiDriverEnabled = false; -uint64_t g_lastScanMs = 0; -uint32_t g_scanCount = 0; -bool g_scanning = false; -// Non-zero while a rescan has the Wii driver hint switched off (see RescanNow). -uint64_t g_driverOffSinceMs = 0; -// When the current scan started (last Wii controller seen). -uint64_t g_lostAtMs = 0; -bool g_hidrawWarned = false; - -// Instance ids whose accelerometers have been switched on. SDL keeps sensors -// off until asked and forgets that when the gamepad is closed, so a re-paired -// remote gets a fresh id and is enabled again. -std::array g_sensorsEnabledFor{}; - -// Zero-point correction subtracted from a remote's accelerometer, in g and in -// SDL's sensor frame, with the config key it is stored under. Resolved once per -// gamepad instance (a re-created joystick of the same remote resolves to the same -// key) and replaced by a calibration run. The Nunchuk accelerometer is left -// uncorrected. -struct RemoteCalibration { - SDL_JoystickID instance = 0; - std::string key; - std::array offset{}; -}; -std::array g_remoteCalibration{}; - -// Frames sampled by a calibration run (about 1.5 s at 60 Hz) and how far a -// sample may stray from the first one before the run is declared "moved". -constexpr int kCalibrationSamples = 90; -constexpr float kCalibrationMaxDeviationG = 0.15f; -// A resting remote reads about 1 g and lies within 20 degrees of face up. -constexpr double kCalibrationMinGravityG = 0.8; -constexpr double kCalibrationMaxGravityG = 1.2; -constexpr double kCalibrationMinTiltCosine = 0.9396926; - -struct AccelCalibration { - bool active = false; - uint32_t chan = 0; - SDL_JoystickID instance = 0; - int count = 0; - double sum[3] = {}; - float first[3] = {}; -}; -AccelCalibration g_calibration; -char g_calibrationMessage[160] = {}; -uint32_t g_calibrationMessageChan = 0; -SDL_JoystickID g_calibrationMessageRemote = 0; - -// Last accepted KPAD acc per port, for the remote and for the Nunchuk. Reused on -// frames that bring no sample or a glitched one, so the game never sees a jump. -struct LastAcc { - bool valid = false; - float acc[3] = {0.0f, -1.0f, 0.0f}; -}; -std::array g_lastAcc{}; -std::array g_lastNunchukAcc{}; - -// A zeroed remote report decodes to minus its zero-g point, at least this far out on every axis. -constexpr float kZeroReportMinG = 4.0f; - -// Case-sensitive substring test that tolerates a null name. -bool NameContains(const char* name, const char* needle) { - return name != nullptr && std::strstr(name, needle) != nullptr; -} - -// Turns on the remote (and Nunchuk) accelerometers once per gamepad instance. -void EnsureSensors(SDL_Gamepad* gamepad, uint32_t port) { - const SDL_JoystickID id = SDL_GetGamepadID(gamepad); - if (g_sensorsEnabledFor[port] == id) { - return; - } - bool allEnabled = true; - for (SDL_SensorType sensor : {SDL_SENSOR_ACCEL, SDL_SENSOR_ACCEL_L}) { - if (SDL_GamepadHasSensor(gamepad, sensor) && !SDL_SetGamepadSensorEnabled(gamepad, sensor, true)) { - RT_LOG(RT_TAG_CONFIG) << "Wii Remote on port " << (port + 1) - << ": could not enable an accelerometer: " << SDL_GetError() << std::endl; - allEnabled = false; - } - } - // Only remember the instance once every sensor is on, so a failed attempt - // is retried on the next sample instead of leaving the accelerometer off. - if (allEnabled) { - g_sensorsEnabledFor[port] = id; - } -} - -// USB interface number of the HID device at `path`, or -1 when it is not a USB -// interface or cannot be found. A DolphinBar shows each remote slot as one. -int UsbInterfaceForPath(const char* path, Uint16 vendor, Uint16 product) { - if (path == nullptr) { - return -1; - } - int interfaceNumber = -1; - SDL_hid_device_info* list = SDL_hid_enumerate(vendor, product); - for (const SDL_hid_device_info* info = list; info != nullptr; info = info->next) { - if (info->path != nullptr && std::strcmp(info->path, path) == 0) { - interfaceNumber = info->interface_number; - break; - } - } - SDL_hid_free_enumeration(list); - return interfaceNumber; -} - -// Config key identifying the physical remote behind a gamepad: its HID serial -// (the Bluetooth address when paired directly), else its USB interface number -// (a DolphinBar has no serial and one interface per remote slot) and the game -// port, else the game port. Only characters valid in a bare TOML key. -std::string RemoteKey(SDL_Gamepad* gamepad, uint32_t port) { - std::string serial; - if (const char* raw = SDL_GetGamepadSerial(gamepad)) { - for (const char* c = raw; *c != '\0'; ++c) { - if (std::isalnum(static_cast(*c))) { - serial += static_cast(std::tolower(static_cast(*c))); - } - } - } - if (serial.find_first_not_of('0') != std::string::npos) { - return "serial_" + serial; - } - const Uint16 vendor = SDL_GetGamepadVendor(gamepad); - const Uint16 product = SDL_GetGamepadProduct(gamepad); - const int interfaceNumber = UsbInterfaceForPath(SDL_GetGamepadPath(gamepad), vendor, product); - char key[48]; - if (interfaceNumber >= 0) { - std::snprintf(key, sizeof(key), "usb_%04x_%04x_if%d_port%u", vendor, product, interfaceNumber, - port + 1); - } else { - std::snprintf(key, sizeof(key), "port_%u", port + 1); - } - return key; -} - -// Calibration of the remote currently on `port`, resolved on first use per gamepad -// instance. A remote without a stored entry has a zero offset. -RemoteCalibration& CalibrationFor(SDL_Gamepad* gamepad, uint32_t port) { - RemoteCalibration& calibration = g_remoteCalibration[port]; - const SDL_JoystickID id = SDL_GetGamepadID(gamepad); - if (calibration.instance != id) { - static bool s_legacyReported = false; - if (!s_legacyReported && RuntimeConfigFile::Get().hasLegacyWiiAccelOffset) { - s_legacyReported = true; - RT_LOG(RT_TAG_CONFIG) << "Old shared Wii Remote calibration ignored; recalibrate each remote in F10" - << std::endl; - } - calibration.instance = id; - calibration.key = RemoteKey(gamepad, port); - const std::optional> stored = RuntimeConfigFile::WiiAccelOffsetFor(calibration.key); - for (size_t i = 0; i < 3; ++i) calibration.offset[i] = stored ? static_cast((*stored)[i]) : 0.0f; - RT_LOG(RT_TAG_CONFIG) << "Wii Remote on port " << (port + 1) << ": calibration key " << calibration.key - << (stored ? "" : " (no stored offset)") << std::endl; - } - return calibration; -} - -// Raw SDL sample in m/s^2, rejecting anything SDL has not delivered yet. -bool ReadSdlAccel(SDL_Gamepad* gamepad, SDL_SensorType sensor, float* sdl) { - return SDL_GamepadSensorEnabled(gamepad, sensor) && SDL_GetGamepadSensorData(gamepad, sensor, sdl, 3) && - std::isfinite(sdl[0]) && std::isfinite(sdl[1]) && std::isfinite(sdl[2]); -} - -// True for a sample that cannot have come from the sensor. Over Bluetooth on -// Windows the remote delivers, a few times a minute, a report whose -// accelerometer bytes are all zero; SDL decodes that as (+a, -b, -c) g with -// a, b, c the zero-g point over the units per g of each axis: about 5 g for the -// remote (factory calibration) and exactly 2.56 g for the Nunchuk (200 units/g). -// Handed to the game as is, one such frame is a full-lock steer plus a 9 g -// "shake". Single axes past 4 g are real readings of a hard shake. `g` is the -// uncorrected SDL sample. -bool IsGlitchedSample(SDL_SensorType sensor, const float* g) { - // An exact zero vector is SDL's sensor buffer before the first report, not - // a reading (the remote never delivers 0 g on all three axes at once). - if (g[0] == 0.0f && g[1] == 0.0f && g[2] == 0.0f) { - return true; - } - if (sensor == SDL_SENSOR_ACCEL) { - return g[0] > kZeroReportMinG && g[1] < -kZeroReportMinG && g[2] < -kZeroReportMinG; - } - constexpr float zero = 2.56f; - return std::fabs(g[0] - zero) < 0.03f && std::fabs(g[1] + zero) < 0.03f && std::fabs(g[2] + zero) < 0.03f; -} - -// One accelerometer sample in g, SDL's sensor frame. The remote's own axes are -// +x left, +y towards the user, +z out of the button face (wiibrew, Dolphin); -// SDL_hidapi_wii.c posts (-wiiX, wiiZ, wiiY): x right across the face, y out of -// the face (+1 at rest, buttons up), z towards the user, i.e. away from the tip. -// The remote's sample gets the zero-point correction of the remote on `port`; the -// Nunchuk's does not. False when there is no sample yet or the sample is a glitch -// (see above). -bool ReadAccelG(SDL_Gamepad* gamepad, uint32_t port, SDL_SensorType sensor, float* g) { - float sdl[3] = {}; - if (!ReadSdlAccel(gamepad, sensor, sdl)) { - return false; - } - for (int i = 0; i < 3; ++i) g[i] = sdl[i] / kStandardGravity; - if (IsGlitchedSample(sensor, g)) { - return false; - } - if (sensor == SDL_SENSOR_ACCEL) { - const std::array& offset = CalibrationFor(gamepad, port).offset; - for (int i = 0; i < 3; ++i) g[i] -= offset[i]; - } - return true; -} - -// KPAD's acc is the remote reading as (-wiiX, -wiiZ, wiiY): x right across the -// face, y through the back of the remote (rest: -1 with the buttons up), z -// towards the user. That is SDL's frame with y negated. Held sideways as a -// wheel the rest vector is (1, 0, 0) and a turn shows up as z = sin(angle), so -// the sign of z is the direction of the turn; Cemu does the same conversion for -// real remotes on the Wii U. -void AccelGToKpad(const float* g, float* kpad) { - kpad[0] = g[0]; - kpad[1] = -g[1]; - kpad[2] = g[2]; -} - -// Sensor -> KPAD acc for ReadKpadSample. -bool ReadAccelAsKpad(SDL_Gamepad* gamepad, uint32_t port, SDL_SensorType sensor, float* kpad) { - float g[3] = {}; - if (!ReadAccelG(gamepad, port, sensor, g)) { - return false; - } - AccelGToKpad(g, kpad); - return true; -} - -// Debug trace of every remote sample (controller.wii_accel_trace = true): -// milliseconds, port, WPAD hold bits, the uncorrected SDL sample in g and the -// KPAD acc handed to the game. One CSV per run, truncated at startup. -void TraceSample(uint32_t chan, const KpadSample& sample, const float* rawG, bool haveRaw, bool accepted) { - static std::ofstream trace; - static bool opened = false; - if (!opened) { - opened = true; - const std::filesystem::path path = RuntimeConfigFile::ApplicationDataDirectory() / "wii_accel_trace.csv"; - trace.open(path, std::ios::trunc); - if (trace) { - trace << "ms,port,hold,raw_x,raw_y,raw_z,ok,kpad_x,kpad_y,kpad_z\n"; - RT_LOG(RT_TAG_CONFIG) << "Wii Remote accelerometer trace: " << path.string() << std::endl; - } else { - RT_LOG(RT_TAG_CONFIG) << "Wii Remote accelerometer trace: could not open " << path.string() << std::endl; - } - } - if (!trace) { - return; - } - char line[192]; - if (haveRaw) { - std::snprintf(line, sizeof(line), "%llu,%u,%04x,%.4f,%.4f,%.4f,%d,%.4f,%.4f,%.4f\n", - static_cast(SDL_GetTicks()), chan + 1, sample.hold, rawG[0], rawG[1], - rawG[2], accepted ? 1 : 0, sample.acc[0], sample.acc[1], sample.acc[2]); - } else { - std::snprintf(line, sizeof(line), "%llu,%u,%04x,,,,0,%.4f,%.4f,%.4f\n", - static_cast(SDL_GetTicks()), chan + 1, sample.hold, sample.acc[0], - sample.acc[1], sample.acc[2]); - } - trace << line; - // A frame per line; flush so a crash or a killed process keeps the tail. - trace.flush(); -} - -// Gamepad of the Wii Remote on `chan`, or nullptr. -SDL_Gamepad* RemoteGamepad(uint32_t chan) { - if (chan >= PAD_MAX_CONTROLLERS || !IsRemoteChannel(chan)) { - return nullptr; - } - return SDL_GetGamepadFromPlayerIndex(static_cast(chan)); -} - -void SetCalibrationMessage(uint32_t chan, const char* message) { - SDL_Gamepad* gamepad = RemoteGamepad(chan); - g_calibrationMessageChan = chan; - g_calibrationMessageRemote = gamepad != nullptr ? SDL_GetGamepadID(gamepad) : 0; - std::snprintf(g_calibrationMessage, sizeof(g_calibrationMessage), "%s", message); -} - -// Ends a calibration run with a message for the overlay. -void FinishAccelCalibration(uint32_t chan, const char* message) { - g_calibration.active = false; - SetCalibrationMessage(chan, message); - RT_LOG(RT_TAG_CONFIG) << "Wii Remote accelerometer calibration: " << message << std::endl; -} - -// One frame of a calibration run: accumulates the uncorrected sample and, once -// enough frames are in, stores the mean minus the ideal rest vector (0, 1, 0). -void StepAccelCalibration() { - if (!g_calibration.active) { - return; - } - SDL_Gamepad* gamepad = RemoteGamepad(g_calibration.chan); - if (gamepad == nullptr || - SDL_GetGamepadID(gamepad) != g_calibration.instance) { - FinishAccelCalibration(g_calibration.chan, "Cancelled: the Wii Remote went away."); - return; - } - EnsureSensors(gamepad, g_calibration.chan); - float sdl[3] = {}; - if (!ReadSdlAccel(gamepad, SDL_SENSOR_ACCEL, sdl)) { - return; // no sample this frame; keep waiting - } - float g[3]; - for (int i = 0; i < 3; ++i) g[i] = sdl[i] / kStandardGravity; - if (IsGlitchedSample(SDL_SENSOR_ACCEL, g)) { - return; // a zeroed report, not a movement - } - if (g_calibration.count == 0) { - for (int i = 0; i < 3; ++i) g_calibration.first[i] = g[i]; - } else { - for (int i = 0; i < 3; ++i) { - if (std::fabs(g[i] - g_calibration.first[i]) > kCalibrationMaxDeviationG) { - FinishAccelCalibration(g_calibration.chan, "Failed: the remote moved. Put it down, buttons up, and try again."); - return; - } - } - } - for (int i = 0; i < 3; ++i) g_calibration.sum[i] += g[i]; - if (++g_calibration.count < kCalibrationSamples) { - return; - } - std::array mean{}; - for (int i = 0; i < 3; ++i) mean[i] = g_calibration.sum[i] / g_calibration.count; - const double length = std::sqrt(mean[0] * mean[0] + mean[1] * mean[1] + mean[2] * mean[2]); - if (length < kCalibrationMinGravityG || length > kCalibrationMaxGravityG || - mean[1] / length < kCalibrationMinTiltCosine) { - FinishAccelCalibration(g_calibration.chan, "Failed: the remote was not resting flat with the buttons up."); - return; - } - const std::array offset = {mean[0], mean[1] - 1.0, mean[2]}; - RemoteCalibration& calibration = CalibrationFor(gamepad, g_calibration.chan); - for (int i = 0; i < 3; ++i) calibration.offset[i] = static_cast(offset[i]); - const bool saved = RuntimeConfigFile::SetWiiAccelOffsetFor(calibration.key, offset); - char message[160]; - std::snprintf(message, sizeof(message), "%s offset x %+.3f y %+.3f z %+.3f g", - saved ? "Calibrated:" : "Calibrated (could not write Config.toml):", offset[0], offset[1], - offset[2]); - FinishAccelCalibration(g_calibration.chan, message); -} - -// True when any controller aurora knows about is a Wii device. -bool AnyWiiControllerConnected() { - const uint32_t count = PADCount(); - for (uint32_t index = 0; index < count; ++index) { - if (KindForName(PADGetNameForControllerIndex(index)) != Kind::NotWii) { - return true; - } - } - return false; -} - -// True for an evdev node served by the kernel hid-wiimote driver. -bool IsKernelWiiNode(SDL_JoystickID id) { - const char* path = SDL_GetGamepadPathForID(id); - return SDL_GetGamepadVendorForID(id) == kNintendoVendor && path != nullptr && - std::strncmp(path, "/dev/input/", 11) == 0; -} - -bool IsClassicNodeName(const char* name) { - return NameContains(name, "Classic Controller"); -} - -// HID device behind a kernel evdev node; the remote and its extension nodes share it. -std::string KernelParent(SDL_JoystickID id) { - static std::unordered_map s_parents; - if (const auto it = s_parents.find(id); it != s_parents.end()) { - return it->second; - } - const char* path = SDL_GetGamepadPathForID(id); - if (path == nullptr) { - return {}; - } - std::error_code ec; - const auto parent = std::filesystem::canonical( - std::filesystem::path("/sys/class/input") / std::filesystem::path(path).filename() / "device" / "device", ec); - if (ec) { - return {}; - } - return s_parents.emplace(id, parent.string()).first->second; -} - -// The kernel node on the same remote as `gamepad`: its Classic Controller node -// when `wantClassic`, else its remote node. -SDL_Gamepad* KernelSibling(SDL_Gamepad* gamepad, bool wantClassic) { - const SDL_JoystickID self = SDL_GetGamepadID(gamepad); - const std::string parent = KernelParent(self); - if (parent.empty()) { - return nullptr; - } - int count = 0; - SDL_JoystickID* ids = SDL_GetGamepads(&count); - if (ids == nullptr) { - return nullptr; - } - SDL_Gamepad* sibling = nullptr; - for (int i = 0; i < count && sibling == nullptr; ++i) { - if (ids[i] != self && IsKernelWiiNode(ids[i]) && - IsClassicNodeName(SDL_GetGamepadNameForID(ids[i])) == wantClassic && KernelParent(ids[i]) == parent) { - sibling = SDL_GetGamepadFromID(ids[i]); - } - } - SDL_free(ids); - return sibling; -} - -// The kernel exposes a remote and its Classic Controller as separate gamepads. -// The remote's port reads both, as on a Wii; the Classic node's own port stays empty. -Kind KindForGamepad(SDL_Gamepad* gamepad) { - if (gamepad == nullptr) { - return Kind::NotWii; - } - const char* name = SDL_GetGamepadName(gamepad); - const Kind kind = KindForName(name); - if ((kind != Kind::Remote && kind != Kind::RemoteWithClassic) || !IsKernelWiiNode(SDL_GetGamepadID(gamepad))) { - return kind; - } - if (IsClassicNodeName(name)) { - return KernelSibling(gamepad, false) != nullptr ? Kind::NotWii : kind; - } - return KernelSibling(gamepad, true) != nullptr ? Kind::RemoteWithClassic : kind; -} - -// A kernel Classic Controller node whose remote node carries it. -bool IsCarriedKernelNode(SDL_Gamepad* gamepad) { - return gamepad != nullptr && IsKernelWiiNode(SDL_GetGamepadID(gamepad)) && - IsClassicNodeName(SDL_GetGamepadName(gamepad)) && KernelSibling(gamepad, false) != nullptr; -} - -// Logs once when the kernel driver serves a Wii controller SDL's driver could have had. -void WarnIfKernelDriverServing() { - if (g_hidrawWarned || !g_wiiDriverEnabled) { - return; - } - const uint64_t now = SDL_GetTicks(); - static uint64_t s_nextCheckMs = 0; - if (now < s_nextCheckMs) { - return; - } - s_nextCheckMs = now + 2000; - int count = 0; - SDL_JoystickID* ids = SDL_GetGamepads(&count); - if (ids == nullptr) { - return; - } - for (int i = 0; i < count && !g_hidrawWarned; ++i) { - const Uint16 product = SDL_GetGamepadProductForID(ids[i]); - if ((product == 0x0306 || product == 0x0330) && IsKernelWiiNode(ids[i])) { - g_hidrawWarned = true; - RT_LOG(RT_TAG_CONFIG) << "A Wii controller is served by the kernel hid-wiimote driver. For full Wii " - << "Remote support see the Linux udev rule in the README." << std::endl; - } - } - SDL_free(ids); -} - -// Route SDL's input diagnostics (HIDAPI open failures, the Wii driver's -// extension/status messages) into console.log, minus the periodic chatter. -// A sub-warning message is written once: SDL repeats the same line on every -// enumeration (one "couldn't open /dev/hidraw7: Permission denied" per HID -// device per pass), and console.log is unbuffered, so the repeats were a -// per-pass burst of writes on the main thread for no new information. -void SDLCALL LogSdlMessage(void*, int category, SDL_LogPriority priority, const char* message) { - if (message == nullptr) { - return; - } - if (category == SDL_LOG_CATEGORY_INPUT && priority < SDL_LOG_PRIORITY_WARN && - (std::strstr(message, "Motion Plus") != nullptr || std::strstr(message, "Resetting report mode") != nullptr)) { - return; - } - if (category != SDL_LOG_CATEGORY_INPUT && priority < SDL_LOG_PRIORITY_WARN) { - return; - } - if (priority < SDL_LOG_PRIORITY_WARN) { - // Device paths in these messages keep changing (/dev/hidrawN climbs with - // hotplug churn), so cap the set instead of holding one string per line - // for the whole session. - static std::unordered_set s_seen; - if (s_seen.size() >= 256) { - s_seen.clear(); - } - if (!s_seen.insert(message).second) { - return; - } - RT_LOG("sdl") << message << " (further identical messages suppressed)" << std::endl; - return; - } - RT_LOG("sdl") << message << std::endl; -} - -// Whether Poll() drives its own periodic re-enumeration. The 1->0->1 hint -// flip below makes SDL close and re-open every HIDAPI device on the main -// thread, and on Linux that means an open() attempt on every /dev/hidraw node -// (each failing with EACCES until a udev rule grants access), which showed up -// as a frame hitch every scan interval even on an empty menu. It exists for -// Windows Bluetooth stacks, where a remote that drops or is switched on after -// launch is not seen again until the driver re-enumerates. Linux and macOS -// already get hotplug from udev / IOKit: SDL re-enumerates when a device -// appears, so nothing periodic is needed there. The overlay's "Rescan now" -// still works everywhere. -#if defined(_WIN32) -constexpr bool kPeriodicRescan = true; -#else -constexpr bool kPeriodicRescan = false; -#endif - -// Second half of a rescan: re-enables the Wii driver once SDL has seen it off. -void FinishRescan(uint64_t now) { - if (g_driverOffSinceMs == 0 || now - g_driverOffSinceMs < kRescanDriverOffMs) { - return; - } - SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_WII, "1"); - g_driverOffSinceMs = 0; - g_lastScanMs = now; - ++g_scanCount; - // The first few and then every tenth, so a remote that never comes back - // leaves a trail in console.log without flooding it. - if (g_scanCount <= 3 || g_scanCount % 10 == 0) { - RT_LOG(RT_TAG_CONFIG) << "Wii Remote rescan #" << g_scanCount << ": HIDAPI Wii driver re-enabled (" - << PADCount() << " controller(s) known to aurora)" << std::endl; - } -} - -} // namespace - -// Enables SDL's HIDAPI Wii driver and player LEDs, and routes SDL's input log. -void ConfigureSdlHints(bool enabled) { - // A rescan may be mid-flight; drop its bookkeeping so Poll() is not left - // waiting for a FinishRescan() that can no longer happen. - g_driverOffSinceMs = 0; - if (!SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_WII, enabled ? "1" : "0")) { - RT_LOG(RT_TAG_CONFIG) << "Failed to set " << SDL_HINT_JOYSTICK_HIDAPI_WII << ": " << SDL_GetError() - << std::endl; - } - // Light the player LED that matches the SDL player index, like the console does. - if (!SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_WII_PLAYER_LED, "1")) { - RT_LOG(RT_TAG_CONFIG) << "Failed to set " << SDL_HINT_JOYSTICK_HIDAPI_WII_PLAYER_LED << ": " - << SDL_GetError() << std::endl; - } - RT_LOG(RT_TAG_CONFIG) << "Bluetooth Wii Remote support " << (enabled ? "enabled" : "disabled") << std::endl; - g_wiiDriverEnabled = enabled; - if (enabled) { - SDL_SetLogPriority(SDL_LOG_CATEGORY_INPUT, SDL_LOG_PRIORITY_DEBUG); - SDL_SetLogOutputFunction(LogSdlMessage, nullptr); - } -} - -// Starts a rescan by disabling the Wii driver hint; Poll() finishes it. -void RescanNow() { - if (!g_wiiDriverEnabled || g_driverOffSinceMs != 0) { - return; - } - // SDL only closes the HID handle of a remote it dropped while the Wii driver - // is disabled, and only re-opens it when the driver is enabled again; both - // must happen on separate joystick updates, so the hint stays at "0" until - // FinishRescan() a few frames later. Flipping 1->0->1 within one frame does - // nothing: SDL only ever sees the final "1". - SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_WII, "0"); - g_driverOffSinceMs = SDL_GetTicks(); - if (g_scanCount < 3) { - RT_LOG(RT_TAG_CONFIG) << "Wii Remote rescan #" << (g_scanCount + 1) << ": HIDAPI Wii driver disabled" - << std::endl; - } -} - -// Per-frame scanning state machine: rescans while no Wii controller is present. -// Also advances an accelerometer calibration run, which needs a sample per frame -// whether or not the game is reading KPAD at that moment. -void Poll() { - StepAccelCalibration(); - // Remember what each port had, so EffectiveKind can bridge a swap. - for (uint32_t port = 0; port < PAD_MAX_CONTROLLERS; ++port) { - (void)EffectiveKind(port); - } - WarnIfKernelDriverServing(); - if (!g_wiiDriverEnabled) { - return; - } - // Always complete a rescan in progress so the driver is never left disabled. - FinishRescan(SDL_GetTicks()); - if (g_driverOffSinceMs != 0) { - return; - } - if (AnyWiiControllerConnected()) { - if (g_scanning) { - RT_LOG(RT_TAG_CONFIG) << "Wii Remote found after " << g_scanCount << " rescan(s)" << std::endl; - } - g_scanning = false; - g_scanCount = 0; - g_lastScanMs = SDL_GetTicks(); - return; - } - if (!RuntimeConfigFile::WiiContinuousScanEnabled(false)) { - g_scanning = false; - return; - } - const uint64_t now = SDL_GetTicks(); - if (!g_scanning) { - RT_LOG(RT_TAG_CONFIG) << "No Wii Remote connected; " - << (kPeriodicRescan ? "scanning for one" : "waiting for one to be paired") - << " (press 1+2 on the remote)" << std::endl; - g_scanning = true; - g_lostAtMs = now; - } - if (!kPeriodicRescan || now - g_lostAtMs < kScanStartDelayMs) { - return; - } - const uint64_t interval = now - g_lostAtMs < kFastScanWindowMs ? kFastScanIntervalMs : kScanIntervalMs; - if (now - g_lastScanMs < interval) { - return; - } - RescanNow(); -} - -// True while Poll() is looking for a remote. -bool IsScanning() { - return g_scanning; -} - -// Whether looking for a remote means periodic rescans or waiting for hotplug. -bool PeriodicRescanEnabled() { - return kPeriodicRescan; -} - -// Number of rescans since a Wii controller was last seen. -uint32_t ScanCount() { - return g_scanCount; -} - -// Maps the gamepad name SDL reports to a Kind: "Nintendo Wii Remote", -// "... with Nunchuk", "... with Classic Controller" and "Nintendo Wii U Pro -// Controller" from SDL's Wii driver; the Linux kernel's "Nintendo Wii Remote -// Classic Controller" is read like the Classic Controller. -Kind KindForName(const char* name) { - // The kernel's Pro Controller is a plain gamepad with a built-in SDL mapping. - if (NameContains(name, "Wii Remote Pro Controller")) return Kind::NotWii; - if (NameContains(name, "Wii U Pro Controller")) return Kind::WiiUPro; - if (!NameContains(name, "Wii Remote")) return Kind::NotWii; - if (NameContains(name, "Nunchuk")) return Kind::RemoteWithNunchuk; - if (NameContains(name, "Classic Controller")) return Kind::RemoteWithClassic; - return Kind::Remote; -} - -// Kind of the SDL gamepad assigned to a game port, NotWii when empty. -Kind KindForPort(uint32_t port) { - if (port >= PAD_MAX_CONTROLLERS) return Kind::NotWii; - SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast(port)); - return KindForGamepad(gamepad); -} - -// Human-readable name of a Kind for the settings overlay. -const char* KindLabel(Kind kind) { - switch (kind) { - case Kind::Remote: return "Wii Remote"; - case Kind::RemoteWithNunchuk: return "Wii Remote + Nunchuk"; - case Kind::RemoteWithClassic: return "Wii Remote + Classic Controller"; - case Kind::WiiUPro: return "Wii U Pro Controller"; - default: return "Not a Wii controller"; - } -} - -// True for the kinds the game reads through KPAD. -static bool IsKpadKind(Kind kind) { - return kind == Kind::Remote || kind == Kind::RemoteWithNunchuk || kind == Kind::RemoteWithClassic; -} - -// Live kind of the port, or the remembered one while a swap is in flight. -// Called from the guest thread only (PADRead, KPADRead, WPADProbe and the -// overlay's Draw all run there), so the port memory needs no locking. -Kind EffectiveKind(uint32_t chan) { - if (chan >= PAD_MAX_CONTROLLERS) return Kind::NotWii; - PortMemory& memory = g_ports[chan]; - SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast(chan)); - const Kind live = KindForGamepad(gamepad); - const uint64_t now = SDL_GetTicks(); - if (live != Kind::NotWii) { - memory.lastKind = live; - memory.lastSeenMs = now; - return live; - } - if (gamepad != nullptr) { - // Another controller took the port: the remote is not coming back here. - memory.lastKind = Kind::NotWii; - return Kind::NotWii; - } - if (IsKpadKind(memory.lastKind) && memory.lastSeenMs != 0 && now - memory.lastSeenMs < kExtensionSwapGraceMs) { - return memory.lastKind; - } - return Kind::NotWii; -} - -// True when the game reads the port through KPAD (live or bridging a swap). -bool IsRemoteChannel(uint32_t chan) { - return IsKpadKind(EffectiveKind(chan)); -} - -// Marks KPAD-served ports as "no controller" in the GameCube pad statuses. -void HideRemotesFromPad(PADStatus* statuses, uint32_t count) { - for (uint32_t port = 0; port < count && port < PAD_MAX_CONTROLLERS; ++port) { - if (IsRemoteChannel(port) || - IsCarriedKernelNode(SDL_GetGamepadFromPlayerIndex(static_cast(port)))) { - statuses[port] = {}; - statuses[port].err = PAD_ERR_NO_CONTROLLER; - } - } -} - -// Neutral sample of the remembered kind, for the frames of an extension swap. -void FillGraceSample(uint32_t chan, Kind kind, KpadSample& sample) { - sample = {}; - for (int i = 0; i < 3; ++i) sample.acc[i] = g_lastAcc[chan].acc[i]; - sample.hasNunchuk = kind == Kind::RemoteWithNunchuk; - sample.hasClassic = kind == Kind::RemoteWithClassic; -} - -// The patched SDL Wii driver posts Classic Controller sticks as WPAD's own -// value times 64 (aurora-main/cmake/AuroraSDL3Patches.cmake), so WPADCLStatus -// is the axis divided back: signed, 0 at the centre, -512..511, +y up. -int16_t ClassicStickRaw(Sint16 axis, bool invert) { - const long value = invert ? -static_cast(axis) : axis; - return static_cast(std::clamp(std::lround(value / 64.0), -512L, 511L)); -} - -// The kernel driver's evdev axes span SDL's full range; scaled to WPAD's. -int16_t KernelClassicStickRaw(Sint16 axis, bool invert) { - float value = static_cast(axis) / 32767.0f; - if (invert) value = -value; - value = std::clamp(value, -1.0f, 1.0f); - return static_cast(std::clamp(std::lround(value * 512.0f), -512L, 511L)); -} - -// KPAD's default Classic Controller stick limits; the game uses the cross clamp. -constexpr float kKpadStickMin = 60.0f; -constexpr float kKpadStickMax = 308.0f; - -// KPAD cross clamp: per-axis dead zone and scale, then the unit circle. -void ClassicStickFloat(const int16_t raw[2], float out[2]) { - for (int i = 0; i < 2; ++i) { - const float magnitude = std::fabs(static_cast(raw[i])); - const float scaled = std::clamp((magnitude - kKpadStickMin) / (kKpadStickMax - kKpadStickMin), 0.0f, 1.0f); - out[i] = raw[i] < 0 ? -scaled : scaled; - } - const float length = std::sqrt(out[0] * out[0] + out[1] * out[1]); - if (length > 1.0f) { - out[0] /= length; - out[1] /= length; - } -} - -// Samples buttons, accelerometers and the extension of the remote on a port. -bool ReadKpadSample(uint32_t chan, KpadSample& sample) { - if (chan >= PAD_MAX_CONTROLLERS) { - return false; - } - SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast(chan)); - const Kind kind = KindForGamepad(gamepad); - if (!IsKpadKind(kind)) { - const Kind remembered = EffectiveKind(chan); - if (!IsKpadKind(remembered)) { - return false; - } - FillGraceSample(chan, remembered, sample); - return true; - } - SDL_Joystick* joystick = SDL_GetGamepadJoystick(gamepad); - if (joystick == nullptr) { - return false; - } - EnsureSensors(gamepad, chan); - - sample = {}; - // The kernel's evdev nodes have regular SDL mappings, so they are read as gamepads. - const bool kernel = IsKernelWiiNode(SDL_GetGamepadID(gamepad)); - const bool classicNode = kernel && IsClassicNodeName(SDL_GetGamepadName(gamepad)); - if (kernel) { - const auto mapped = [&](SDL_GamepadButton button, uint32_t bit) { - if (SDL_GetGamepadButton(gamepad, button)) sample.hold |= bit; - }; - if (!classicNode) { - mapped(SDL_GAMEPAD_BUTTON_EAST, kWpadA); - mapped(SDL_GAMEPAD_BUTTON_SOUTH, kWpadB); - mapped(SDL_GAMEPAD_BUTTON_WEST, kWpadOne); - mapped(SDL_GAMEPAD_BUTTON_NORTH, kWpadTwo); - mapped(SDL_GAMEPAD_BUTTON_START, kWpadPlus); - mapped(SDL_GAMEPAD_BUTTON_BACK, kWpadMinus); - mapped(SDL_GAMEPAD_BUTTON_GUIDE, kWpadHome); - mapped(SDL_GAMEPAD_BUTTON_DPAD_UP, kWpadUp); - mapped(SDL_GAMEPAD_BUTTON_DPAD_DOWN, kWpadDown); - mapped(SDL_GAMEPAD_BUTTON_DPAD_LEFT, kWpadLeft); - mapped(SDL_GAMEPAD_BUTTON_DPAD_RIGHT, kWpadRight); - } - } else { - const auto raw = [&](int index, uint32_t bit) { - if (SDL_GetJoystickButton(joystick, index)) sample.hold |= bit; - }; - raw(kRawA, kWpadA); - raw(kRawB, kWpadB); - raw(kRawOne, kWpadOne); - raw(kRawTwo, kWpadTwo); - raw(kRawPlus, kWpadPlus); - raw(kRawMinus, kWpadMinus); - raw(kRawHome, kWpadHome); - raw(kRawDpadUp, kWpadUp); - raw(kRawDpadDown, kWpadDown); - raw(kRawDpadLeft, kWpadLeft); - raw(kRawDpadRight, kWpadRight); - } - - float rawG[3] = {}; - const bool haveRaw = ReadSdlAccel(gamepad, SDL_SENSOR_ACCEL, rawG); - for (float& v : rawG) v /= kStandardGravity; - LastAcc& last = g_lastAcc[chan]; - const bool accepted = ReadAccelAsKpad(gamepad, chan, SDL_SENSOR_ACCEL, sample.acc); - if (accepted) { - last.valid = true; - for (int i = 0; i < 3; ++i) last.acc[i] = sample.acc[i]; - } else { - // No sample this frame or a glitched one: repeat the last good reading - // (rest pose, buttons up, until there is one). - for (int i = 0; i < 3; ++i) sample.acc[i] = last.acc[i]; - } - if (RuntimeConfigFile::WiiAccelTraceEnabled(false)) { - TraceSample(chan, sample, rawG, haveRaw, accepted); - } - - if (kind == Kind::RemoteWithClassic) { - sample.hasClassic = true; - // The driver posts the extension's buttons as joystick buttons numbered - // by SDL_GAMEPAD_BUTTON_*: a/b/x/y by position (a on the east), +/-, - // Home, the L/R clicks as shoulders, the D-pad as buttons 11-14 (never - // through SDL's gamepad mapping, which expects a hat), ZL/ZR as the - // trigger axes. - SDL_Gamepad* classic = gamepad; - if (kernel && !classicNode) { - classic = KernelSibling(gamepad, true); - if (classic == nullptr) { - classic = gamepad; - } - } - const auto cl = [&](SDL_GamepadButton button, uint32_t bit) { - if (kernel ? SDL_GetGamepadButton(classic, button) : SDL_GetJoystickButton(joystick, button)) { - sample.clHold |= bit; - } - }; - cl(SDL_GAMEPAD_BUTTON_EAST, kClA); - cl(SDL_GAMEPAD_BUTTON_SOUTH, kClB); - cl(SDL_GAMEPAD_BUTTON_NORTH, kClX); - cl(SDL_GAMEPAD_BUTTON_WEST, kClY); - cl(SDL_GAMEPAD_BUTTON_START, kClPlus); - cl(SDL_GAMEPAD_BUTTON_BACK, kClMinus); - cl(SDL_GAMEPAD_BUTTON_GUIDE, kClHome); - cl(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, kClL); - cl(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, kClR); - cl(SDL_GAMEPAD_BUTTON_DPAD_UP, kClUp); - cl(SDL_GAMEPAD_BUTTON_DPAD_DOWN, kClDown); - cl(SDL_GAMEPAD_BUTTON_DPAD_LEFT, kClLeft); - cl(SDL_GAMEPAD_BUTTON_DPAD_RIGHT, kClRight); - if (SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > 0) sample.clHold |= kClZL; - if (SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > 0) sample.clHold |= kClZR; - const Sint16 lx = SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_LEFTX); - const Sint16 ly = SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_LEFTY); - const Sint16 rx = SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_RIGHTX); - const Sint16 ry = SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_RIGHTY); - if (kernel) { - sample.clLStickRaw[0] = KernelClassicStickRaw(lx, false); - sample.clLStickRaw[1] = KernelClassicStickRaw(ly, true); - sample.clRStickRaw[0] = KernelClassicStickRaw(rx, false); - sample.clRStickRaw[1] = KernelClassicStickRaw(ry, true); - sample.clLStick[0] = std::clamp(static_cast(lx) / 32767.0f, -1.0f, 1.0f); - sample.clLStick[1] = std::clamp(-static_cast(ly) / 32767.0f, -1.0f, 1.0f); - sample.clRStick[0] = std::clamp(static_cast(rx) / 32767.0f, -1.0f, 1.0f); - sample.clRStick[1] = std::clamp(-static_cast(ry) / 32767.0f, -1.0f, 1.0f); - } else { - sample.clLStickRaw[0] = ClassicStickRaw(lx, false); - sample.clLStickRaw[1] = ClassicStickRaw(ly, true); - sample.clRStickRaw[0] = ClassicStickRaw(rx, false); - sample.clRStickRaw[1] = ClassicStickRaw(ry, true); - ClassicStickFloat(sample.clLStickRaw, sample.clLStick); - ClassicStickFloat(sample.clRStickRaw, sample.clRStick); - } - // Only the full-press click of L/R reaches SDL; report it as a full pull. - sample.clTriggerL = (sample.clHold & kClL) ? 255 : 0; - sample.clTriggerR = (sample.clHold & kClR) ? 255 : 0; - } - - if (kind == Kind::RemoteWithNunchuk) { - sample.hasNunchuk = true; - if (SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER)) sample.hold |= kWpadC; - if (SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > 0) sample.hold |= kWpadZ; - sample.stick[0] = static_cast(SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX)) / 32767.0f; - // SDL's y grows downwards; KPAD's stick y is up-positive. - sample.stick[1] = -static_cast(SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY)) / 32767.0f; - for (float& v : sample.stick) v = std::clamp(v, -1.0f, 1.0f); - LastAcc& lastNunchuk = g_lastNunchukAcc[chan]; - if (ReadAccelAsKpad(gamepad, chan, SDL_SENSOR_ACCEL_L, sample.nunchukAcc)) { - lastNunchuk.valid = true; - for (int i = 0; i < 3; ++i) lastNunchuk.acc[i] = sample.nunchukAcc[i]; - } else { - for (int i = 0; i < 3; ++i) sample.nunchukAcc[i] = lastNunchuk.acc[i]; - } - } - return true; -} - -// Corrected SDL sample and KPAD vector of the remote on a port, for the overlay. -bool ReadAccelDebug(uint32_t chan, float sdlG[3], float kpadAcc[3]) { - if (!IsRemoteChannel(chan)) { - return false; - } - SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast(chan)); - if (gamepad == nullptr) { - return false; - } - EnsureSensors(gamepad, chan); - if (!ReadAccelG(gamepad, chan, SDL_SENSOR_ACCEL, sdlG)) { - return false; - } - AccelGToKpad(sdlG, kpadAcc); - return true; -} - -// Begins collecting rest samples from the remote on `chan`. -void StartAccelCalibration(uint32_t chan) { - SDL_Gamepad* gamepad = RemoteGamepad(chan); - if (gamepad == nullptr) { - FinishAccelCalibration(chan, "No Wii Remote on this port."); - return; - } - g_calibration = {}; - g_calibration.active = true; - g_calibration.chan = chan; - g_calibration.instance = SDL_GetGamepadID(gamepad); - g_calibrationMessage[0] = '\0'; -} - -// Zeroes the stored correction of the remote on `chan`. -void ClearAccelCalibration(uint32_t chan) { - if (g_calibration.chan == chan) { - g_calibration.active = false; - } - SDL_Gamepad* gamepad = RemoteGamepad(chan); - if (gamepad == nullptr) { - SetCalibrationMessage(chan, "No Wii Remote on this port."); - return; - } - RemoteCalibration& calibration = CalibrationFor(gamepad, chan); - calibration.offset = {}; - RuntimeConfigFile::SetWiiAccelOffsetFor(calibration.key, {0.0, 0.0, 0.0}); - SetCalibrationMessage(chan, "Calibration cleared."); -} - -// Correction in effect for the remote on `chan`, for the overlay. -std::optional> ReadAccelOffset(uint32_t chan) { - SDL_Gamepad* gamepad = RemoteGamepad(chan); - if (gamepad == nullptr) { - return std::nullopt; - } - const RemoteCalibration& calibration = CalibrationFor(gamepad, chan); - return std::array{calibration.offset[0], calibration.offset[1], calibration.offset[2]}; -} - -// True while a calibration run is collecting samples from `chan`. -bool IsAccelCalibrating(uint32_t chan) { - return g_calibration.active && g_calibration.chan == chan; -} - -// Share of the calibration samples collected so far, 0..1; 0 when idle. -float AccelCalibrationProgress() { - return g_calibration.active ? static_cast(g_calibration.count) / kCalibrationSamples : 0.0f; -} - -// Outcome of the last calibration run on `chan` for the overlay, or nullptr. -const char* AccelCalibrationMessage(uint32_t chan) { - if (g_calibrationMessage[0] == '\0' || g_calibrationMessageChan != chan) { - return nullptr; - } - SDL_Gamepad* gamepad = RemoteGamepad(chan); - const SDL_JoystickID remote = gamepad != nullptr ? SDL_GetGamepadID(gamepad) : 0; - return remote == g_calibrationMessageRemote ? g_calibrationMessage : nullptr; -} - -} // namespace WiiRemoteInput diff --git a/runtime/src/wiimote/wiimote_ext_crypto.cpp b/runtime/src/wiimote/wiimote_ext_crypto.cpp new file mode 100644 index 0000000..a345f58 --- /dev/null +++ b/runtime/src/wiimote/wiimote_ext_crypto.cpp @@ -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 +#include + +namespace Wiimote::ExtCrypto { +namespace Detail { + +using SBox = std::array; + +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 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; +using Key = std::array; + +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((std::rotr(ans[0] ^ t[5], t[2]) - t[9]) ^ t[4]), + static_cast((std::rotr(ans[1] ^ t[1], t[0]) - t[5]) ^ t[7]), + static_cast((std::rotr(ans[2] ^ t[6], t[8]) - t[2]) ^ t[0]), + static_cast((std::rotr(ans[3] ^ t[4], t[7]) - t[3]) ^ t[2]), + static_cast((std::rotr(ans[4] ^ t[1], t[6]) - t[3]) ^ t[4]), + static_cast((std::rotr(ans[5] ^ t[7], t[8]) - t[5]) ^ t[9]), + }; +} + +} // namespace Detail + +std::optional FromKey(const std::array& 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(a[key[4]] ^ b[rand[3]]), static_cast(a[key[2]] ^ b[rand[5]]), + static_cast(a[key[5]] ^ b[rand[7]]), static_cast(a[key[0]] ^ b[rand[2]]), + static_cast(a[key[1]] ^ b[rand[4]]), static_cast(a[key[3]] ^ b[rand[9]]), + static_cast(a[rand[0]] ^ b[rand[6]]), static_cast(a[rand[1]] ^ b[rand[8]])}; + t.sb = {static_cast(a[key[0]] ^ b[rand[1]]), static_cast(a[key[5]] ^ b[rand[4]]), + static_cast(a[key[3]] ^ b[rand[0]]), static_cast(a[key[2]] ^ b[rand[9]]), + static_cast(a[key[4]] ^ b[rand[7]]), static_cast(a[key[1]] ^ b[rand[8]]), + static_cast(a[rand[3]] ^ b[rand[5]]), static_cast(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((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((data[i] ^ sb[address % 8]) + ft[address % 8]); + } +} + +} // namespace Wiimote::ExtCrypto diff --git a/runtime/src/wiimote/wiimote_extension.cpp b/runtime/src/wiimote/wiimote_extension.cpp new file mode 100644 index 0000000..bec0d67 --- /dev/null +++ b/runtime/src/wiimote/wiimote_extension.cpp @@ -0,0 +1,145 @@ +#include "wiimote_extension.h" + +#include + +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 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 || (report[1] & 0x04) == 0) return; + const uint32_t address = (report[2] << 16) | (report[3] << 8) | report[4]; + if (report[0] == kReportReadMemory) { + m_readAddress = address; + return; + } + if (report[0] != kReportWriteMemory || !InExtensionSpace(address)) return; + const size_t length = std::min(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((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(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& 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 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(((rx >> 3) & 0x03) << 6 | (lx & 0x3F)); + plain[1] = static_cast(((rx >> 1) & 0x03) << 6 | (ly & 0x3F)); + plain[2] = static_cast((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 diff --git a/runtime/src/wiimote/wiimote_host.cpp b/runtime/src/wiimote/wiimote_host.cpp new file mode 100644 index 0000000..642bb8c --- /dev/null +++ b/runtime/src/wiimote/wiimote_host.cpp @@ -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 +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" bool PAD_HLE_RumbleEnabled(); + +namespace Wiimote { +namespace HostDetail { + +using Clock = std::chrono::steady_clock; +using Report = std::vector; + +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(((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 alive{true}; + std::atomic phase{Phase::Waiting}; + std::atomic wake{true}; + std::atomic battery{-1}; + std::atomic extension{-1}; // -1 none, else the extension id byte + std::atomic extensionAttached{false}; + // Rumble bit of the game's last report, before the vibration setting. + std::atomic gameRumble{false}; + + std::mutex writeMutex; + std::condition_variable writeCv; + std::deque writes; + std::mutex inputMutex; + std::deque 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& 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 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, Phase phase); + void PowerOff(const std::shared_ptr& remote); + static void ReadLoop(std::shared_ptr remote); + static void WriteLoop(std::shared_ptr remote); + static void Queue(Remote& remote, Report report); + + mutable std::mutex m_mutex; + std::array, Bluetooth::kRemoteSlots> m_slots; + std::array m_links{}; + std::vector> m_waiting; + std::map m_lastSlot; + std::map m_cooldown; + std::set m_refusedLogged; + // Remotes the OS knows but that were off when last seen, by path. + std::map m_dormant; + Clock::time_point m_lastEnumerate{}; + uint32_t m_deviceChanges = 0; + + std::atomic 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 m_cancelDiscovery{false}; + std::atomic m_searching{false}; + std::atomic 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 Manager::At(size_t slot) const { + std::lock_guard 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& report) { + const auto remote = At(slot); + if (!remote) return false; + { + std::lock_guard 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(~(kButtonMask >> 8)); + report[2] &= static_cast(~kButtonMask); + } + remote->extensionState.Neutralize(report); + } + return true; +} + +void Manager::Queue(Remote& remote, Report report) { + { + std::lock_guard 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(rumble ? 0x01 : 0x00)}); + return; + } + Queue(*remote, Report(report, report + size)); +} + +void Manager::OnLinkStateChanged(size_t slot, Bluetooth::LinkState state) { + std::shared_ptr remote; + { + std::lock_guard 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 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->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, Phase phase) { + remote->phase = phase; + remote->gameRumble = false; + if (phase != Phase::Linked) { + std::lock_guard lock(remote->writeMutex); + remote->writes.clear(); + } + remote->writeCv.notify_one(); +} + +void Manager::OnGuestInquiry(bool active) { + if (active) { + std::lock_guard lock(m_discoveryMutex); + m_guestSearching = true; + } else { + std::lock_guard 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> remotes; + { + std::lock_guard 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 lock(m_scanMutex); + m_scanCv.wait_for(lock, kScanInterval, [this] { return !m_running; }); + } +} + +void Manager::Reap() { + std::vector> dead; + { + std::lock_guard 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 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 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 open; + { + std::lock_guard 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 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->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 lock(m_mutex); + m_waiting.push_back(remote); + } + AssignWaiting(); +} + +void Manager::AssignWaiting() { + std::lock_guard lock(m_mutex); + for (auto it = m_waiting.begin(); it != m_waiting.end();) { + const std::shared_ptr& 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) { + std::array 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(read)); + if (size == 0) continue; + Report report(buffer.begin(), buffer.begin() + static_cast(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 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) { + 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 batch; + { + std::unique_lock 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(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(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 lock(m_discoveryMutex); + const auto now = Clock::now(); + bool anyRemote = false; + { + std::lock_guard 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 lock(m_discoveryMutex); + if (!status.empty()) m_pairingStatus = status; + if (connected > 0) { + m_scanCv.notify_all(); + } + } + if (automatic && connected == 0) { + std::unique_lock 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; + { + std::lock_guard 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 lock(m_discoveryMutex); + info.status = m_pairingStatus; + return info; +} + +void Manager::StartPairing() { + { + std::lock_guard 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 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 diff --git a/runtime/src/wiimote/wiimote_platform_unix.cpp b/runtime/src/wiimote/wiimote_platform_unix.cpp new file mode 100644 index 0000000..970ed13 --- /dev/null +++ b/runtime/src/wiimote/wiimote_platform_unix.cpp @@ -0,0 +1,496 @@ +#if !defined(_WIN32) + +#include "wiimote_platform.h" + +#include "runtime_log.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__linux__) +#include +#include + +#include +#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(*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(c))) hex += static_cast(std::tolower(static_cast(c))); + } + return hex; +} + +std::mutex g_noticeMutex; +std::string g_notice; + +void SetNotice(const std::string& notice) { + std::lock_guard 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& 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 + bool Bind(Fn& fn, const char* symbol) { + fn = reinterpret_cast(dlsym(m_handle, symbol)); + return fn != nullptr; + } + + template + 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) { + 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& 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> 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 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 lock(g_busMutex, std::try_to_lock); + if (!lock.owns_lock()) return cached; + checked = now; + std::string adapter; + std::vector 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& cancel, std::string& status) { +#if defined(__linux__) + std::lock_guard lock(g_busMutex); + BlueZ* bus = Bus(); + std::string adapter; + std::vector 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 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 lock(g_busMutex, std::chrono::milliseconds(kShortTimeoutMs)); + if (!lock.owns_lock()) return false; + BlueZ* bus = Bus(); + std::string adapter; + std::vector 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 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 diff --git a/runtime/src/wiimote/wiimote_platform_windows.cpp b/runtime/src/wiimote/wiimote_platform_windows.cpp new file mode 100644 index 0000000..883294e --- /dev/null +++ b/runtime/src/wiimote/wiimote_platform_windows.cpp @@ -0,0 +1,300 @@ +#if defined(_WIN32) + +#include "wiimote_platform.h" + +#include "runtime_log.h" + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +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(¶ms, &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& All() const { return m_radios; } + +private: + std::vector 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(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(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> g_recent; + +bool RecentlyPaired(const std::string& address) { + std::lock_guard 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 lock(g_recentMutex); + g_recent.emplace_back(address, std::chrono::steady_clock::now()); +} + +std::vector Devices(const Radio& radio, bool inquiry) { + std::vector 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(¶ms, &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 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& 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 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(&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(*c))) hex += static_cast(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(*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(*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 diff --git a/runtime/tests/bluetooth_controller_tests.cpp b/runtime/tests/bluetooth_controller_tests.cpp new file mode 100644 index 0000000..99e05a2 --- /dev/null +++ b/runtime/tests/bluetooth_controller_tests.cpp @@ -0,0 +1,368 @@ +#include "bluetooth/bt_controller.h" +#include "bluetooth/bt_sdp.h" + +#include +#include +#include +#include +#include + +namespace { + +using Bytes = std::vector; +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 attached{}; + std::array wake{}; + std::array, Bluetooth::kRemoteSlots> input; + std::vector> output; + std::array states{}; + std::vector 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(b[i] | (b[i + 1] << 8)); +} + +void PutLe16(Bytes& b, uint16_t v) { + b.push_back(static_cast(v)); + b.push_back(static_cast(v >> 8)); +} + +Bytes Command(uint16_t opcode, Bytes params) { + Bytes packet; + PutLe16(packet, opcode); + packet.push_back(static_cast(params.size())); + packet.insert(packet.end(), params.begin(), params.end()); + return packet; +} + +struct Guest { + Controller& controller; + uint64_t now = 1000; + + std::vector 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 Events() { + std::vector events; + Bytes event; + while (controller.PopEvent(event)) events.push_back(event); + return events; + } + std::vector Acl() { + std::vector 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(handle | 0x2000)); + PutLe16(packet, static_cast(payload.size() + 4)); + PutLe16(packet, static_cast(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(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 Signals(const std::vector& acl) { + std::vector 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(i + 4), + packet.begin() + static_cast(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((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(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((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; +} diff --git a/translator/src/Translator.Core/CodeGen/CxxLinearCodeGenerator.EmitInstruction.cs b/translator/src/Translator.Core/CodeGen/CxxLinearCodeGenerator.EmitInstruction.cs index b4874b1..8c35b11 100644 --- a/translator/src/Translator.Core/CodeGen/CxxLinearCodeGenerator.EmitInstruction.cs +++ b/translator/src/Translator.Core/CodeGen/CxxLinearCodeGenerator.EmitInstruction.cs @@ -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)});"); diff --git a/translator/src/Translator.Core/CodeGen/CxxLinearCodeGenerator.FlatGuestMemory.cs b/translator/src/Translator.Core/CodeGen/CxxLinearCodeGenerator.FlatGuestMemory.cs index ed2317f..72b2124 100644 --- a/translator/src/Translator.Core/CodeGen/CxxLinearCodeGenerator.FlatGuestMemory.cs +++ b/translator/src/Translator.Core/CodeGen/CxxLinearCodeGenerator.FlatGuestMemory.cs @@ -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); diff --git a/translator/tests/Translator.Tests/CodeGenTests.cs b/translator/tests/Translator.Tests/CodeGenTests.cs index 2051006..d52f402 100644 --- a/translator/tests/Translator.Tests/CodeGenTests.cs +++ b/translator/tests/Translator.Tests/CodeGenTests.cs @@ -480,6 +480,41 @@ public class CodeGenTests Assert.Contains("MemoryInline::FlatWrite8((ctx->gpr[6] + -32768), static_cast(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 + { + ["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() {