Implement GC modem adapter

This implements the GameCube modem adapter. This implementation is stable but not perfect; it drops frames if the receive FIFO length is exceeded. This is probably due to the unimplemented interrupt mentioned in the comments. If the tapserver end of the connection is aware of this limitation, it's easily circumvented by lowering the MTU of the link, but ideally this wouldn't be necessary.

This has been tested with a couple of different versions of Phantasy Star Online, including Episodes 1 & 2 Trial Edition. The Trial Edition is the only version of the game that supports the Modem Adapter and not the Broadband Adapter, which is what made this commit necessary in the first place.
This commit is contained in:
Martin Michelsen 2023-12-02 23:37:28 -08:00
parent 083116a89c
commit 02deaa6748
14 changed files with 954 additions and 7 deletions

View File

@ -29,6 +29,12 @@ enum class StringSetting(
"BBA_TAPSERVER_DESTINATION",
"/tmp/dolphin-tap"
),
MAIN_MODEM_TAPSERVER_DESTINATION(
Settings.FILE_DOLPHIN,
Settings.SECTION_INI_CORE,
"MODEM_TAPSERVER_DESTINATION",
"/tmp/dolphin-modem-tap"
),
MAIN_CUSTOM_RTC_VALUE(
Settings.FILE_DOLPHIN,
Settings.SECTION_INI_CORE,

View File

@ -1121,6 +1121,16 @@ class SettingsFragmentPresenter(
R.string.bba_builtin_dns_description
)
)
} else if (serialPort1Type == 13) {
// Modem Adapter (tapserver)
sl.add(
InputStringSetting(
context,
StringSetting.MAIN_MODEM_TAPSERVER_DESTINATION,
R.string.modem_tapserver_destination,
R.string.modem_tapserver_destination_description
)
)
}
}

View File

@ -135,6 +135,8 @@
<string name="xlink_kai_bba_ip_description">IP address or hostname of device running the XLink Kai client</string>
<string name="bba_tapserver_destination">Tapserver destination</string>
<string name="bba_tapserver_destination_description">Enter the socket path or netloc (address:port) of the tapserver instance</string>
<string name="modem_tapserver_destination">Tapserver destination</string>
<string name="modem_tapserver_destination_description">Enter the socket path or netloc (address:port) of the tapserver instance</string>
<string name="bba_builtin_dns">DNS Server</string>
<string name="bba_builtin_dns_description">Use 8.8.8.8 for normal DNS, else enter your custom one</string>

View File

