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,