From 9a87d2761226bd089d57011bf58a6dd8326df58d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Lam?= Date: Sun, 29 Nov 2020 17:44:17 +0100 Subject: [PATCH] IOS/WD: Implement more parts of the interface This commit implements the following commands: * open * close * GetMode * SetLinkState (used to actually trigger scanning) * GetLinkState (used to check if the driver is in the expected state) * GetInfo * RecvFrame and RecvNotification (stubbed) * Disassociate (stubbed) GetInfo was already implemented, but the structure wasn't initialized correctly so the info was being rejected by official titles. That has also been fixed in this commit. Some of the checks may seem unimportant but official titles actually require WD to return error codes... Failing to do so can cause hangs and softlocks when DS communications are shut down. This minimal implementation is enough to satisfy the Mii channel and all other DS games, except Tales of Graces (https://dolp.in/i11977) which still softlocks because it probably requires us to actually feed it frame data. --- Source/Core/Core/Analytics.cpp | 5 +- Source/Core/Core/Analytics.h | 5 + Source/Core/Core/IOS/Network/WD/Command.cpp | 339 ++++++++++++++++++-- Source/Core/Core/IOS/Network/WD/Command.h | 87 ++++- Source/Core/Core/State.cpp | 2 +- 5 files changed, 403 insertions(+), 35 deletions(-) diff --git a/Source/Core/Core/Analytics.cpp b/Source/Core/Core/Analytics.cpp index e46dcd2832..2b8e65be1d 100644 --- a/Source/Core/Core/Analytics.cpp +++ b/Source/Core/Core/Analytics.cpp @@ -133,7 +133,7 @@ void DolphinAnalytics::ReportGameStart() } // Keep in sync with enum class GameQuirk definition. -constexpr std::array GAME_QUIRKS_NAMES{"icache-matters", +constexpr std::array GAME_QUIRKS_NAMES{"icache-matters", "directly-reads-wiimote-input", "uses-DVDLowStopLaser", "uses-DVDLowOffset", @@ -144,7 +144,8 @@ constexpr std::array GAME_QUIRKS_NAMES{"icache-matters", "uses-different-partition-command", "uses-di-interrupt-command", "mismatched-gpu-texgens-between-xf-and-bp", - "mismatched-gpu-colors-between-xf-and-bp"}; + "mismatched-gpu-colors-between-xf-and-bp", + "uses-uncommon-wd-mode"}; static_assert(GAME_QUIRKS_NAMES.size() == static_cast(GameQuirk::COUNT), "Game quirks names and enum definition are out of sync."); diff --git a/Source/Core/Core/Analytics.h b/Source/Core/Core/Analytics.h index 94f7eb5a78..1d9ac59053 100644 --- a/Source/Core/Core/Analytics.h +++ b/Source/Core/Core/Analytics.h @@ -54,6 +54,11 @@ enum class GameQuirk MISMATCHED_GPU_TEXGENS_BETWEEN_XF_AND_BP, MISMATCHED_GPU_COLORS_BETWEEN_XF_AND_BP, + // The WD module can be configured to operate in six different modes. + // In practice, only mode 1 (DS communications) and mode 3 (AOSS access point scanning) + // are used by games and the system menu respectively. + USES_UNCOMMON_WD_MODE, + COUNT, }; diff --git a/Source/Core/Core/IOS/Network/WD/Command.cpp b/Source/Core/Core/IOS/Network/WD/Command.cpp index 363e49b091..4f22cfaa1b 100644 --- a/Source/Core/Core/IOS/Network/WD/Command.cpp +++ b/Source/Core/Core/IOS/Network/WD/Command.cpp @@ -4,33 +4,330 @@ #include "Core/IOS/Network/WD/Command.h" -#include #include #include +#include "Common/BitSet.h" #include "Common/CommonTypes.h" #include "Common/Logging/Log.h" #include "Common/Network.h" #include "Common/Swap.h" - +#include "Core/Analytics.h" #include "Core/HW/Memmap.h" #include "Core/IOS/Network/MACUtils.h" namespace IOS::HLE::Device { -NetWDCommand::NetWDCommand(Kernel& ios, const std::string& device_name) : Device(ios, device_name) +namespace { +// clang-format off +// Channel: FEDC BA98 7654 3210 +constexpr u16 LegalChannelMask = 0b0111'1111'1111'1110u; +constexpr u16 LegalNitroChannelMask = 0b0011'1111'1111'1110u; +// clang-format on + +u16 SelectWifiChannel(u16 enabled_channels_mask, u16 current_channel) +{ + const Common::BitSet enabled_channels{enabled_channels_mask & LegalChannelMask}; + u16 next_channel = current_channel; + for (int i = 0; i < 16; ++i) + { + next_channel = (next_channel + 3) % 16; + if (enabled_channels[next_channel]) + return next_channel; + } + // This does not make a lot of sense, but it is what WD does. + return u16(enabled_channels[next_channel]); +} + +u16 MakeNitroAllowedChannelMask(u16 enabled_channels_mask, u16 nitro_mask) +{ + nitro_mask &= LegalNitroChannelMask; + // TODO: WD's version of this function has some complicated logic to determine the actual mask. + return enabled_channels_mask & nitro_mask; +} +} // namespace + +NetWDCommand::Status NetWDCommand::GetTargetStatusForMode(WD::Mode mode) +{ + switch (mode) + { + case WD::Mode::DSCommunications: + return Status::ScanningForDS; + case WD::Mode::AOSSAccessPointScan: + return Status::ScanningForAOSSAccessPoint; + default: + return Status::Idle; + } +} + +NetWDCommand::NetWDCommand(Kernel& ios, const std::string& device_name) : Device(ios, device_name) +{ + // TODO: use the MPCH setting in setting.txt to determine this value. + m_nitro_enabled_channels = LegalNitroChannelMask; + + // TODO: Set the version string here. This is exposed to the PPC. + m_info.mac = IOS::Net::GetMACAddress(); + m_info.enabled_channels = 0xfffe; + m_info.channel = SelectWifiChannel(m_info.enabled_channels, 0); + // The country code is supposed to be null terminated as it is logged with printf in WD. + std::strncpy(m_info.country_code.data(), "US", m_info.country_code.size()); + m_info.nitro_allowed_channels = + MakeNitroAllowedChannelMask(m_info.enabled_channels, m_nitro_enabled_channels); + m_info.initialised = true; +} + +void NetWDCommand::Update() +{ + Device::Update(); + ProcessRecvRequests(); + HandleStateChange(); +} + +void NetWDCommand::ProcessRecvRequests() +{ + // Because we currently do not actually emulate the wireless driver, we have no frames + // and no notification data that could be used to reply to requests. + // Therefore, requests are left pending to simulate the situation where there is nothing to send. + + // All pending requests must still be processed when the handle to the resource manager is closed. + const bool force_process = m_clear_all_requests.TestAndClear(); + + const auto process_queue = [&](std::deque& queue) { + if (!force_process) + return; + + while (!queue.empty()) + { + const auto request = queue.front(); + s32 result; + + // If the resource manager handle is closed while processing a request, + // InvalidFd is returned. + if (m_ipc_owner_fd < 0) + { + result = s32(ResultCode::InvalidFd); + } + else + { + // TODO: Frame/notification data would be copied here. + // And result would be set to the data length or to an error code. + result = 0; + } + + INFO_LOG_FMT(IOS_NET, "Processed request {:08x} (result {:08x})", request, result); + m_ios.EnqueueIPCReply(Request{request}, result); + queue.pop_front(); + } + }; + + process_queue(m_recv_notification_requests); + process_queue(m_recv_frame_requests); +} + +void NetWDCommand::HandleStateChange() +{ + const auto status = m_status; + const auto target_status = m_target_status; + + if (status == target_status) + return; + + INFO_LOG_FMT(IOS_NET, "{}: Handling status change ({} -> {})", __func__, status, target_status); + + switch (status) + { + case Status::Idle: + switch (target_status) + { + case Status::ScanningForAOSSAccessPoint: + // This is supposed to reset the driver first by going into another state. + // However, we can worry about that once we actually emulate WL. + m_status = Status::ScanningForAOSSAccessPoint; + break; + case Status::ScanningForDS: + // This is supposed to set a bunch of Wi-Fi driver parameters and initiate a scan. + m_status = Status::ScanningForDS; + break; + case Status::Idle: + break; + } + break; + + case Status::ScanningForDS: + m_status = Status::Idle; + break; + + case Status::ScanningForAOSSAccessPoint: + // We are supposed to reset the driver by going into a reset state. + // However, we can worry about that once we actually emulate WL. + break; + } + + INFO_LOG_FMT(IOS_NET, "{}: done (status: {} -> {}, target was {})", __func__, status, m_status, + target_status); +} + +void NetWDCommand::DoState(PointerWrap& p) +{ + Device::DoState(p); + p.Do(m_ipc_owner_fd); + p.Do(m_mode); + p.Do(m_buffer_flags); + p.Do(m_status); + p.Do(m_target_status); + p.Do(m_nitro_enabled_channels); + p.Do(m_info); + p.Do(m_recv_frame_requests); + p.Do(m_recv_notification_requests); +} + +IPCCommandResult NetWDCommand::Open(const OpenRequest& request) +{ + if (m_ipc_owner_fd < 0) + { + const auto flags = u32(request.flags); + const auto mode = WD::Mode(flags & 0xFFFF); + const auto buffer_flags = flags & 0x7FFF0000; + INFO_LOG_FMT(IOS_NET, "Opening with mode={} buffer_flags={:08x}", mode, buffer_flags); + + // We don't support anything other than mode 1 and mode 3 at the moment. + if (mode != WD::Mode::DSCommunications && mode != WD::Mode::AOSSAccessPointScan) + { + ERROR_LOG_FMT(IOS_NET, "Unsupported WD operating mode: {}", mode); + DolphinAnalytics::Instance().ReportGameQuirk(GameQuirk::USES_UNCOMMON_WD_MODE); + return GetDefaultReply(s32(ResultCode::UnavailableCommand)); + } + + if (m_target_status == Status::Idle && mode <= WD::Mode::Unknown6) + { + m_mode = mode; + m_ipc_owner_fd = request.fd; + m_buffer_flags = buffer_flags; + } + } + + INFO_LOG_FMT(IOS_NET, "Opened"); + return Device::Open(request); +} + +IPCCommandResult NetWDCommand::Close(u32 fd) +{ + if (m_ipc_owner_fd < 0 || fd != u32(m_ipc_owner_fd)) + { + ERROR_LOG_FMT(IOS_NET, "Invalid close attempt."); + return GetDefaultReply(u32(ResultCode::InvalidFd)); + } + + INFO_LOG_FMT(IOS_NET, "Closing and resetting status to Idle"); + m_target_status = m_status = Status::Idle; + + m_ipc_owner_fd = -1; + m_clear_all_requests.Set(); + return Device::Close(fd); +} + +IPCCommandResult NetWDCommand::SetLinkState(const IOCtlVRequest& request) +{ + const auto* vector = request.GetVector(0); + if (!vector || vector->address == 0) + return GetDefaultReply(u32(ResultCode::IllegalParameter)); + + const u32 state = Memory::Read_U32(vector->address); + INFO_LOG_FMT(IOS_NET, "WD_SetLinkState called (state={}, mode={})", state, m_mode); + + if (state == 0) + { + if (!WD::IsValidMode(m_mode)) + return GetDefaultReply(u32(ResultCode::UnavailableCommand)); + + INFO_LOG_FMT(IOS_NET, "WD_SetLinkState: setting target status to 1 (Idle)"); + m_target_status = Status::Idle; + } + else + { + if (state != 1) + return GetDefaultReply(u32(ResultCode::IllegalParameter)); + + if (!WD::IsValidMode(m_mode)) + return GetDefaultReply(u32(ResultCode::UnavailableCommand)); + + const auto target_status = GetTargetStatusForMode(m_mode); + if (m_status != target_status && m_info.enabled_channels == 0) + return GetDefaultReply(u32(ResultCode::UnavailableCommand)); + + INFO_LOG_FMT(IOS_NET, "WD_SetLinkState: setting target status to {}", target_status); + m_target_status = target_status; + } + + return GetDefaultReply(IPC_SUCCESS); +} + +IPCCommandResult NetWDCommand::GetLinkState(const IOCtlVRequest& request) const +{ + INFO_LOG_FMT(IOS_NET, "WD_GetLinkState called (status={}, mode={})", m_status, m_mode); + if (!WD::IsValidMode(m_mode)) + return GetDefaultReply(u32(ResultCode::UnavailableCommand)); + + // Contrary to what the name of the ioctl suggests, this returns a boolean, not the current state. + return GetDefaultReply(u32(m_status == GetTargetStatusForMode(m_mode))); +} + +IPCCommandResult NetWDCommand::Disassociate(const IOCtlVRequest& request) +{ + const auto* vector = request.GetVector(0); + if (!vector || vector->address == 0) + return GetDefaultReply(u32(ResultCode::IllegalParameter)); + + Common::MACAddress mac; + Memory::CopyFromEmu(mac.data(), vector->address, mac.size()); + + INFO_LOG_FMT(IOS_NET, "WD_Disassociate: MAC {}", Common::MacAddressToString(mac)); + + if (m_mode != WD::Mode::DSCommunications && m_mode != WD::Mode::Unknown5 && + m_mode != WD::Mode::Unknown6) + { + ERROR_LOG_FMT(IOS_NET, "WD_Disassociate: cannot disassociate in mode {}", m_mode); + return GetDefaultReply(u32(ResultCode::UnavailableCommand)); + } + + const auto target_status = GetTargetStatusForMode(m_mode); + if (m_status != target_status) + { + ERROR_LOG_FMT(IOS_NET, "WD_Disassociate: cannot disassociate in status {} (target {})", + m_status, target_status); + return GetDefaultReply(u32(ResultCode::UnavailableCommand)); + } + + // TODO: Check the input MAC address and only return 0x80008001 if it is unknown. + return GetDefaultReply(u32(ResultCode::IllegalParameter)); +} + +IPCCommandResult NetWDCommand::GetInfo(const IOCtlVRequest& request) const +{ + const auto* vector = request.GetVector(0); + if (!vector || vector->address == 0) + return GetDefaultReply(u32(ResultCode::IllegalParameter)); + + Memory::CopyToEmu(vector->address, &m_info, sizeof(m_info)); + return GetDefaultReply(IPC_SUCCESS); } -// This is just for debugging / playing around. -// There really is no reason to implement wd unless we can bend it such that -// we can talk to the DS. IPCCommandResult NetWDCommand::IOCtlV(const IOCtlVRequest& request) { - s32 return_value = IPC_SUCCESS; - switch (request.request) { + case IOCTLV_WD_INVALID: + return GetDefaultReply(u32(ResultCode::UnavailableCommand)); + case IOCTLV_WD_GET_MODE: + return GetDefaultReply(s32(m_mode)); + case IOCTLV_WD_SET_LINKSTATE: + return SetLinkState(request); + case IOCTLV_WD_GET_LINKSTATE: + return GetLinkState(request); + case IOCTLV_WD_DISASSOC: + return Disassociate(request); + case IOCTLV_WD_SCAN: { // Gives parameters detailing type of scan and what to match @@ -59,25 +356,19 @@ IPCCommandResult NetWDCommand::IOCtlV(const IOCtlVRequest& request) break; case IOCTLV_WD_GET_INFO: - { - Info* info = (Info*)Memory::GetPointer(request.io_vectors.at(0).address); - memset(info, 0, sizeof(Info)); - // Probably used to disallow certain channels? - memcpy(info->country, "US", 2); - info->ntr_allowed_channels = Common::swap16(0xfffe); + return GetInfo(request); - const Common::MACAddress address = IOS::Net::GetMACAddress(); - std::copy(address.begin(), address.end(), info->mac); - } - break; + case IOCTLV_WD_RECV_FRAME: + m_recv_frame_requests.emplace_back(request.address); + return GetNoReply(); + + case IOCTLV_WD_RECV_NOTIFICATION: + m_recv_notification_requests.emplace_back(request.address); + return GetNoReply(); - case IOCTLV_WD_GET_MODE: - case IOCTLV_WD_SET_LINKSTATE: - case IOCTLV_WD_GET_LINKSTATE: case IOCTLV_WD_SET_CONFIG: case IOCTLV_WD_GET_CONFIG: case IOCTLV_WD_CHANGE_BEACON: - case IOCTLV_WD_DISASSOC: case IOCTLV_WD_MP_SEND_FRAME: case IOCTLV_WD_SEND_FRAME: case IOCTLV_WD_CALL_WL: @@ -85,12 +376,10 @@ IPCCommandResult NetWDCommand::IOCtlV(const IOCtlVRequest& request) case IOCTLV_WD_GET_LASTERROR: case IOCTLV_WD_CHANGE_GAMEINFO: case IOCTLV_WD_CHANGE_VTSF: - case IOCTLV_WD_RECV_FRAME: - case IOCTLV_WD_RECV_NOTIFICATION: default: request.Dump(GetDeviceName(), Common::Log::IOS_NET, Common::Log::LINFO); } - return GetDefaultReply(return_value); + return GetDefaultReply(IPC_SUCCESS); } } // namespace IOS::HLE::Device diff --git a/Source/Core/Core/IOS/Network/WD/Command.h b/Source/Core/Core/IOS/Network/WD/Command.h index c23c42f21a..78d294036e 100644 --- a/Source/Core/Core/IOS/Network/WD/Command.h +++ b/Source/Core/Core/IOS/Network/WD/Command.h @@ -4,23 +4,65 @@ #pragma once +#include #include #include "Common/CommonTypes.h" +#include "Common/Flag.h" +#include "Common/Network.h" +#include "Common/Swap.h" #include "Core/IOS/Device.h" +namespace IOS::HLE::WD +{ +// Values 2, 4, 5, 6 exist as well but are not known to be used by games, the Mii Channel +// or the system menu. +enum class Mode +{ + NotInitialized = 0, + // Used by games to broadcast DS programs or to communicate with a DS more generally. + DSCommunications = 1, + Unknown2 = 2, + // AOSS (https://en.wikipedia.org/wiki/AOSS) is a WPS-like feature. + // This is only known to be used by the system menu. + AOSSAccessPointScan = 3, + Unknown4 = 4, + Unknown5 = 5, + Unknown6 = 6, +}; + +constexpr bool IsValidMode(Mode mode) +{ + return mode >= Mode::DSCommunications && mode <= Mode::Unknown6; +} +} // namespace IOS::HLE::WD + namespace IOS::HLE::Device { class NetWDCommand : public Device { public: + enum class ResultCode : u32 + { + InvalidFd = 0x80008000, + IllegalParameter = 0x80008001, + UnavailableCommand = 0x80008002, + DriverError = 0x80008003, + }; + NetWDCommand(Kernel& ios, const std::string& device_name); + IPCCommandResult Open(const OpenRequest& request) override; + IPCCommandResult Close(u32 fd) override; IPCCommandResult IOCtlV(const IOCtlVRequest& request) override; + void Update() override; + bool IsOpened() const override { return true; } + void DoState(PointerWrap& p) override; private: enum { + IOCTLV_WD_INVALID = 0x1000, IOCTLV_WD_GET_MODE = 0x1001, // WD_GetMode IOCTLV_WD_SET_LINKSTATE = 0x1002, // WD_SetLinkState IOCTLV_WD_GET_LINKSTATE = 0x1003, // WD_GetLinkState @@ -89,14 +131,45 @@ private: struct Info { - u8 mac[6]; - u16 ntr_allowed_channels; - u16 unk8; - char country[2]; - u32 unkc; - char wlversion[0x50]; - u8 unk[0x30]; + Common::MACAddress mac{}; + Common::BigEndianValue enabled_channels{}; + Common::BigEndianValue nitro_allowed_channels{}; + std::array country_code{}; + u8 channel{}; + bool initialised{}; + std::array wl_version{}; }; + static_assert(sizeof(Info) == 0x90); #pragma pack(pop) + + enum class Status + { + Idle, + ScanningForAOSSAccessPoint, + ScanningForDS, + }; + + void ProcessRecvRequests(); + void HandleStateChange(); + static Status GetTargetStatusForMode(WD::Mode mode); + + IPCCommandResult SetLinkState(const IOCtlVRequest& request); + IPCCommandResult GetLinkState(const IOCtlVRequest& request) const; + IPCCommandResult Disassociate(const IOCtlVRequest& request); + IPCCommandResult GetInfo(const IOCtlVRequest& request) const; + + s32 m_ipc_owner_fd = -1; + WD::Mode m_mode = WD::Mode::NotInitialized; + u32 m_buffer_flags{}; + + Status m_status = Status::Idle; + Status m_target_status = Status::Idle; + + u16 m_nitro_enabled_channels{}; + Info m_info; + + Common::Flag m_clear_all_requests; + std::deque m_recv_frame_requests; + std::deque m_recv_notification_requests; }; } // namespace IOS::HLE::Device diff --git a/Source/Core/Core/State.cpp b/Source/Core/Core/State.cpp index 31802ca471..ead28c5c0c 100644 --- a/Source/Core/Core/State.cpp +++ b/Source/Core/Core/State.cpp @@ -74,7 +74,7 @@ static Common::Event g_compressAndDumpStateSyncEvent; static std::thread g_save_thread; // Don't forget to increase this after doing changes on the savestate system -constexpr u32 STATE_VERSION = 127; // Last changed in PR 9300 (temp) +constexpr u32 STATE_VERSION = 127; // Last changed in PR 9300 // Maps savestate versions to Dolphin versions. // Versions after 42 don't need to be added to this list,