diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt
index ea685b817d..f959764564 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt
@@ -23,6 +23,18 @@ enum class StringSetting(
"BBA_BUILTIN_DNS",
"3.18.217.27"
),
+ MAIN_BBA_TAPSERVER_DESTINATION(
+ Settings.FILE_DOLPHIN,
+ Settings.SECTION_INI_CORE,
+ "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,
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt
index 44e2b2b915..a3ec018b46 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt
@@ -1101,6 +1101,16 @@ class SettingsFragmentPresenter(
R.string.xlink_kai_bba_ip_description
)
)
+ } else if (serialPort1Type == 11) {
+ // Broadband Adapter (tapserver)
+ sl.add(
+ InputStringSetting(
+ context,
+ StringSetting.MAIN_BBA_TAPSERVER_DESTINATION,
+ R.string.bba_tapserver_destination,
+ R.string.bba_tapserver_destination_description
+ )
+ )
} else if (serialPort1Type == 12) {
// Broadband Adapter (Built In)
sl.add(
@@ -1111,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
+ )
+ )
}
}
diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml
index 251886b98a..89ddc6b705 100644
--- a/Source/Android/app/src/main/res/values/strings.xml
+++ b/Source/Android/app/src/main/res/values/strings.xml
@@ -133,6 +133,10 @@
For setup instructions, refer to this page.
XLink Kai IP Address/hostname
IP address or hostname of device running the XLink Kai client
+ Tapserver destination
+ Enter the socket path or netloc (address:port) of the tapserver instance
+ Tapserver destination
+ Enter the socket path or netloc (address:port) of the tapserver instance
DNS Server
Use 8.8.8.8 for normal DNS, else enter your custom one
diff --git a/Source/Core/Common/SocketContext.cpp b/Source/Core/Common/SocketContext.cpp
index defc333c11..15f9fd9010 100644
--- a/Source/Core/Common/SocketContext.cpp
+++ b/Source/Core/Common/SocketContext.cpp
@@ -8,12 +8,27 @@ namespace Common
#ifdef _WIN32
SocketContext::SocketContext()
{
- static_cast(WSAStartup(MAKEWORD(2, 2), &m_data));
+ std::lock_guard g(s_lock);
+ if (s_num_objects == 0)
+ {
+ static_cast(WSAStartup(MAKEWORD(2, 2), &s_data));
+ }
+ s_num_objects++;
}
SocketContext::~SocketContext()
{
- WSACleanup();
+ std::lock_guard g(s_lock);
+ s_num_objects--;
+ if (s_num_objects == 0)
+ {
+ WSACleanup();
+ }
}
+
+std::mutex SocketContext::s_lock;
+size_t SocketContext::s_num_objects = 0;
+WSADATA SocketContext::s_data;
+
#else
SocketContext::SocketContext() = default;
SocketContext::~SocketContext() = default;
diff --git a/Source/Core/Common/SocketContext.h b/Source/Core/Common/SocketContext.h
index 7e072fd8c0..0aa4929e89 100644
--- a/Source/Core/Common/SocketContext.h
+++ b/Source/Core/Common/SocketContext.h
@@ -5,6 +5,7 @@
#ifdef _WIN32
#include
+#include
#endif
namespace Common
@@ -23,7 +24,9 @@ public:
private:
#ifdef _WIN32
- WSADATA m_data;
+ static std::mutex s_lock;
+ static size_t s_num_objects;
+ static WSADATA s_data;
#endif
};
} // namespace Common
diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt
index 290947e722..e87983e15b 100644
--- a/Source/Core/Core/CMakeLists.txt
+++ b/Source/Core/Core/CMakeLists.txt
@@ -189,6 +189,12 @@ add_library(core
HW/DVD/DVDThread.h
HW/DVD/FileMonitor.cpp
HW/DVD/FileMonitor.h
+ HW/EXI/BBA/TAPServerConnection.cpp
+ HW/EXI/BBA/TAPServerBBA.cpp
+ HW/EXI/BBA/XLINK_KAI_BBA.cpp
+ HW/EXI/BBA/BuiltIn.cpp
+ HW/EXI/BBA/BuiltIn.h
+ HW/EXI/Modem/TAPServerModem.cpp
HW/EXI/EXI_Channel.cpp
HW/EXI/EXI_Channel.h
HW/EXI/EXI_Device.cpp
@@ -209,6 +215,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
@@ -696,9 +704,6 @@ if(WIN32)
target_sources(core PRIVATE
HW/EXI/BBA/TAP_Win32.cpp
HW/EXI/BBA/TAP_Win32.h
- HW/EXI/BBA/XLINK_KAI_BBA.cpp
- HW/EXI/BBA/BuiltIn.cpp
- HW/EXI/BBA/BuiltIn.h
HW/WiimoteReal/IOWin.cpp
HW/WiimoteReal/IOWin.h
)
@@ -712,18 +717,11 @@ if(WIN32)
elseif(APPLE)
target_sources(core PRIVATE
HW/EXI/BBA/TAP_Apple.cpp
- HW/EXI/BBA/TAPServer_Apple.cpp
- HW/EXI/BBA/XLINK_KAI_BBA.cpp
- HW/EXI/BBA/BuiltIn.cpp
- HW/EXI/BBA/BuiltIn.h
)
target_link_libraries(core PUBLIC ${IOB_LIBRARY})
elseif(UNIX)
target_sources(core PRIVATE
HW/EXI/BBA/TAP_Unix.cpp
- HW/EXI/BBA/XLINK_KAI_BBA.cpp
- HW/EXI/BBA/BuiltIn.cpp
- HW/EXI/BBA/BuiltIn.h
)
if(ANDROID)
target_sources(core PRIVATE
@@ -778,4 +776,4 @@ endif()
if(USE_RETRO_ACHIEVEMENTS)
target_link_libraries(core PRIVATE rcheevos)
target_compile_definitions(core PRIVATE -DUSE_RETRO_ACHIEVEMENTS)
-endif()
\ No newline at end of file
+endif()
diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp
index 21c453b6c7..432abbeb24 100644
--- a/Source/Core/Core/Config/MainSettings.cpp
+++ b/Source/Core/Core/Config/MainSettings.cpp
@@ -137,6 +137,10 @@ const Info MAIN_BBA_XLINK_CHAT_OSD{{System::Main, "Core", "BBA_XLINK_CHAT_
// Schthack PSO Server - https://schtserv.com/
const Info MAIN_BBA_BUILTIN_DNS{{System::Main, "Core", "BBA_BUILTIN_DNS"},
"3.18.217.27"};
+const Info MAIN_BBA_TAPSERVER_DESTINATION{
+ {System::Main, "Core", "BBA_TAPSERVER_DESTINATION"}, "/tmp/dolphin-tap"};
+const Info MAIN_MODEM_TAPSERVER_DESTINATION{
+ {System::Main, "Core", "MODEM_TAPSERVER_DESTINATION"}, "/tmp/dolphin-modem-tap"};
const Info MAIN_BBA_BUILTIN_IP{{System::Main, "Core", "BBA_BUILTIN_IP"}, ""};
const Info& GetInfoForSIDevice(int channel)
diff --git a/Source/Core/Core/Config/MainSettings.h b/Source/Core/Core/Config/MainSettings.h
index dddda8ae7a..5c028d9b36 100644
--- a/Source/Core/Core/Config/MainSettings.h
+++ b/Source/Core/Core/Config/MainSettings.h
@@ -96,6 +96,8 @@ extern const Info MAIN_BBA_XLINK_IP;
extern const Info MAIN_BBA_XLINK_CHAT_OSD;
extern const Info MAIN_BBA_BUILTIN_DNS;
extern const Info MAIN_BBA_BUILTIN_IP;
+extern const Info MAIN_BBA_TAPSERVER_DESTINATION;
+extern const Info MAIN_MODEM_TAPSERVER_DESTINATION;
const Info& GetInfoForSIDevice(int channel);
const Info& GetInfoForAdapterRumble(int channel);
const Info& GetInfoForSimulateKonga(int channel);
diff --git a/Source/Core/Core/HW/EXI/BBA/TAPServerBBA.cpp b/Source/Core/Core/HW/EXI/BBA/TAPServerBBA.cpp
new file mode 100644
index 0000000000..badbbf1ca0
--- /dev/null
+++ b/Source/Core/Core/HW/EXI/BBA/TAPServerBBA.cpp
@@ -0,0 +1,75 @@
+// Copyright 2020 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "Core/HW/EXI/EXI_DeviceEthernet.h"
+
+#include
+
+#include "Common/Logging/Log.h"
+
+namespace ExpansionInterface
+{
+
+CEXIETHERNET::TAPServerNetworkInterface::TAPServerNetworkInterface(CEXIETHERNET* eth_ref,
+ const std::string& destination)
+ : NetworkInterface(eth_ref),
+ m_tapserver_if(
+ destination,
+ std::bind(&TAPServerNetworkInterface::HandleReceivedFrame, this, std::placeholders::_1),
+ BBA_RECV_SIZE)
+{
+}
+
+bool CEXIETHERNET::TAPServerNetworkInterface::Activate()
+{
+ return m_tapserver_if.Activate();
+}
+
+void CEXIETHERNET::TAPServerNetworkInterface::Deactivate()
+{
+ m_tapserver_if.Deactivate();
+}
+
+bool CEXIETHERNET::TAPServerNetworkInterface::IsActivated()
+{
+ return m_tapserver_if.IsActivated();
+}
+
+bool CEXIETHERNET::TAPServerNetworkInterface::RecvInit()
+{
+ return m_tapserver_if.RecvInit();
+}
+
+void CEXIETHERNET::TAPServerNetworkInterface::RecvStart()
+{
+ m_tapserver_if.RecvStart();
+}
+
+void CEXIETHERNET::TAPServerNetworkInterface::RecvStop()
+{
+ m_tapserver_if.RecvStop();
+}
+
+bool CEXIETHERNET::TAPServerNetworkInterface::SendFrame(const u8* frame, u32 size)
+{
+ const bool ret = m_tapserver_if.SendFrame(frame, size);
+ if (ret)
+ m_eth_ref->SendComplete();
+ return ret;
+}
+
+void CEXIETHERNET::TAPServerNetworkInterface::HandleReceivedFrame(std::string&& data)
+{
+ if (data.size() > BBA_RECV_SIZE)
+ {
+ ERROR_LOG_FMT(SP1, "Received BBA frame of size {}, which is larger than maximum size {}",
+ data.size(), BBA_RECV_SIZE);
+ return;
+ }
+
+ std::memcpy(m_eth_ref->mRecvBuffer.get(), data.data(), data.size());
+ m_eth_ref->mRecvBufferLength = static_cast(data.size());
+ m_eth_ref->RecvHandlePacket();
+}
+
+} // namespace ExpansionInterface
diff --git a/Source/Core/Core/HW/EXI/BBA/TAPServerConnection.cpp b/Source/Core/Core/HW/EXI/BBA/TAPServerConnection.cpp
new file mode 100644
index 0000000000..b67a94dff1
--- /dev/null
+++ b/Source/Core/Core/HW/EXI/BBA/TAPServerConnection.cpp
@@ -0,0 +1,365 @@
+// Copyright 2020 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "Core/HW/EXI/EXI_DeviceEthernet.h"
+
+#ifdef _WIN32
+#include
+#include
+#else
+#include
+#include
+#include
+#include
+#include
+#endif
+
+#include
+#include
+
+#include "Common/CommonFuncs.h"
+#include "Common/Logging/Log.h"
+#include "Common/StringUtil.h"
+#include "Core/HW/EXI/EXI_Device.h"
+
+namespace ExpansionInterface
+{
+
+#ifdef _WIN32
+using ws_ssize_t = int;
+#else
+#define closesocket close
+using ws_ssize_t = ssize_t;
+#endif
+
+#ifdef __linux__
+#define SEND_FLAGS MSG_NOSIGNAL
+#else
+#define SEND_FLAGS 0
+#endif
+
+TAPServerConnection::TAPServerConnection(const std::string& destination,
+ std::function recv_cb,
+ std::size_t max_frame_size)
+ : m_destination(destination), m_recv_cb(recv_cb), m_max_frame_size(max_frame_size)
+{
+}
+
+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;
+ sockaddr_storage ss;
+ std::memset(&ss, 0, sizeof(ss));
+ if (destination[0] != '/')
+ {
+ // IP address or hostname
+ const std::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;
+ }
+
+ sockaddr_in* sin = reinterpret_cast(&ss);
+ const sf::IpAddress dest_ip(destination.substr(0, colon_offset));
+ if (dest_ip == sf::IpAddress::None || dest_ip == sf::IpAddress::Any)
+ {
+ ERROR_LOG_FMT(SP1, "Destination IP address is not valid\n");
+ return -1;
+ }
+ sin->sin_addr.s_addr = htonl(dest_ip.toInteger());
+ sin->sin_family = AF_INET;
+ const std::string port_str = destination.substr(colon_offset + 1);
+ const int dest_port = std::atoi(port_str.c_str());
+ if (dest_port < 1 || dest_port > 65535)
+ {
+ ERROR_LOG_FMT(SP1, "Destination port is not valid\n");
+ return -1;
+ }
+ sin->sin_port = htons(dest_port);
+ ss_size = sizeof(*sin);
+#ifndef _WIN32
+ }
+ else
+ {
+ // UNIX socket
+ sockaddr_un* sun = reinterpret_cast(&ss);
+ if (destination.size() + 1 > sizeof(sun->sun_path))
+ {
+ ERROR_LOG_FMT(SP1, "Socket path is too long; unable to create tapserver connection\n");
+ return -1;
+ }
+ sun->sun_family = AF_UNIX;
+ std::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
+ }
+
+ const 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 create tapserver connection\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(&ss), ss_size) == -1)
+ {
+ INFO_LOG_FMT(SP1, "Couldn't connect socket ({}), unable to create tapserver connection\n",
+ Common::StrNetworkError());
+ closesocket(fd);
+ return -1;
+ }
+
+ return fd;
+}
+
+bool TAPServerConnection::Activate()
+{
+ if (IsActivated())
+ return true;
+
+ m_fd = ConnectToDestination(m_destination);
+ if (m_fd < 0)
+ return false;
+
+ return RecvInit();
+}
+
+void TAPServerConnection::Deactivate()
+{
+ m_read_enabled.Clear();
+ m_read_shutdown.Set();
+ if (m_read_thread.joinable())
+ m_read_thread.join();
+ m_read_shutdown.Clear();
+
+ if (m_fd >= 0)
+ closesocket(m_fd);
+ m_fd = -1;
+}
+
+bool TAPServerConnection::IsActivated()
+{
+ return (m_fd >= 0);
+}
+
+bool TAPServerConnection::RecvInit()
+{
+ m_read_thread = std::thread(&TAPServerConnection::ReadThreadHandler, this);
+ return true;
+}
+
+void TAPServerConnection::RecvStart()
+{
+ m_read_enabled.Set();
+}
+
+void TAPServerConnection::RecvStop()
+{
+ m_read_enabled.Clear();
+}
+
+bool TAPServerConnection::SendAndRemoveAllHDLCFrames(std::string* send_buf)
+{
+ while (!send_buf->empty())
+ {
+ const std::size_t start_offset = send_buf->find(0x7E);
+ if (start_offset == std::string::npos)
+ {
+ break;
+ }
+ const std::size_t end_sentinel_offset = send_buf->find(0x7E, start_offset + 1);
+ if (end_sentinel_offset == std::string::npos)
+ {
+ break;
+ }
+ const std::size_t end_offset = end_sentinel_offset + 1;
+ const std::size_t size = end_offset - start_offset;
+
+ const u8 size_bytes[2] = {static_cast(size), static_cast(size >> 8)};
+ if (send(m_fd, reinterpret_cast(size_bytes), 2, SEND_FLAGS) != 2)
+ {
+ ERROR_LOG_FMT(SP1, "SendAndRemoveAllHDLCFrames(): could not write size field");
+ return false;
+ }
+ const int written_bytes =
+ send(m_fd, send_buf->data() + start_offset, static_cast(size), SEND_FLAGS);
+ if (u32(written_bytes) != size)
+ {
+ ERROR_LOG_FMT(SP1,
+ "SendAndRemoveAllHDLCFrames(): expected to write {} bytes, instead wrote {}",
+ size, written_bytes);
+ return false;
+ }
+ *send_buf = send_buf->substr(end_offset);
+ }
+ return true;
+}
+
+bool TAPServerConnection::SendFrame(const u8* frame, u32 size)
+{
+ INFO_LOG_FMT(SP1, "SendFrame {}\n{}", size, ArrayToString(frame, size, 0x10));
+
+ // On Windows, the data pointer is of type const char*; on other systems it is
+ // of type const void*. This is the reason for the reinterpret_cast here and
+ // in the other send/recv calls in this file.
+ const u8 size_bytes[2] = {static_cast(size), static_cast(size >> 8)};
+ if (send(m_fd, reinterpret_cast(size_bytes), 2, SEND_FLAGS) != 2)
+ {
+ ERROR_LOG_FMT(SP1, "SendFrame(): could not write size field");
+ return false;
+ }
+ const int written_bytes =
+ send(m_fd, reinterpret_cast(frame), static_cast(size), SEND_FLAGS);
+ if (u32(written_bytes) != size)
+ {
+ ERROR_LOG_FMT(SP1, "SendFrame(): expected to write {} bytes, instead wrote {}", size,
+ written_bytes);
+ return false;
+ }
+ return true;
+}
+
+void TAPServerConnection::ReadThreadHandler()
+{
+ enum class ReadState
+ {
+ SIZE,
+ SIZE_HIGH,
+ DATA,
+ SKIP,
+ };
+ ReadState read_state = ReadState::SIZE;
+
+ std::size_t frame_bytes_received = 0;
+ std::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;
+ int select_res = select(m_fd + 1, &rfds, nullptr, nullptr, &timeout);
+ if (select_res < 0)
+ {
+ ERROR_LOG_FMT(SP1, "Can\'t poll tapserver fd: {}", Common::StrNetworkError());
+ continue;
+ }
+ if (select_res == 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];
+ const ws_ssize_t bytes_read = recv(m_fd, reinterpret_cast(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 > m_max_frame_size)
+ {
+ ERROR_LOG_FMT(SP1, "Packet is too large ({} bytes); dropping it", frame_bytes_expected);
+ read_state = ReadState::SKIP;
+ }
+ else
+ {
+ // If read is disabled, we still need to actually read the frame in
+ // order to avoid applying backpressure on the remote end, but we
+ // should drop the frame instead of forwarding it to the client.
+ read_state = m_read_enabled.IsSet() ? ReadState::DATA : ReadState::SKIP;
+ }
+ }
+ else
+ {
+ ERROR_LOG_FMT(SP1, "Failed to read size field from destination: {}",
+ Common::StrNetworkError());
+ }
+ 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;
+ const ws_ssize_t bytes_read = recv(m_fd, reinterpret_cast(&size_high), 1, 0);
+ if (bytes_read != 1)
+ {
+ ERROR_LOG_FMT(SP1, "Failed to read split size field from destination: {}",
+ Common::StrNetworkError());
+ break;
+ }
+ frame_bytes_expected |= (size_high << 8);
+ frame_data.resize(frame_bytes_expected, '\0');
+ if (frame_bytes_expected > m_max_frame_size)
+ {
+ ERROR_LOG_FMT(SP1, "Packet is too large ({} bytes); dropping it", frame_bytes_expected);
+ read_state = ReadState::SKIP;
+ }
+ else
+ {
+ read_state = m_read_enabled.IsSet() ? ReadState::DATA : ReadState::SKIP;
+ }
+ break;
+ }
+ case ReadState::DATA:
+ case ReadState::SKIP:
+ {
+ const std::size_t bytes_to_read = frame_data.size() - frame_bytes_received;
+ const ws_ssize_t bytes_read = recv(m_fd, frame_data.data() + frame_bytes_received,
+ static_cast(bytes_to_read), 0);
+ if (bytes_read <= 0)
+ {
+ ERROR_LOG_FMT(SP1, "Failed to read data from destination: {}", Common::StrNetworkError());
+ break;
+ }
+ frame_bytes_received += bytes_read;
+ if (frame_bytes_received == frame_bytes_expected)
+ {
+ if (read_state == ReadState::DATA)
+ {
+ m_recv_cb(std::move(frame_data));
+ }
+ frame_data.clear();
+ frame_bytes_received = 0;
+ frame_bytes_expected = 0;
+ read_state = ReadState::SIZE;
+ }
+ break;
+ }
+ }
+ }
+}
+
+} // namespace ExpansionInterface
diff --git a/Source/Core/Core/HW/EXI/BBA/TAPServerConnection.h b/Source/Core/Core/HW/EXI/BBA/TAPServerConnection.h
new file mode 100644
index 0000000000..42c2020ffa
--- /dev/null
+++ b/Source/Core/Core/HW/EXI/BBA/TAPServerConnection.h
@@ -0,0 +1,56 @@
+// Copyright 2020 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include
+#include
+#include
+
+#include "Common/Flag.h"
+#include "Common/SocketContext.h"
+
+namespace ExpansionInterface
+{
+
+class TAPServerConnection
+{
+public:
+ using RecvCallback = std::function;
+
+ TAPServerConnection(const std::string& destination, RecvCallback recv_cb,
+ std::size_t max_frame_size);
+
+ bool Activate();
+ void Deactivate();
+ bool IsActivated();
+ bool RecvInit();
+ void RecvStart();
+ void RecvStop();
+ bool SendAndRemoveAllHDLCFrames(std::string* send_buf);
+ bool SendFrame(const u8* frame, u32 size);
+
+private:
+ enum class ReadState
+ {
+ Size,
+ SizeHigh,
+ Data,
+ Skip,
+ };
+
+ const std::string m_destination;
+ const RecvCallback m_recv_cb;
+ const std::size_t m_max_frame_size;
+ Common::SocketContext m_socket_context;
+
+ int m_fd = -1;
+ std::thread m_read_thread;
+ Common::Flag m_read_enabled;
+ Common::Flag m_read_shutdown;
+
+ bool StartReadThread();
+ void ReadThreadHandler();
+};
+
+} // namespace ExpansionInterface
diff --git a/Source/Core/Core/HW/EXI/BBA/TAPServer_Apple.cpp b/Source/Core/Core/HW/EXI/BBA/TAPServer_Apple.cpp
deleted file mode 100644
index de71067c1b..0000000000
--- a/Source/Core/Core/HW/EXI/BBA/TAPServer_Apple.cpp
+++ /dev/null
@@ -1,129 +0,0 @@
-// Copyright 2020 Dolphin Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#include "Core/HW/EXI/EXI_DeviceEthernet.h"
-
-#include
-#include
-#include
-#include
-
-#include "Common/CommonFuncs.h"
-#include "Common/Logging/Log.h"
-#include "Common/StringUtil.h"
-#include "Core/HW/EXI/EXI_Device.h"
-
-namespace ExpansionInterface
-{
-// This interface is only implemented on macOS, since macOS needs a replacement
-// for TunTap when the kernel extension is no longer supported. This interface
-// only appears in the menu on macOS, so on other platforms, it does nothing and
-// refuses to activate.
-
-constexpr char socket_path[] = "/tmp/dolphin-tap";
-
-bool CEXIETHERNET::TAPServerNetworkInterface::Activate()
-{
- if (IsActivated())
- return true;
-
- sockaddr_un sun = {};
- if (sizeof(socket_path) > sizeof(sun.sun_path))
- {
- ERROR_LOG_FMT(SP1, "Socket path is too long, unable to init BBA");
- return false;
- }
- sun.sun_family = AF_UNIX;
- strcpy(sun.sun_path, socket_path);
-
- fd = socket(AF_UNIX, SOCK_STREAM, 0);
- if (fd == -1)
- {
- ERROR_LOG_FMT(SP1, "Couldn't create socket, unable to init BBA");
- return false;
- }
-
- if (connect(fd, reinterpret_cast(&sun), sizeof(sun)) == -1)
- {
- ERROR_LOG_FMT(SP1, "Couldn't connect socket ({}), unable to init BBA",
- Common::LastStrerrorString());
- close(fd);
- fd = -1;
- return false;
- }
-
- INFO_LOG_FMT(SP1, "BBA initialized.");
- return RecvInit();
-}
-
-bool CEXIETHERNET::TAPServerNetworkInterface::SendFrame(const u8* frame, u32 size)
-{
- {
- const std::string s = ArrayToString(frame, size, 0x10);
- INFO_LOG_FMT(SP1, "SendFrame {}\n{}", size, s);
- }
-
- auto size16 = u16(size);
- if (write(fd, &size16, 2) != 2)
- {
- ERROR_LOG_FMT(SP1, "SendFrame(): could not write size field");
- return false;
- }
- int written_bytes = write(fd, frame, size);
- if (u32(written_bytes) != size)
- {
- ERROR_LOG_FMT(SP1, "SendFrame(): expected to write {} bytes, instead wrote {}", size,
- written_bytes);
- return false;
- }
- else
- {
- m_eth_ref->SendComplete();
- return true;
- }
-}
-
-void CEXIETHERNET::TAPServerNetworkInterface::ReadThreadHandler()
-{
- while (!readThreadShutdown.IsSet())
- {
- fd_set rfds;
- FD_ZERO(&rfds);
- FD_SET(fd, &rfds);
-
- timeval timeout;
- timeout.tv_sec = 0;
- timeout.tv_usec = 50000;
- if (select(fd + 1, &rfds, nullptr, nullptr, &timeout) <= 0)
- continue;
-
- u16 size;
- if (read(fd, &size, 2) != 2)
- {
- ERROR_LOG_FMT(SP1, "Failed to read size field from BBA: {}", Common::LastStrerrorString());
- }
- else
- {
- int read_bytes = read(fd, m_eth_ref->mRecvBuffer.get(), size);
- if (read_bytes < 0)
- {
- ERROR_LOG_FMT(SP1, "Failed to read packet data from BBA: {}", Common::LastStrerrorString());
- }
- else if (readEnabled.IsSet())
- {
- std::string data_string = ArrayToString(m_eth_ref->mRecvBuffer.get(), read_bytes, 0x10);
- INFO_LOG_FMT(SP1, "Read data: {}", data_string);
- m_eth_ref->mRecvBufferLength = read_bytes;
- m_eth_ref->RecvHandlePacket();
- }
- }
- }
-}
-
-bool CEXIETHERNET::TAPServerNetworkInterface::RecvInit()
-{
- readThread = std::thread(&CEXIETHERNET::TAPServerNetworkInterface::ReadThreadHandler, this);
- return true;
-}
-
-} // namespace ExpansionInterface
diff --git a/Source/Core/Core/HW/EXI/EXI_Device.cpp b/Source/Core/Core/HW/EXI/EXI_Device.cpp
index 425ce9dbb2..0d3ba0f8f9 100644
--- a/Source/Core/Core/HW/EXI/EXI_Device.cpp
+++ b/Source/Core/Core/HW/EXI/EXI_Device.cpp
@@ -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"
@@ -137,11 +138,9 @@ std::unique_ptr EXIDevice_Create(Core::System& system, const EXIDevi
result = std::make_unique(system, BBADeviceType::TAP);
break;
-#if defined(__APPLE__)
case EXIDeviceType::EthernetTapServer:
result = std::make_unique(system, BBADeviceType::TAPSERVER);
break;
-#endif
case EXIDeviceType::EthernetXLink:
result = std::make_unique(system, BBADeviceType::XLINK);
@@ -151,6 +150,10 @@ std::unique_ptr EXIDevice_Create(Core::System& system, const EXIDevi
result = std::make_unique(system, BBADeviceType::BuiltIn);
break;
+ case EXIDeviceType::ModemTapServer:
+ result = std::make_unique(system, ModemDeviceType::TAPSERVER);
+ break;
+
case EXIDeviceType::Gecko:
result = std::make_unique(system);
break;
diff --git a/Source/Core/Core/HW/EXI/EXI_Device.h b/Source/Core/Core/HW/EXI/EXI_Device.h
index 2c90d772f9..f405de167e 100644
--- a/Source/Core/Core/HW/EXI/EXI_Device.h
+++ b/Source/Core/Core/HW/EXI/EXI_Device.h
@@ -39,9 +39,9 @@ enum class EXIDeviceType : int
MemoryCardFolder,
AGP,
EthernetXLink,
- // Only used on Apple devices.
EthernetTapServer,
EthernetBuiltIn,
+ ModemTapServer,
None = 0xFF
};
@@ -88,7 +88,7 @@ std::unique_ptr EXIDevice_Create(Core::System& system, EXIDeviceType
template <>
struct fmt::formatter
- : EnumFormatter
+ : EnumFormatter
{
static constexpr array_type names = {
_trans("Dummy"),
@@ -105,6 +105,7 @@ struct fmt::formatter
_trans("Broadband Adapter (XLink Kai)"),
_trans("Broadband Adapter (tapserver)"),
_trans("Broadband Adapter (HLE)"),
+ _trans("Modem Adapter (tapserver)"),
};
constexpr formatter() : EnumFormatter(names) {}
diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceEthernet.cpp b/Source/Core/Core/HW/EXI/EXI_DeviceEthernet.cpp
index 549ace3a6e..fb2660ed04 100644
--- a/Source/Core/Core/HW/EXI/EXI_DeviceEthernet.cpp
+++ b/Source/Core/Core/HW/EXI/EXI_DeviceEthernet.cpp
@@ -50,12 +50,11 @@ CEXIETHERNET::CEXIETHERNET(Core::System& system, BBADeviceType type) : IEXIDevic
m_network_interface = std::make_unique(this);
INFO_LOG_FMT(SP1, "Created TAP physical network interface.");
break;
-#if defined(__APPLE__)
case BBADeviceType::TAPSERVER:
- m_network_interface = std::make_unique(this);
+ m_network_interface = std::make_unique(
+ this, Config::Get(Config::MAIN_BBA_TAPSERVER_DESTINATION));
INFO_LOG_FMT(SP1, "Created tapserver physical network interface.");
break;
-#endif
case BBADeviceType::BuiltIn:
m_network_interface = std::make_unique(
this, Config::Get(Config::MAIN_BBA_BUILTIN_DNS), Config::Get(Config::MAIN_BBA_BUILTIN_IP));
diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceEthernet.h b/Source/Core/Core/HW/EXI/EXI_DeviceEthernet.h
index cdbf645aee..465f050521 100644
--- a/Source/Core/Core/HW/EXI/EXI_DeviceEthernet.h
+++ b/Source/Core/Core/HW/EXI/EXI_DeviceEthernet.h
@@ -17,7 +17,9 @@
#include "Common/Flag.h"
#include "Common/Network.h"
+#include "Common/SocketContext.h"
#include "Core/HW/EXI/BBA/BuiltIn.h"
+#include "Core/HW/EXI/BBA/TAPServerConnection.h"
#include "Core/HW/EXI/EXI_Device.h"
class PointerWrap;
@@ -205,9 +207,7 @@ enum class BBADeviceType
{
TAP,
XLINK,
-#if defined(__APPLE__)
TAPSERVER,
-#endif
BuiltIn,
};
@@ -364,21 +364,25 @@ private:
#endif
};
-#if defined(__APPLE__)
- class TAPServerNetworkInterface : public TAPNetworkInterface
+ class TAPServerNetworkInterface : public NetworkInterface
{
public:
- explicit TAPServerNetworkInterface(CEXIETHERNET* eth_ref) : TAPNetworkInterface(eth_ref) {}
+ TAPServerNetworkInterface(CEXIETHERNET* eth_ref, const std::string& destination);
public:
bool Activate() override;
+ void Deactivate() override;
+ bool IsActivated() override;
bool SendFrame(const u8* frame, u32 size) override;
bool RecvInit() override;
+ void RecvStart() override;
+ void RecvStop() override;
private:
- void ReadThreadHandler();
+ TAPServerConnection m_tapserver_if;
+
+ void HandleReceivedFrame(std::string&& data);
};
-#endif
class XLinkNetworkInterface : public NetworkInterface
{
diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceModem.cpp b/Source/Core/Core/HW/EXI/EXI_DeviceModem.cpp
new file mode 100644
index 0000000000..17d4a7c484
--- /dev/null
+++ b/Source/Core/Core/HW/EXI/EXI_DeviceModem.cpp
@@ -0,0 +1,398 @@
+// Copyright 2024 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "Core/HW/EXI/EXI_DeviceModem.h"
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+#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(
+ this, Config::Get(Config::MAIN_MODEM_TAPSERVER_DESTINATION));
+ INFO_LOG_FMT(SP1, "Created tapserver physical network interface.");
+ break;
+ }
+
+ m_regs[Register::DEVICE_TYPE] = 0x02;
+ m_regs[Register::INTERRUPT_MASK] = 0x02;
+}
+
+CEXIModem::~CEXIModem()
+{
+ m_network_interface->RecvStop();
+ 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->RecvStop();
+ 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
+ const u32 be_data = htonl(data);
+ HandleWriteModemTransfer(&be_data, size);
+ }
+ else
+ { // Write device register
+ u8 reg_num = static_cast((m_transfer_descriptor >> 24) & 0x1F);
+ bool should_update_interrupts = false;
+ for (; size && reg_num < m_regs.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
+ const u8 reg_num = static_cast((m_transfer_descriptor >> 24) & 0x1F);
+ if (reg_num == 0)
+ {
+ return 0x02020000; // Device ID (modem)
+ }
+ u32 ret = 0;
+ for (u8 z = 0; z < size; z++)
+ {
+ if (reg_num + z >= m_regs.size())
+ {
+ break;
+ }
+ 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)
+{
+ const 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;
+ }
+ const u16 bytes_requested_after_read = bytes_requested - size;
+
+ if ((m_transfer_descriptor & 0x0F000000) == 0x03000000)
+ {
+ // AT command buffer
+ const std::size_t bytes_to_copy = std::min(size, m_at_reply_data.size());
+ std::memcpy(data, m_at_reply_data.data(), bytes_to_copy);
+ m_at_reply_data = m_at_reply_data.substr(bytes_to_copy);
+ m_regs[Register::AT_REPLY_SIZE] = static_cast(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 g(m_receive_buffer_lock);
+ const std::size_t bytes_to_copy = std::min(size, m_receive_buffer.size());
+ std::memcpy(data, m_receive_buffer.data(), bytes_to_copy);
+ m_receive_buffer = m_receive_buffer.substr(bytes_to_copy);
+ 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)
+{
+ const 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;
+ }
+ const u16 bytes_expected_after_write = bytes_expected - size;
+
+ if ((m_transfer_descriptor & 0x0F000000) == 0x03000000)
+ { // AT command buffer
+ m_at_command_data.append(reinterpret_cast(data), size);
+ RunAllPendingATCommands();
+ m_regs[Register::AT_COMMAND_SIZE] = static_cast(m_at_command_data.size());
+ }
+ else if ((m_transfer_descriptor & 0x0F000000) == 0x08000000)
+ { // Packet send buffer
+ m_send_buffer.append(reinterpret_cast(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->SendAndRemoveAllHDLCFrames(&m_send_buffer);
+ }
+ 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(u8 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.
+ const u16 bytes_available =
+ static_cast(std::min(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 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] = static_cast(m_at_reply_data.size());
+ SetInterruptFlag(Interrupt::AT_REPLY_DATA_AVAILABLE, !m_at_reply_data.empty(), true);
+}
+
+void CEXIModem::RunAllPendingATCommands()
+{
+ for (std::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);
+
+ INFO_LOG_FMT(SP1, "Received AT command: {}", command);
+
+ if (command.substr(0, 3) == "ATZ" || command == "ATH0")
+ {
+ // Reset (ATZ) or hang up (ATH0)
+ m_network_interface->RecvStop();
+ m_network_interface->Deactivate();
+ AddATReply("OK\r");
+ }
+ else if (command.substr(0, 3) == "ATD")
+ {
+ // Dial
+ if (m_network_interface->Activate())
+ {
+ m_network_interface->RecvStart();
+ AddATReply("OK\rCONNECT 115200\r"); // Maximum baud rate
+ }
+ else
+ {
+ AddATReply("OK\rNO ANSWER\r");
+ }
+ }
+ else
+ {
+ // PSO sends several other AT commands during modem setup, but in our
+ // implementation we don't actually have to do anything in response to
+ // them, so we just pretend we did.
+ AddATReply("OK\r");
+ }
+ }
+}
+
+} // namespace ExpansionInterface
diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceModem.h b/Source/Core/Core/HW/EXI/EXI_DeviceModem.h
new file mode 100644
index 0000000000..786d8db119
--- /dev/null
+++ b/Source/Core/Core/HW/EXI/EXI_DeviceModem.h
@@ -0,0 +1,172 @@
+// Copyright 2024 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include
+#include
+#include
+
+#include "Common/Flag.h"
+#include "Common/Network.h"
+#include "Core/HW/EXI/BBA/TAPServerConnection.h"
+#include "Core/HW/EXI/EXI_Device.h"
+
+class PointerWrap;
+
+namespace ExpansionInterface
+{
+
+static constexpr std::size_t 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:
+ // Note: The names in these enums are based on reverse-engineering of PSO and
+ // not on any documentation of the GC modem or its chipset. If official
+ // documentation is found, any names in there probably will not match these
+ // names.
+
+ 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(u8 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 SendAndRemoveAllHDLCFrames(std::string*) { return false; }
+ virtual bool RecvInit() { return false; }
+ virtual void RecvStart() {}
+ virtual void RecvStop() {}
+
+ virtual ~NetworkInterface() = default;
+ };
+
+ class TAPServerNetworkInterface : public NetworkInterface
+ {
+ public:
+ TAPServerNetworkInterface(CEXIModem* modem_ref, const std::string& destination);
+
+ public:
+ virtual bool Activate() override;
+ virtual void Deactivate() override;
+ virtual bool IsActivated() override;
+ virtual bool SendAndRemoveAllHDLCFrames(std::string* send_buffer) override;
+ virtual bool RecvInit() override;
+ virtual void RecvStart() override;
+ virtual void RecvStop() override;
+
+ private:
+ TAPServerConnection m_tapserver_if;
+
+ void HandleReceivedFrame(std::string&& data);
+ };
+
+ std::unique_ptr 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 m_regs{};
+};
+} // namespace ExpansionInterface
diff --git a/Source/Core/Core/HW/EXI/Modem/TAPServerModem.cpp b/Source/Core/Core/HW/EXI/Modem/TAPServerModem.cpp
new file mode 100644
index 0000000000..5723146b24
--- /dev/null
+++ b/Source/Core/Core/HW/EXI/Modem/TAPServerModem.cpp
@@ -0,0 +1,65 @@
+// Copyright 2024 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "Core/HW/EXI/EXI_DeviceModem.h"
+
+#include "Common/Logging/Log.h"
+
+namespace ExpansionInterface
+{
+
+CEXIModem::TAPServerNetworkInterface::TAPServerNetworkInterface(CEXIModem* modem_ref,
+ const std::string& destination)
+ : NetworkInterface(modem_ref),
+ m_tapserver_if(
+ destination,
+ std::bind(&TAPServerNetworkInterface::HandleReceivedFrame, this, std::placeholders::_1),
+ MODEM_RECV_SIZE)
+{
+}
+
+bool CEXIModem::TAPServerNetworkInterface::Activate()
+{
+ return m_tapserver_if.Activate();
+}
+
+void CEXIModem::TAPServerNetworkInterface::Deactivate()
+{
+ m_tapserver_if.Deactivate();
+}
+
+bool CEXIModem::TAPServerNetworkInterface::IsActivated()
+{
+ return m_tapserver_if.IsActivated();
+}
+
+bool CEXIModem::TAPServerNetworkInterface::SendAndRemoveAllHDLCFrames(std::string* send_buffer)
+{
+ const std::size_t orig_size = send_buffer->size();
+ const bool send_succeeded = m_tapserver_if.SendAndRemoveAllHDLCFrames(send_buffer);
+ if (send_succeeded && (send_buffer->size() < orig_size))
+ m_modem_ref->SendComplete();
+ return send_succeeded;
+}
+
+bool CEXIModem::TAPServerNetworkInterface::RecvInit()
+{
+ return m_tapserver_if.RecvInit();
+}
+
+void CEXIModem::TAPServerNetworkInterface::RecvStart()
+{
+ m_tapserver_if.RecvStart();
+}
+
+void CEXIModem::TAPServerNetworkInterface::RecvStop()
+{
+ m_tapserver_if.RecvStop();
+}
+
+void CEXIModem::TAPServerNetworkInterface::HandleReceivedFrame(std::string&& data)
+{
+ m_modem_ref->AddToReceiveBuffer(std::move(data));
+}
+
+} // namespace ExpansionInterface
diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props
index 50a6eb5494..5622bef028 100644
--- a/Source/Core/DolphinLib.props
+++ b/Source/Core/DolphinLib.props
@@ -284,6 +284,7 @@
+
@@ -937,7 +938,10 @@
+
+
+
@@ -948,6 +952,7 @@
+
diff --git a/Source/Core/DolphinQt/Settings/BroadbandAdapterSettingsDialog.cpp b/Source/Core/DolphinQt/Settings/BroadbandAdapterSettingsDialog.cpp
index 91aeae8a22..0eecfdf455 100644
--- a/Source/Core/DolphinQt/Settings/BroadbandAdapterSettingsDialog.cpp
+++ b/Source/Core/DolphinQt/Settings/BroadbandAdapterSettingsDialog.cpp
@@ -48,6 +48,32 @@ void BroadbandAdapterSettingsDialog::InitControls()
window_title = tr("Broadband Adapter MAC Address");
break;
+ case Type::TapServer:
+ case Type::ModemTapServer:
+ {
+ const 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("");
+ description = new QLabel(
+ tr("Enter the IP address and port of the tapserver instance you want to connect to."));
+#else
+ address_label = new QLabel(tr("Destination (UNIX socket path or address:port):"));
+ address_placeholder =
+ is_modem ? QStringLiteral(u"/tmp/dolphin-modem-tap") : QStringLiteral(u"/tmp/dolphin-tap");
+ description =
+ new QLabel(tr("The default value \"%1\" will work with a local tapserver and newserv."
+ " You can also enter a network location (address:port) to connect to a "
+ "remote tapserver.")
+ .arg(address_placeholder));
+#endif
+ window_title = tr("BBA destination address");
+ break;
+ }
+
case Type::BuiltIn:
address_label = new QLabel(tr("Enter the DNS server to use:"));
address_placeholder = QStringLiteral("8.8.8.8");
@@ -114,6 +140,12 @@ void BroadbandAdapterSettingsDialog::SaveAddress()
Config::SetBaseOrCurrent(Config::MAIN_BBA_MAC, bba_new_address);
break;
}
+ 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;
diff --git a/Source/Core/DolphinQt/Settings/BroadbandAdapterSettingsDialog.h b/Source/Core/DolphinQt/Settings/BroadbandAdapterSettingsDialog.h
index ca8d813cbe..08a8793a08 100644
--- a/Source/Core/DolphinQt/Settings/BroadbandAdapterSettingsDialog.h
+++ b/Source/Core/DolphinQt/Settings/BroadbandAdapterSettingsDialog.h
@@ -15,7 +15,9 @@ public:
{
Ethernet,
XLinkKai,
- BuiltIn
+ TapServer,
+ BuiltIn,
+ ModemTapServer
};
explicit BroadbandAdapterSettingsDialog(QWidget* target, Type bba_type);
diff --git a/Source/Core/DolphinQt/Settings/GameCubePane.cpp b/Source/Core/DolphinQt/Settings/GameCubePane.cpp
index 8c89badf00..c268625959 100644
--- a/Source/Core/DolphinQt/Settings/GameCubePane.cpp
+++ b/Source/Core/DolphinQt/Settings/GameCubePane.cpp
@@ -149,10 +149,9 @@ void GameCubePane::CreateWidgets()
EXIDeviceType::Dummy,
EXIDeviceType::Ethernet,
EXIDeviceType::EthernetXLink,
-#ifdef __APPLE__
EXIDeviceType::EthernetTapServer,
-#endif
EXIDeviceType::EthernetBuiltIn,
+ EXIDeviceType::ModemTapServer,
})
{
m_slot_combos[ExpansionInterface::Slot::SP1]->addItem(tr(fmt::format("{:n}", device).c_str()),
@@ -355,7 +354,9 @@ void GameCubePane::UpdateButton(ExpansionInterface::Slot slot)
case ExpansionInterface::Slot::SP1:
has_config = (device == ExpansionInterface::EXIDeviceType::Ethernet ||
device == ExpansionInterface::EXIDeviceType::EthernetXLink ||
- device == ExpansionInterface::EXIDeviceType::EthernetBuiltIn);
+ device == ExpansionInterface::EXIDeviceType::EthernetTapServer ||
+ device == ExpansionInterface::EXIDeviceType::EthernetBuiltIn ||
+ device == ExpansionInterface::EXIDeviceType::ModemTapServer);
break;
}
@@ -400,6 +401,21 @@ void GameCubePane::OnConfigPressed(ExpansionInterface::Slot slot)
dialog.exec();
return;
}
+ case ExpansionInterface::EXIDeviceType::EthernetTapServer:
+ {
+ BroadbandAdapterSettingsDialog dialog(this, BroadbandAdapterSettingsDialog::Type::TapServer);
+ SetQWidgetWindowDecorations(&dialog);
+ 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);