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);