@ -193,6 +193,7 @@ add_library(core
HW/EXI/BBA/XLINK_KAI_BBA.cpp
HW/EXI/BBA/BuiltIn.cpp
HW/EXI/BBA/BuiltIn.h
HW/EXI/Modem/TAPServer.cpp
HW/EXI/EXI_Channel.cpp
HW/EXI/EXI_Channel.h
HW/EXI/EXI_Device.cpp
@ -213,6 +214,8 @@ add_library(core
HW/EXI/EXI_DeviceMemoryCard.h
HW/EXI/EXI_DeviceMic.cpp
HW/EXI/EXI_DeviceMic.h
HW/EXI/EXI_DeviceModem.cpp
HW/EXI/EXI_DeviceModem.h
HW/EXI/EXI.cpp
HW/EXI/EXI.h
HW/GBAPad.cpp

View File

@ -139,6 +139,8 @@ const Info<std::string> MAIN_BBA_BUILTIN_DNS{{System::Main, "Core", "BBA_BUILTIN
"3.18.217.27"};
const Info<std::string> MAIN_BBA_TAPSERVER_DESTINATION{
{System::Main, "Core", "BBA_TAPSERVER_DESTINATION"}, "/tmp/dolphin-tap"};
const Info<std::string> MAIN_MODEM_TAPSERVER_DESTINATION{
{System::Main, "Core", "MODEM_TAPSERVER_DESTINATION"}, "/tmp/dolphin-modem-tap"};
const Info<std::string> MAIN_BBA_BUILTIN_IP{{System::Main, "Core", "BBA_BUILTIN_IP"}, ""};
const Info<SerialInterface::SIDevices>& GetInfoForSIDevice(int channel)

View File

@ -97,6 +97,7 @@ extern const Info<bool> MAIN_BBA_XLINK_CHAT_OSD;
extern const Info<std::string> MAIN_BBA_BUILTIN_DNS;
extern const Info<std::string> MAIN_BBA_BUILTIN_IP;
extern const Info<std::string> MAIN_BBA_TAPSERVER_DESTINATION;
extern const Info<std::string> MAIN_MODEM_TAPSERVER_DESTINATION;
const Info<SerialInterface::SIDevices>& GetInfoForSIDevice(int channel);
const Info<bool>& GetInfoForAdapterRumble(int channel);
const Info<bool>& GetInfoForSimulateKonga(int channel);

View File

@ -14,6 +14,7 @@
#include "Core/HW/EXI/EXI_DeviceIPL.h"
#include "Core/HW/EXI/EXI_DeviceMemoryCard.h"
#include "Core/HW/EXI/EXI_DeviceMic.h"
#include "Core/HW/EXI/EXI_DeviceModem.h"
#include "Core/HW/Memmap.h"
#include "Core/System.h"
@ -149,6 +150,10 @@ std::unique_ptr<IEXIDevice> EXIDevice_Create(Core::System& system, const EXIDevi
result = std::make_unique<CEXIETHERNET>(system, BBADeviceType::BuiltIn);
break;
case EXIDeviceType::ModemTapServer:
result = std::make_unique<CEXIModem>(system, ModemDeviceType::TAPSERVER);
break;
case EXIDeviceType::Gecko:
result = std::make_unique<CEXIGecko>(system);
break;

View File

@ -41,6 +41,7 @@ enum class EXIDeviceType : int
EthernetXLink,
EthernetTapServer,
EthernetBuiltIn,
ModemTapServer,
None = 0xFF
};
@ -87,7 +88,7 @@ std::unique_ptr<IEXIDevice> EXIDevice_Create(Core::System& system, EXIDeviceType
template <>
struct fmt::formatter<ExpansionInterface::EXIDeviceType>
: EnumFormatter<ExpansionInterface::EXIDeviceType::EthernetBuiltIn>
: EnumFormatter<ExpansionInterface::EXIDeviceType::ModemTapServer>
{
static constexpr array_type names = {
_trans("Dummy"),
@ -104,6 +105,7 @@ struct fmt::formatter<ExpansionInterface::EXIDeviceType>
_trans("Broadband Adapter (XLink Kai)"),
_trans("Broadband Adapter (tapserver)"),
_trans("Broadband Adapter (HLE)"),
_trans("Modem Adapter (tapserver)"),
};
constexpr formatter() : EnumFormatter(names) {}

View File

@ -0,0 +1,382 @@
// Copyright 2008 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Core/HW/EXI/EXI_DeviceModem.h"
#include <inttypes.h>
#include <algorithm>
#include <memory>
#include <optional>
#include <string>
#include "Common/BitUtils.h"
#include "Common/ChunkFile.h"
#include "Common/CommonTypes.h"
#include "Common/Logging/Log.h"
#include "Common/Network.h"
#include "Common/StringUtil.h"
#include "Core/Config/MainSettings.h"
#include "Core/CoreTiming.h"
#include "Core/HW/EXI/EXI.h"
#include "Core/HW/Memmap.h"
#include "Core/PowerPC/PowerPC.h"
#include "Core/System.h"
namespace ExpansionInterface
{
CEXIModem::CEXIModem(Core::System& system, ModemDeviceType type) : IEXIDevice(system)
{
switch (type)
{
case ModemDeviceType::TAPSERVER:
m_network_interface = std::make_unique<TAPServerNetworkInterface>(
this, Config::Get(Config::MAIN_MODEM_TAPSERVER_DESTINATION));
INFO_LOG_FMT(SP1, "Created tapserver physical network interface.");
break;
}
for (size_t z = 0; z < m_regs.size(); z++)
{
m_regs[z] = 0;
}
m_regs[Register::DEVICE_TYPE] = 0x02;
m_regs[Register::INTERRUPT_MASK] = 0x02;
}
CEXIModem::~CEXIModem()
{
m_network_interface->Deactivate();
}
bool CEXIModem::IsPresent() const
{
return true;
}
void CEXIModem::SetCS(int cs)
{
m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR;
}
bool CEXIModem::IsInterruptSet()
{
return !!(m_regs[Register::INTERRUPT_MASK] & m_regs[Register::PENDING_INTERRUPT_MASK]);
}
void CEXIModem::ImmWrite(u32 data, u32 size)
{
if (m_transfer_descriptor == INVALID_TRANSFER_DESCRIPTOR)
{
m_transfer_descriptor = data;
if (m_transfer_descriptor == 0x00008000)
{ // Reset
m_network_interface->Deactivate();
m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR;
}
}
else if (!IsWriteTransfer(m_transfer_descriptor))
{
ERROR_LOG_FMT(SP1, "Received EXI IMM write {:x} ({} bytes) after read command {:x}", data, size,
m_transfer_descriptor);
m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR;
}
else if (IsModemTransfer(m_transfer_descriptor))
{ // Write AT command buffer or packet send buffer
u32 be_data = htonl(data);
HandleWriteModemTransfer(&be_data, size);
}
else
{ // Write device register
uint8_t reg_num = static_cast<uint8_t>((m_transfer_descriptor >> 24) & 0x1F);
bool should_update_interrupts = false;
for (; size; size--)
{
should_update_interrupts |=
((reg_num == Register::INTERRUPT_MASK) || (reg_num == Register::PENDING_INTERRUPT_MASK));
m_regs[reg_num++] = (data >> 24);
data <<= 8;
}
if (should_update_interrupts)
{
m_system.GetExpansionInterface().ScheduleUpdateInterrupts(CoreTiming::FromThread::CPU, 0);
}
m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR;
}
}
void CEXIModem::DMAWrite(u32 addr, u32 size)
{
if (m_transfer_descriptor == INVALID_TRANSFER_DESCRIPTOR)
{
ERROR_LOG_FMT(SP1, "Received EXI DMA write {:x} ({} bytes) after read command {:x}", addr, size,
m_transfer_descriptor);
}
else if (!IsWriteTransfer(m_transfer_descriptor))
{
ERROR_LOG_FMT(SP1, "Received EXI DMA write {:x} ({} bytes) after read command {:x}", addr, size,
m_transfer_descriptor);
m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR;
}
else if (!IsModemTransfer(m_transfer_descriptor))
{
ERROR_LOG_FMT(SP1, "Received EXI DMA write {:x} ({} bytes) to registers {:x}", addr, size,
m_transfer_descriptor);
m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR;
}
else
{
auto& memory = m_system.GetMemory();
HandleWriteModemTransfer(memory.GetPointer(addr), size);
}
}
u32 CEXIModem::ImmRead(u32 size)
{
if (m_transfer_descriptor == INVALID_TRANSFER_DESCRIPTOR)
{
ERROR_LOG_FMT(SP1, "Received EXI IMM read ({} bytes) with no pending transfer", size);
return 0;
}
else if (IsWriteTransfer(m_transfer_descriptor))
{
ERROR_LOG_FMT(SP1, "Received EXI IMM read ({} bytes) after write command {:x}", size,
m_transfer_descriptor);
m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR;
return 0;
}
else if (IsModemTransfer(m_transfer_descriptor))
{
u32 be_data = 0;
HandleReadModemTransfer(&be_data, size);
return ntohl(be_data);
}
else
{ // Read device register
uint8_t reg_num = static_cast<uint8_t>((m_transfer_descriptor >> 24) & 0x1F);
if (reg_num == 0)
{
return 0x02020000; // Device ID (modem)
}
u32 ret = 0;
for (size_t z = 0; z < size; z++)
{
ret |= (m_regs[reg_num + z] << ((3 - z) * 8));
}
m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR;
return ret;
}
}
void CEXIModem::DMARead(u32 addr, u32 size)
{
if (m_transfer_descriptor == INVALID_TRANSFER_DESCRIPTOR)
{
ERROR_LOG_FMT(SP1, "Received EXI DMA read {:x} ({} bytes) with no pending transfer", addr,
size);
}
else if (IsWriteTransfer(m_transfer_descriptor))
{
ERROR_LOG_FMT(SP1, "Received EXI DMA read {:x} ({} bytes) after write command {:x}", addr, size,
m_transfer_descriptor);
m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR;
}
else if (!IsModemTransfer(m_transfer_descriptor))
{
ERROR_LOG_FMT(SP1, "Received EXI DMA read {:x} ({} bytes) to registers {:x}", addr, size,
m_transfer_descriptor);
m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR;
}
else
{
auto& memory = m_system.GetMemory();
HandleReadModemTransfer(memory.GetPointer(addr), size);
}
}
void CEXIModem::HandleReadModemTransfer(void* data, u32 size)
{
u16 bytes_requested = GetModemTransferSize(m_transfer_descriptor);
if (size > bytes_requested)
{
ERROR_LOG_FMT(SP1, "More bytes requested ({}) than originally requested for transfer {:x}",
size, m_transfer_descriptor);
size = bytes_requested;
}
u16 bytes_requested_after_read = bytes_requested - size;
if ((m_transfer_descriptor & 0x0F000000) == 0x03000000)
{ // AT command buffer
memcpy(data, m_at_reply_data.data(), std::min<size_t>(size, m_at_reply_data.size()));
m_at_reply_data = m_at_reply_data.substr(size);
m_regs[Register::AT_REPLY_SIZE] = m_at_reply_data.size();
SetInterruptFlag(Interrupt::AT_REPLY_DATA_AVAILABLE, !m_at_reply_data.empty(), true);
}
else if ((m_transfer_descriptor & 0x0F000000) == 0x08000000)
{ // Packet receive buffer
std::lock_guard<std::mutex> g(m_receive_buffer_lock);
size_t bytes_to_copy = std::min<size_t>(size, m_receive_buffer.size());
memcpy(data, m_receive_buffer.data(), bytes_to_copy);
m_receive_buffer = m_receive_buffer.substr(size);
OnReceiveBufferSizeChangedLocked(true);
}
else
{
ERROR_LOG_FMT(SP1, "Invalid modem read transfer type {:x}", m_transfer_descriptor);
}
m_transfer_descriptor =
(bytes_requested_after_read == 0) ?
INVALID_TRANSFER_DESCRIPTOR :
SetModemTransferSize(m_transfer_descriptor, bytes_requested_after_read);
}
void CEXIModem::HandleWriteModemTransfer(const void* data, u32 size)
{
u16 bytes_expected = GetModemTransferSize(m_transfer_descriptor);
if (size > bytes_expected)
{
ERROR_LOG_FMT(SP1, "More bytes received ({}) than expected for transfer {:x}", size,
m_transfer_descriptor);
return;
}
u16 bytes_expected_after_write = bytes_expected - size;
if ((m_transfer_descriptor & 0x0F000000) == 0x03000000)
{ // AT command buffer
m_at_command_data.append(reinterpret_cast<const char*>(data), size);
RunAllPendingATCommands();
m_regs[Register::AT_COMMAND_SIZE] = m_at_command_data.size();
}
else if ((m_transfer_descriptor & 0x0F000000) == 0x08000000)
{ // Packet send buffer
m_send_buffer.append(reinterpret_cast<const char*>(data), size);
// A more accurate implementation would only set this interrupt if the send
// FIFO has enough space; however, we can clear the send FIFO "instantly"
// from the emulated program's perspective, so we always tell it the send
// FIFO is empty.
SetInterruptFlag(Interrupt::SEND_BUFFER_BELOW_THRESHOLD, true, true);
m_network_interface->SendFrames();
}
else
{
ERROR_LOG_FMT(SP1, "Invalid modem write transfer type {:x}", m_transfer_descriptor);
}
m_transfer_descriptor =
(bytes_expected_after_write == 0) ?
INVALID_TRANSFER_DESCRIPTOR :
SetModemTransferSize(m_transfer_descriptor, bytes_expected_after_write);
}
void CEXIModem::DoState(PointerWrap& p)
{
// There isn't really any state to save. The registers depend on the state of
// the external connection, which Dolphin doesn't have control over. What
// should happen when the user saves a state during an online session and
// loads it later? The remote server presumably doesn't support point-in-time
// snapshots and reloading thereof.
}
u16 CEXIModem::GetTxThreshold() const
{
return (m_regs[Register::TX_THRESHOLD_HIGH] << 8) | m_regs[Register::TX_THRESHOLD_LOW];
}
u16 CEXIModem::GetRxThreshold() const
{
return (m_regs[Register::RX_THRESHOLD_HIGH] << 8) | m_regs[Register::RX_THRESHOLD_LOW];
}
void CEXIModem::SetInterruptFlag(uint8_t what, bool enabled, bool from_cpu)
{
if (enabled)
{
m_regs[Register::PENDING_INTERRUPT_MASK] |= what;
}
else
{
m_regs[Register::PENDING_INTERRUPT_MASK] &= (~what);
}
m_system.GetExpansionInterface().ScheduleUpdateInterrupts(
from_cpu ? CoreTiming::FromThread::CPU : CoreTiming::FromThread::NON_CPU, 0);
}
void CEXIModem::OnReceiveBufferSizeChangedLocked(bool from_cpu)
{
// The caller is expected to hold m_receive_buffer_lock when calling this.
uint16_t bytes_available = std::min<size_t>(m_receive_buffer.size(), 0x200);
m_regs[Register::BYTES_AVAILABLE_HIGH] = (bytes_available >> 8) & 0xFF;
m_regs[Register::BYTES_AVAILABLE_LOW] = bytes_available & 0xFF;
SetInterruptFlag(Interrupt::RECEIVE_BUFFER_ABOVE_THRESHOLD,
m_receive_buffer.size() >= GetRxThreshold(), from_cpu);
// TODO: There is a second interrupt here, which the GameCube modem library
// expects to be used when large frames are received. However, the correct
// semantics for this interrupt aren't obvious. Reverse-engineering some games
// that use the modem adapter makes it look like this interrupt should trigger
// when there's any data at all in the receive buffer, but implementing the
// interrupt this way causes them to crash. Further research is needed.
// SetInterruptFlag(Interrupt::RECEIVE_BUFFER_NOT_EMPTY, !m_receive_buffer.empty(), from_cpu);
}
void CEXIModem::SendComplete()
{
// See comment in HandleWriteModemTransfer about why this is always true.
SetInterruptFlag(Interrupt::SEND_BUFFER_BELOW_THRESHOLD, true, true);
}
void CEXIModem::AddToReceiveBuffer(std::string&& data)
{
std::lock_guard<std::mutex> g(m_receive_buffer_lock);
if (m_receive_buffer.empty())
{
m_receive_buffer = std::move(data);
}
else
{
m_receive_buffer += data;
}
OnReceiveBufferSizeChangedLocked(false);
}
void CEXIModem::AddATReply(const std::string& data)
{
m_at_reply_data += data;
m_regs[Register::AT_REPLY_SIZE] = m_at_reply_data.size();
SetInterruptFlag(Interrupt::AT_REPLY_DATA_AVAILABLE, !m_at_reply_data.empty(), false);
}
void CEXIModem::RunAllPendingATCommands()
{
for (size_t newline_pos = m_at_command_data.find_first_of("\r\n");
newline_pos != std::string::npos; newline_pos = m_at_command_data.find_first_of("\r\n"))
{
std::string command = m_at_command_data.substr(0, newline_pos);
m_at_command_data = m_at_command_data.substr(newline_pos + 1);
if (command == "ATZ")
{ // Reset
m_network_interface->Deactivate();
AddATReply("OK\r");
}
else if (command.substr(0, 3) == "ATD")
{ // Dial
if (m_network_interface->Activate())
{
AddATReply("OK\rCONNECT 115200\r"); // Maximum baud rate
}
else
{
AddATReply("OK\rNO ANSWER\r");
}
}
else
{
INFO_LOG_FMT(SP1, "Unhandled AT command: {}", command);
AddATReply("OK\r");
}
}
}
} // namespace ExpansionInterface

View File

@ -0,0 +1,179 @@
// Copyright 2008 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#include <map>
#include <mutex>
#include <thread>
#include <vector>
#ifdef _WIN32
#include <Windows.h>
#endif
#include <SFML/Network.hpp>
#include "Common/Flag.h"
#include "Common/Network.h"
#include "Common/SocketContext.h"
#include "Core/HW/EXI/BBA/BuiltIn.h"
#include "Core/HW/EXI/EXI_Device.h"
class PointerWrap;
namespace ExpansionInterface
{
#define MODEM_RECV_SIZE 0x800
enum
{
EXI_DEVTYPE_MODEM = 0x02020000,
};
enum class ModemDeviceType
{
TAPSERVER,
};
class CEXIModem : public IEXIDevice
{
public:
CEXIModem(Core::System& system, ModemDeviceType type);
virtual ~CEXIModem();
void SetCS(int cs) override;
bool IsPresent() const override;
bool IsInterruptSet() override;
void ImmWrite(u32 data, u32 size) override;
u32 ImmRead(u32 size) override;
void DMAWrite(u32 addr, u32 size) override;
void DMARead(u32 addr, u32 size) override;
void DoState(PointerWrap& p) override;
private:
enum Interrupt
{ // Used for Register::INTERRUPT_MASK and Register::PENDING_INTERRUPT_MASK
AT_REPLY_DATA_AVAILABLE = 0x02,
SEND_BUFFER_BELOW_THRESHOLD = 0x10,
RECEIVE_BUFFER_ABOVE_THRESHOLD = 0x20,
RECEIVE_BUFFER_NOT_EMPTY = 0x40,
};
enum Register
{
DEVICE_TYPE = 0x00,
INTERRUPT_MASK = 0x01,
PENDING_INTERRUPT_MASK = 0x02,
UNKNOWN_03 = 0x03,
AT_COMMAND_SIZE = 0x04,
AT_REPLY_SIZE = 0x05,
UNKNOWN_06 = 0x06,
UNKNOWN_07 = 0x07,
UNKNOWN_08 = 0x08,
BYTES_SENT_HIGH = 0x09,
BYTES_SENT_LOW = 0x0A,
BYTES_AVAILABLE_HIGH = 0x0B,
BYTES_AVAILABLE_LOW = 0x0C,
ESR = 0x0D,
TX_THRESHOLD_HIGH = 0x0E,
TX_THRESHOLD_LOW = 0x0F,
RX_THRESHOLD_HIGH = 0x10,
RX_THRESHOLD_LOW = 0x11,
STATUS = 0x12,
FWT = 0x13,
};
u16 GetTxThreshold() const;
u16 GetRxThreshold() const;
void SetInterruptFlag(uint8_t what, bool enabled, bool from_cpu);
void HandleReadModemTransfer(void* data, u32 size);
void HandleWriteModemTransfer(const void* data, u32 size);
void OnReceiveBufferSizeChangedLocked(bool from_cpu);
void SendComplete();
void AddToReceiveBuffer(std::string&& data);
void RunAllPendingATCommands();
void AddATReply(const std::string& data);
static inline bool TransferIsResetCommand(u32 transfer_descriptor)
{
return (transfer_descriptor == 0x80000000);
}
static inline bool IsWriteTransfer(u32 transfer_descriptor)
{
return (transfer_descriptor & 0x40000000);
}
static inline bool IsModemTransfer(u32 transfer_descriptor)
{
return (transfer_descriptor & 0x20000000);
}
static inline u16 GetModemTransferSize(u32 transfer_descriptor)
{
return ((transfer_descriptor >> 8) & 0xFFFF);
}
static inline u32 SetModemTransferSize(u32 transfer_descriptor, u16 new_size)
{
return (transfer_descriptor & 0xFF000000) | (new_size << 8);
}
class NetworkInterface
{
protected:
CEXIModem* m_modem_ref = nullptr;
explicit NetworkInterface(CEXIModem* modem_ref) : m_modem_ref{modem_ref} {}
public:
virtual bool Activate() { return false; }
virtual void Deactivate() {}
virtual bool IsActivated() { return false; }
virtual bool SendFrames() { return false; }
virtual bool RecvInit() { return false; }
virtual void RecvStart() {}
virtual void RecvStop() {}
virtual ~NetworkInterface() = default;
};
class TAPServerNetworkInterface : public NetworkInterface
{
public:
explicit TAPServerNetworkInterface(CEXIModem* modem_ref, const std::string& destination)
: NetworkInterface(modem_ref), m_destination(destination)
{
}
public:
bool Activate() override;
void Deactivate() override;
bool IsActivated() override;
bool SendFrames() override;
bool RecvInit() override;
void RecvStart() override;
void RecvStop() override;
private:
std::string m_destination;
Common::SocketContext m_socket_context;
int m_fd = -1;
std::thread m_read_thread;
Common::Flag m_read_enabled;
Common::Flag m_read_shutdown;
void ReadThreadHandler();
};
std::unique_ptr<NetworkInterface> m_network_interface;
static constexpr u32 INVALID_TRANSFER_DESCRIPTOR = 0xFFFFFFFF;
u32 m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR;
std::string m_at_command_data;
std::string m_at_reply_data;
std::string m_send_buffer;
std::mutex m_receive_buffer_lock;
std::string m_receive_buffer;
std::array<u8, 0x20> m_regs;
};
} // namespace ExpansionInterface

View File

@ -0,0 +1,324 @@
// Copyright 2020 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Core/HW/EXI/EXI_DeviceModem.h"
#ifdef _WIN32
#include <winsock2.h>
#include <ws2ipdef.h>
#else
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>
#endif
#include "Common/CommonFuncs.h"
#include "Common/Logging/Log.h"
#include "Common/StringUtil.h"
#include "Core/HW/EXI/EXI_Device.h"
namespace ExpansionInterface
{
#ifdef _WIN32
static constexpr auto pi_close = &closesocket;
using ws_ssize_t = int;
#else
static constexpr auto pi_close = &close;
using ws_ssize_t = ssize_t;
#endif
#ifdef __LINUX__
#define SEND_FLAGS MSG_NOSIGNAL
#else
#define SEND_FLAGS 0
#endif
static int ConnectToDestination(const std::string& destination)
{
if (destination.empty())
{
ERROR_LOG_FMT(SP1, "Cannot connect: destination is empty\n");
return -1;
}
int ss_size;
struct sockaddr_storage ss;
memset(&ss, 0, sizeof(ss));
if (destination[0] != '/')
{
// IP address or hostname
size_t colon_offset = destination.find(':');
if (colon_offset == std::string::npos)
{
ERROR_LOG_FMT(SP1, "Destination IP address does not include port\n");
return -1;
}
struct sockaddr_in* sin = reinterpret_cast<struct sockaddr_in*>(&ss);
sin->sin_addr.s_addr = htonl(sf::IpAddress(destination.substr(0, colon_offset)).toInteger());
sin->sin_family = AF_INET;
sin->sin_port = htons(stoul(destination.substr(colon_offset + 1)));
ss_size = sizeof(*sin);
#ifndef _WIN32
}
else
{
// UNIX socket
struct sockaddr_un* sun = reinterpret_cast<struct sockaddr_un*>(&ss);
if (destination.size() + 1 > sizeof(sun->sun_path))
{
ERROR_LOG_FMT(SP1, "Socket path is too long, unable to init BBA\n");
return -1;
}
sun->sun_family = AF_UNIX;
strcpy(sun->sun_path, destination.c_str());
ss_size = sizeof(*sun);
#else
}
else
{
ERROR_LOG_FMT(SP1, "UNIX sockets are not supported on Windows\n");
return -1;
#endif
}
int fd = socket(ss.ss_family, SOCK_STREAM, (ss.ss_family == AF_INET) ? IPPROTO_TCP : 0);
if (fd == -1)
{
ERROR_LOG_FMT(SP1, "Couldn't create socket; unable to init BBA\n");
return -1;
}
#ifdef __APPLE__
int opt_no_sigpipe = 1;
if (setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &opt_no_sigpipe, sizeof(opt_no_sigpipe)) < 0)
INFO_LOG_FMT(SP1, "Failed to set SO_NOSIGPIPE on socket\n");
#endif
if (connect(fd, reinterpret_cast<sockaddr*>(&ss), ss_size) == -1)
{
std::string s = Common::LastStrerrorString();
INFO_LOG_FMT(SP1, "Couldn't connect socket ({}), unable to init BBA\n", s.c_str());
pi_close(fd);
return -1;
}
return fd;
}
bool CEXIModem::TAPServerNetworkInterface::Activate()
{
if (IsActivated())
return true;
m_fd = ConnectToDestination(m_destination);
if (m_fd < 0)
{
return false;
}
INFO_LOG_FMT(SP1, "Modem initialized.");
return RecvInit();
}
void CEXIModem::TAPServerNetworkInterface::Deactivate()
{
if (m_fd >= 0)
{
pi_close(m_fd);
}
m_fd = -1;
m_read_enabled.Clear();
m_read_shutdown.Set();
if (m_read_thread.joinable())
{
m_read_thread.join();
}
m_read_shutdown.Clear();
}
bool CEXIModem::TAPServerNetworkInterface::IsActivated()
{
return (m_fd >= 0);
}
bool CEXIModem::TAPServerNetworkInterface::RecvInit()
{
m_read_thread = std::thread(&CEXIModem::TAPServerNetworkInterface::ReadThreadHandler, this);
return true;
}
void CEXIModem::TAPServerNetworkInterface::RecvStart()
{
m_read_enabled.Set();
}
void CEXIModem::TAPServerNetworkInterface::RecvStop()
{
m_read_enabled.Clear();
}
bool CEXIModem::TAPServerNetworkInterface::SendFrames()
{
while (!m_modem_ref->m_send_buffer.empty())
{
size_t start_offset = m_modem_ref->m_send_buffer.find(0x7E);
if (start_offset == std::string::npos)
{
break;
}
size_t end_sentinel_offset = m_modem_ref->m_send_buffer.find(0x7E, start_offset + 1);
if (end_sentinel_offset == std::string::npos)
{
break;
}
size_t end_offset = end_sentinel_offset + 1;
size_t size = end_offset - start_offset;
uint8_t size_bytes[2] = {static_cast<u8>(size), static_cast<u8>(size >> 8)};
if (send(m_fd, size_bytes, 2, SEND_FLAGS) != 2)
{
ERROR_LOG_FMT(SP1, "SendFrames(): could not write size field");
return false;
}
int written_bytes =
send(m_fd, m_modem_ref->m_send_buffer.data() + start_offset, size, SEND_FLAGS);
if (u32(written_bytes) != size)
{
ERROR_LOG_FMT(SP1, "SendFrames(): expected to write {} bytes, instead wrote {}", size,
written_bytes);
return false;
}
else
{
m_modem_ref->m_send_buffer = m_modem_ref->m_send_buffer.substr(end_offset);
m_modem_ref->SendComplete();
}
}
return true;
}
void CEXIModem::TAPServerNetworkInterface::ReadThreadHandler()
{
enum class ReadState
{
SIZE,
SIZE_HIGH,
DATA,
SKIP,
};
ReadState read_state = ReadState::SIZE;
size_t frame_bytes_received = 0;
size_t frame_bytes_expected = 0;
std::string frame_data;
while (!m_read_shutdown.IsSet())
{
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(m_fd, &rfds);
timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec = 50000;
if (select(m_fd + 1, &rfds, nullptr, nullptr, &timeout) <= 0)
continue;
// The tapserver protocol is very simple: there is a 16-bit little-endian
// size field, followed by that many bytes of packet data
switch (read_state)
{
case ReadState::SIZE:
{
u8 size_bytes[2];
ws_ssize_t bytes_read = recv(m_fd, reinterpret_cast<char*>(size_bytes), 2, 0);
if (bytes_read == 1)
{
read_state = ReadState::SIZE_HIGH;
frame_bytes_expected = size_bytes[0];
}
else if (bytes_read == 2)
{
frame_bytes_expected = size_bytes[0] | (size_bytes[1] << 8);
frame_data.resize(frame_bytes_expected, '\0');
if (frame_bytes_expected > MODEM_RECV_SIZE)
{
ERROR_LOG_FMT(SP1, "Packet is too large ({} bytes); dropping it", frame_bytes_expected);
read_state = ReadState::SKIP;
}
else
{
read_state = ReadState::DATA;
}
}
else
{
ERROR_LOG_FMT(SP1, "Failed to read size field from destination: {}",
Common::LastStrerrorString());
}
break;
}
case ReadState::SIZE_HIGH:
{
// This handles the annoying case where only one byte of the size field
// was available earlier.
u8 size_high = 0;
ws_ssize_t bytes_read = recv(m_fd, reinterpret_cast<char*>(&size_high), 1, 0);
if (bytes_read == 1)
{
frame_bytes_expected |= (size_high << 8);
frame_data.resize(frame_bytes_expected, '\0');
if (frame_bytes_expected > MODEM_RECV_SIZE)
{
ERROR_LOG_FMT(SP1, "Packet is too large ({} bytes); dropping it", frame_bytes_expected);
read_state = ReadState::SKIP;
}
else
{
read_state = ReadState::DATA;
}
}
else
{
ERROR_LOG_FMT(SP1, "Failed to read split size field from destination: {}",
Common::LastStrerrorString());
}
break;
}
case ReadState::DATA:
case ReadState::SKIP:
{
ws_ssize_t bytes_read = recv(m_fd, frame_data.data() + frame_bytes_received,
frame_data.size() - frame_bytes_received, 0);
if (bytes_read <= 0)
{
ERROR_LOG_FMT(SP1, "Failed to read data from destination: {}",
Common::LastStrerrorString());
}
else
{
frame_bytes_received += bytes_read;
if (frame_bytes_received == frame_bytes_expected)
{
if (read_state == ReadState::DATA)
{
m_modem_ref->AddToReceiveBuffer(std::move(frame_data));
}
frame_data.clear();
frame_bytes_received = 0;
frame_bytes_expected = 0;
read_state = ReadState::SIZE;
}
}
break;
}
}
}
}
} // namespace ExpansionInterface

View File

@ -49,7 +49,12 @@ void BroadbandAdapterSettingsDialog::InitControls()
break;
case Type::TapServer:
current_address = QString::fromStdString(Config::Get(Config::MAIN_BBA_TAPSERVER_DESTINATION));
case Type::ModemTapServer:
{
bool is_modem = (m_bba_type == Type::ModemTapServer);
current_address =
QString::fromStdString(Config::Get(is_modem ? Config::MAIN_MODEM_TAPSERVER_DESTINATION :
Config::MAIN_BBA_TAPSERVER_DESTINATION));
#ifdef _WIN32
address_label = new QLabel(tr("Destination (address:port):"));
address_placeholder = QStringLiteral("");
@ -58,12 +63,24 @@ void BroadbandAdapterSettingsDialog::InitControls()
#else
address_label = new QLabel(tr("Destination (UNIX socket path or address:port):"));
address_placeholder = QStringLiteral("/tmp/dolphin-tap");
description = new QLabel(tr(
"The default value \"/tmp/dolphin-tap\" will work with a local tapserver and newserv. You "
"can also enter a network location (address:port) to connect to a remote tapserver."));
if (is_modem)
{
description = new QLabel(
tr("The default value \"/tmp/dolphin-modem-tap\" will work with a local tapserver and "
"newserv. You "
"can also enter a network location (address:port) to connect to a remote tapserver."));
}
else
{
description = new QLabel(
tr("The default value \"/tmp/dolphin-tap\" will work with a local tapserver and newserv. "
"You "
"can also enter a network location (address:port) to connect to a remote tapserver."));
}
#endif
window_title = tr("BBA destination address");
break;
}
case Type::BuiltIn:
address_label = new QLabel(tr("Enter the DNS server to use:"));
@ -134,6 +151,9 @@ void BroadbandAdapterSettingsDialog::SaveAddress()
case Type::TapServer:
Config::SetBaseOrCurrent(Config::MAIN_BBA_TAPSERVER_DESTINATION, bba_new_address);
break;
case Type::ModemTapServer:
Config::SetBaseOrCurrent(Config::MAIN_MODEM_TAPSERVER_DESTINATION, bba_new_address);
break;
case Type::BuiltIn:
Config::SetBaseOrCurrent(Config::MAIN_BBA_BUILTIN_DNS, bba_new_address);
break;

View File

@ -16,7 +16,8 @@ public:
Ethernet,
XLinkKai,
TapServer,
BuiltIn
BuiltIn,
ModemTapServer
};
explicit BroadbandAdapterSettingsDialog(QWidget* target, Type bba_type);

View File

@ -151,6 +151,7 @@ void GameCubePane::CreateWidgets()
EXIDeviceType::EthernetXLink,
EXIDeviceType::EthernetTapServer,
EXIDeviceType::EthernetBuiltIn,
EXIDeviceType::ModemTapServer,
})
{
m_slot_combos[ExpansionInterface::Slot::SP1]->addItem(tr(fmt::format("{:n}", device).c_str()),
@ -354,7 +355,8 @@ void GameCubePane::UpdateButton(ExpansionInterface::Slot slot)
has_config = (device == ExpansionInterface::EXIDeviceType::Ethernet ||
device == ExpansionInterface::EXIDeviceType::EthernetXLink ||
device == ExpansionInterface::EXIDeviceType::EthernetTapServer ||
device == ExpansionInterface::EXIDeviceType::EthernetBuiltIn);
device == ExpansionInterface::EXIDeviceType::EthernetBuiltIn ||
device == ExpansionInterface::EXIDeviceType::ModemTapServer);
break;
}
@ -406,6 +408,14 @@ void GameCubePane::OnConfigPressed(ExpansionInterface::Slot slot)
dialog.exec();
return;
}
case ExpansionInterface::EXIDeviceType::ModemTapServer:
{
BroadbandAdapterSettingsDialog dialog(this,
BroadbandAdapterSettingsDialog::Type::ModemTapServer);
SetQWidgetWindowDecorations(&dialog);
dialog.exec();
return;
}
case ExpansionInterface::EXIDeviceType::EthernetBuiltIn:
{
BroadbandAdapterSettingsDialog dialog(this, BroadbandAdapterSettingsDialog::Type::BuiltIn);