@@ -4,33 +4,330 @@

#include "Core/IOS/Network/WD/Command.h"

#include <algorithm>
#include <cstring>
#include <string>

#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
{
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<u16> 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;
}

// 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)
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<u32>& 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
{
s32 return_value = IPC_SUCCESS;
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);
}

IPCCommandResult NetWDCommand::IOCtlV(const IOCtlVRequest& request)
{
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,38 +356,31 @@ 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);

const Common::MACAddress address = IOS::Net::GetMACAddress();
std::copy(address.begin(), address.end(), info->mac);
}
break;
return GetInfo(request);

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:
case IOCTLV_WD_MEASURE_CHANNEL:
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);
DolphinAnalytics::Instance().ReportGameQuirk(GameQuirk::USES_WD_UNIMPLEMENTED_IOCTL);
request.Dump(GetDeviceName(), Common::Log::IOS_NET, Common::Log::LWARNING);
}

return GetDefaultReply(return_value);
return GetDefaultReply(IPC_SUCCESS);
}
} // namespace IOS::HLE::Device
@@ -4,23 +4,65 @@

#pragma once

#include <deque>
#include <string>

#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 @@ class NetWDCommand : public Device

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<u16> enabled_channels{};
Common::BigEndianValue<u16> nitro_allowed_channels{};
std::array<char, 4> country_code{};
u8 channel{};
bool initialised{};
std::array<char, 0x80> 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<u32> m_recv_frame_requests;
std::deque<u32> m_recv_notification_requests;
};
} // namespace IOS::HLE::Device
@@ -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 = 126; // Last changed in PR 9348
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,