From 02deaa6748c44ffb2a370e4fadc8633a2ed25ac1 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sat, 2 Dec 2023 23:37:28 -0800 Subject: [PATCH] Implement GC modem adapter This implements the GameCube modem adapter. This implementation is stable but not perfect; it drops frames if the receive FIFO length is exceeded. This is probably due to the unimplemented interrupt mentioned in the comments. If the tapserver end of the connection is aware of this limitation, it's easily circumvented by lowering the MTU of the link, but ideally this wouldn't be necessary. This has been tested with a couple of different versions of Phantasy Star Online, including Episodes 1 & 2 Trial Edition. The Trial Edition is the only version of the game that supports the Modem Adapter and not the Broadband Adapter, which is what made this commit necessary in the first place. --- .../features/settings/model/StringSetting.kt | 6 + .../settings/ui/SettingsFragmentPresenter.kt | 10 + .../app/src/main/res/values/strings.xml | 2 + Source/Core/Core/CMakeLists.txt | 3 + Source/Core/Core/Config/MainSettings.cpp | 2 + Source/Core/Core/Config/MainSettings.h | 1 + Source/Core/Core/HW/EXI/EXI_Device.cpp | 5 + Source/Core/Core/HW/EXI/EXI_Device.h | 4 +- Source/Core/Core/HW/EXI/EXI_DeviceModem.cpp | 382 ++++++++++++++++++ Source/Core/Core/HW/EXI/EXI_DeviceModem.h | 179 ++++++++ Source/Core/Core/HW/EXI/Modem/TAPServer.cpp | 324 +++++++++++++++ .../BroadbandAdapterSettingsDialog.cpp | 28 +- .../Settings/BroadbandAdapterSettingsDialog.h | 3 +- .../Core/DolphinQt/Settings/GameCubePane.cpp | 12 +- 14 files changed, 954 insertions(+), 7 deletions(-) create mode 100644 Source/Core/Core/HW/EXI/EXI_DeviceModem.cpp create mode 100644 Source/Core/Core/HW/EXI/EXI_DeviceModem.h create mode 100644 Source/Core/Core/HW/EXI/Modem/TAPServer.cpp 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 8dc9e619c5..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 @@ -29,6 +29,12 @@ enum class StringSetting( "BBA_TAPSERVER_DESTINATION", "/tmp/dolphin-tap" ), + MAIN_MODEM_TAPSERVER_DESTINATION( + Settings.FILE_DOLPHIN, + Settings.SECTION_INI_CORE, + "MODEM_TAPSERVER_DESTINATION", + "/tmp/dolphin-modem-tap" + ), MAIN_CUSTOM_RTC_VALUE( Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, 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 a0de70a723..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 @@ -1121,6 +1121,16 @@ class SettingsFragmentPresenter( R.string.bba_builtin_dns_description ) ) + } else if (serialPort1Type == 13) { + // Modem Adapter (tapserver) + sl.add( + InputStringSetting( + context, + StringSetting.MAIN_MODEM_TAPSERVER_DESTINATION, + R.string.modem_tapserver_destination, + R.string.modem_tapserver_destination_description + ) + ) } } diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 2e8fc6856e..89ddc6b705 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -135,6 +135,8 @@ 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/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index 10b60a1a1c..f36235d7d4 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -193,6 +193,7 @@ add_library(core HW/EXI/BBA/XLINK_KAI_BBA.cpp HW/EXI/BBA/BuiltIn.cpp HW/EXI/BBA/BuiltIn.h + HW/EXI/Modem/TAPServer.cpp HW/EXI/EXI_Channel.cpp HW/EXI/EXI_Channel.h HW/EXI/EXI_Device.cpp @@ -213,6 +214,8 @@ add_library(core HW/EXI/EXI_DeviceMemoryCard.h HW/EXI/EXI_DeviceMic.cpp HW/EXI/EXI_DeviceMic.h + HW/EXI/EXI_DeviceModem.cpp + HW/EXI/EXI_DeviceModem.h HW/EXI/EXI.cpp HW/EXI/EXI.h HW/GBAPad.cpp diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index cdd852c260..432abbeb24 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -139,6 +139,8 @@ const Info MAIN_BBA_BUILTIN_DNS{{System::Main, "Core", "BBA_BUILTIN "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 b13615df5d..5c028d9b36 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -97,6 +97,7 @@ 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/EXI_Device.cpp b/Source/Core/Core/HW/EXI/EXI_Device.cpp index ad25a94522..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" @@ -149,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 c927a255d3..f405de167e 100644 --- a/Source/Core/Core/HW/EXI/EXI_Device.h +++ b/Source/Core/Core/HW/EXI/EXI_Device.h @@ -41,6 +41,7 @@ enum class EXIDeviceType : int EthernetXLink, EthernetTapServer, EthernetBuiltIn, + ModemTapServer, None = 0xFF }; @@ -87,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"), @@ -104,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_DeviceModem.cpp b/Source/Core/Core/HW/EXI/EXI_DeviceModem.cpp new file mode 100644 index 0000000000..867044ec89 --- /dev/null +++ b/Source/Core/Core/HW/EXI/EXI_DeviceModem.cpp @@ -0,0 +1,382 @@ +// Copyright 2008 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/HW/EXI/EXI_DeviceModem.h" + +#include + +#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; + } + + for (size_t z = 0; z < m_regs.size(); z++) + { + m_regs[z] = 0; + } + m_regs[Register::DEVICE_TYPE] = 0x02; + m_regs[Register::INTERRUPT_MASK] = 0x02; +} + +CEXIModem::~CEXIModem() +{ + m_network_interface->Deactivate(); +} + +bool CEXIModem::IsPresent() const +{ + return true; +} + +void CEXIModem::SetCS(int cs) +{ + m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR; +} + +bool CEXIModem::IsInterruptSet() +{ + return !!(m_regs[Register::INTERRUPT_MASK] & m_regs[Register::PENDING_INTERRUPT_MASK]); +} + +void CEXIModem::ImmWrite(u32 data, u32 size) +{ + if (m_transfer_descriptor == INVALID_TRANSFER_DESCRIPTOR) + { + m_transfer_descriptor = data; + if (m_transfer_descriptor == 0x00008000) + { // Reset + m_network_interface->Deactivate(); + m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR; + } + } + else if (!IsWriteTransfer(m_transfer_descriptor)) + { + ERROR_LOG_FMT(SP1, "Received EXI IMM write {:x} ({} bytes) after read command {:x}", data, size, + m_transfer_descriptor); + m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR; + } + else if (IsModemTransfer(m_transfer_descriptor)) + { // Write AT command buffer or packet send buffer + u32 be_data = htonl(data); + HandleWriteModemTransfer(&be_data, size); + } + else + { // Write device register + uint8_t reg_num = static_cast((m_transfer_descriptor >> 24) & 0x1F); + bool should_update_interrupts = false; + for (; size; size--) + { + should_update_interrupts |= + ((reg_num == Register::INTERRUPT_MASK) || (reg_num == Register::PENDING_INTERRUPT_MASK)); + m_regs[reg_num++] = (data >> 24); + data <<= 8; + } + if (should_update_interrupts) + { + m_system.GetExpansionInterface().ScheduleUpdateInterrupts(CoreTiming::FromThread::CPU, 0); + } + m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR; + } +} + +void CEXIModem::DMAWrite(u32 addr, u32 size) +{ + if (m_transfer_descriptor == INVALID_TRANSFER_DESCRIPTOR) + { + ERROR_LOG_FMT(SP1, "Received EXI DMA write {:x} ({} bytes) after read command {:x}", addr, size, + m_transfer_descriptor); + } + else if (!IsWriteTransfer(m_transfer_descriptor)) + { + ERROR_LOG_FMT(SP1, "Received EXI DMA write {:x} ({} bytes) after read command {:x}", addr, size, + m_transfer_descriptor); + m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR; + } + else if (!IsModemTransfer(m_transfer_descriptor)) + { + ERROR_LOG_FMT(SP1, "Received EXI DMA write {:x} ({} bytes) to registers {:x}", addr, size, + m_transfer_descriptor); + m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR; + } + else + { + auto& memory = m_system.GetMemory(); + HandleWriteModemTransfer(memory.GetPointer(addr), size); + } +} + +u32 CEXIModem::ImmRead(u32 size) +{ + if (m_transfer_descriptor == INVALID_TRANSFER_DESCRIPTOR) + { + ERROR_LOG_FMT(SP1, "Received EXI IMM read ({} bytes) with no pending transfer", size); + return 0; + } + else if (IsWriteTransfer(m_transfer_descriptor)) + { + ERROR_LOG_FMT(SP1, "Received EXI IMM read ({} bytes) after write command {:x}", size, + m_transfer_descriptor); + m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR; + return 0; + } + else if (IsModemTransfer(m_transfer_descriptor)) + { + u32 be_data = 0; + HandleReadModemTransfer(&be_data, size); + return ntohl(be_data); + } + else + { // Read device register + uint8_t reg_num = static_cast((m_transfer_descriptor >> 24) & 0x1F); + if (reg_num == 0) + { + return 0x02020000; // Device ID (modem) + } + u32 ret = 0; + for (size_t z = 0; z < size; z++) + { + ret |= (m_regs[reg_num + z] << ((3 - z) * 8)); + } + m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR; + return ret; + } +} + +void CEXIModem::DMARead(u32 addr, u32 size) +{ + if (m_transfer_descriptor == INVALID_TRANSFER_DESCRIPTOR) + { + ERROR_LOG_FMT(SP1, "Received EXI DMA read {:x} ({} bytes) with no pending transfer", addr, + size); + } + else if (IsWriteTransfer(m_transfer_descriptor)) + { + ERROR_LOG_FMT(SP1, "Received EXI DMA read {:x} ({} bytes) after write command {:x}", addr, size, + m_transfer_descriptor); + m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR; + } + else if (!IsModemTransfer(m_transfer_descriptor)) + { + ERROR_LOG_FMT(SP1, "Received EXI DMA read {:x} ({} bytes) to registers {:x}", addr, size, + m_transfer_descriptor); + m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR; + } + else + { + auto& memory = m_system.GetMemory(); + HandleReadModemTransfer(memory.GetPointer(addr), size); + } +} + +void CEXIModem::HandleReadModemTransfer(void* data, u32 size) +{ + u16 bytes_requested = GetModemTransferSize(m_transfer_descriptor); + if (size > bytes_requested) + { + ERROR_LOG_FMT(SP1, "More bytes requested ({}) than originally requested for transfer {:x}", + size, m_transfer_descriptor); + size = bytes_requested; + } + u16 bytes_requested_after_read = bytes_requested - size; + + if ((m_transfer_descriptor & 0x0F000000) == 0x03000000) + { // AT command buffer + memcpy(data, m_at_reply_data.data(), std::min(size, m_at_reply_data.size())); + m_at_reply_data = m_at_reply_data.substr(size); + m_regs[Register::AT_REPLY_SIZE] = m_at_reply_data.size(); + SetInterruptFlag(Interrupt::AT_REPLY_DATA_AVAILABLE, !m_at_reply_data.empty(), true); + } + else if ((m_transfer_descriptor & 0x0F000000) == 0x08000000) + { // Packet receive buffer + std::lock_guard g(m_receive_buffer_lock); + size_t bytes_to_copy = std::min(size, m_receive_buffer.size()); + memcpy(data, m_receive_buffer.data(), bytes_to_copy); + m_receive_buffer = m_receive_buffer.substr(size); + OnReceiveBufferSizeChangedLocked(true); + } + else + { + ERROR_LOG_FMT(SP1, "Invalid modem read transfer type {:x}", m_transfer_descriptor); + } + + m_transfer_descriptor = + (bytes_requested_after_read == 0) ? + INVALID_TRANSFER_DESCRIPTOR : + SetModemTransferSize(m_transfer_descriptor, bytes_requested_after_read); +} + +void CEXIModem::HandleWriteModemTransfer(const void* data, u32 size) +{ + u16 bytes_expected = GetModemTransferSize(m_transfer_descriptor); + if (size > bytes_expected) + { + ERROR_LOG_FMT(SP1, "More bytes received ({}) than expected for transfer {:x}", size, + m_transfer_descriptor); + return; + } + u16 bytes_expected_after_write = bytes_expected - size; + + if ((m_transfer_descriptor & 0x0F000000) == 0x03000000) + { // AT command buffer + m_at_command_data.append(reinterpret_cast(data), size); + RunAllPendingATCommands(); + m_regs[Register::AT_COMMAND_SIZE] = m_at_command_data.size(); + } + else if ((m_transfer_descriptor & 0x0F000000) == 0x08000000) + { // Packet send buffer + m_send_buffer.append(reinterpret_cast(data), size); + // A more accurate implementation would only set this interrupt if the send + // FIFO has enough space; however, we can clear the send FIFO "instantly" + // from the emulated program's perspective, so we always tell it the send + // FIFO is empty. + SetInterruptFlag(Interrupt::SEND_BUFFER_BELOW_THRESHOLD, true, true); + m_network_interface->SendFrames(); + } + else + { + ERROR_LOG_FMT(SP1, "Invalid modem write transfer type {:x}", m_transfer_descriptor); + } + + m_transfer_descriptor = + (bytes_expected_after_write == 0) ? + INVALID_TRANSFER_DESCRIPTOR : + SetModemTransferSize(m_transfer_descriptor, bytes_expected_after_write); +} + +void CEXIModem::DoState(PointerWrap& p) +{ + // There isn't really any state to save. The registers depend on the state of + // the external connection, which Dolphin doesn't have control over. What + // should happen when the user saves a state during an online session and + // loads it later? The remote server presumably doesn't support point-in-time + // snapshots and reloading thereof. +} + +u16 CEXIModem::GetTxThreshold() const +{ + return (m_regs[Register::TX_THRESHOLD_HIGH] << 8) | m_regs[Register::TX_THRESHOLD_LOW]; +} + +u16 CEXIModem::GetRxThreshold() const +{ + return (m_regs[Register::RX_THRESHOLD_HIGH] << 8) | m_regs[Register::RX_THRESHOLD_LOW]; +} + +void CEXIModem::SetInterruptFlag(uint8_t what, bool enabled, bool from_cpu) +{ + if (enabled) + { + m_regs[Register::PENDING_INTERRUPT_MASK] |= what; + } + else + { + m_regs[Register::PENDING_INTERRUPT_MASK] &= (~what); + } + m_system.GetExpansionInterface().ScheduleUpdateInterrupts( + from_cpu ? CoreTiming::FromThread::CPU : CoreTiming::FromThread::NON_CPU, 0); +} + +void CEXIModem::OnReceiveBufferSizeChangedLocked(bool from_cpu) +{ + // The caller is expected to hold m_receive_buffer_lock when calling this. + uint16_t bytes_available = std::min(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] = m_at_reply_data.size(); + SetInterruptFlag(Interrupt::AT_REPLY_DATA_AVAILABLE, !m_at_reply_data.empty(), false); +} + +void CEXIModem::RunAllPendingATCommands() +{ + for (size_t newline_pos = m_at_command_data.find_first_of("\r\n"); + newline_pos != std::string::npos; newline_pos = m_at_command_data.find_first_of("\r\n")) + { + std::string command = m_at_command_data.substr(0, newline_pos); + m_at_command_data = m_at_command_data.substr(newline_pos + 1); + + if (command == "ATZ") + { // Reset + m_network_interface->Deactivate(); + AddATReply("OK\r"); + } + else if (command.substr(0, 3) == "ATD") + { // Dial + if (m_network_interface->Activate()) + { + AddATReply("OK\rCONNECT 115200\r"); // Maximum baud rate + } + else + { + AddATReply("OK\rNO ANSWER\r"); + } + } + else + { + INFO_LOG_FMT(SP1, "Unhandled AT command: {}", command); + AddATReply("OK\r"); + } + } +} + +} // namespace ExpansionInterface 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..d6ba896667 --- /dev/null +++ b/Source/Core/Core/HW/EXI/EXI_DeviceModem.h @@ -0,0 +1,179 @@ +// Copyright 2008 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#endif + +#include + +#include "Common/Flag.h" +#include "Common/Network.h" +#include "Common/SocketContext.h" +#include "Core/HW/EXI/BBA/BuiltIn.h" +#include "Core/HW/EXI/EXI_Device.h" + +class PointerWrap; + +namespace ExpansionInterface +{ + +#define MODEM_RECV_SIZE 0x800 + +enum +{ + EXI_DEVTYPE_MODEM = 0x02020000, +}; + +enum class ModemDeviceType +{ + TAPSERVER, +}; + +class CEXIModem : public IEXIDevice +{ +public: + CEXIModem(Core::System& system, ModemDeviceType type); + virtual ~CEXIModem(); + void SetCS(int cs) override; + bool IsPresent() const override; + bool IsInterruptSet() override; + void ImmWrite(u32 data, u32 size) override; + u32 ImmRead(u32 size) override; + void DMAWrite(u32 addr, u32 size) override; + void DMARead(u32 addr, u32 size) override; + void DoState(PointerWrap& p) override; + +private: + enum Interrupt + { // Used for Register::INTERRUPT_MASK and Register::PENDING_INTERRUPT_MASK + AT_REPLY_DATA_AVAILABLE = 0x02, + SEND_BUFFER_BELOW_THRESHOLD = 0x10, + RECEIVE_BUFFER_ABOVE_THRESHOLD = 0x20, + RECEIVE_BUFFER_NOT_EMPTY = 0x40, + }; + enum Register + { + DEVICE_TYPE = 0x00, + INTERRUPT_MASK = 0x01, + PENDING_INTERRUPT_MASK = 0x02, + UNKNOWN_03 = 0x03, + AT_COMMAND_SIZE = 0x04, + AT_REPLY_SIZE = 0x05, + UNKNOWN_06 = 0x06, + UNKNOWN_07 = 0x07, + UNKNOWN_08 = 0x08, + BYTES_SENT_HIGH = 0x09, + BYTES_SENT_LOW = 0x0A, + BYTES_AVAILABLE_HIGH = 0x0B, + BYTES_AVAILABLE_LOW = 0x0C, + ESR = 0x0D, + TX_THRESHOLD_HIGH = 0x0E, + TX_THRESHOLD_LOW = 0x0F, + RX_THRESHOLD_HIGH = 0x10, + RX_THRESHOLD_LOW = 0x11, + STATUS = 0x12, + FWT = 0x13, + }; + + u16 GetTxThreshold() const; + u16 GetRxThreshold() const; + void SetInterruptFlag(uint8_t what, bool enabled, bool from_cpu); + void HandleReadModemTransfer(void* data, u32 size); + void HandleWriteModemTransfer(const void* data, u32 size); + void OnReceiveBufferSizeChangedLocked(bool from_cpu); + void SendComplete(); + void AddToReceiveBuffer(std::string&& data); + void RunAllPendingATCommands(); + void AddATReply(const std::string& data); + + static inline bool TransferIsResetCommand(u32 transfer_descriptor) + { + return (transfer_descriptor == 0x80000000); + } + static inline bool IsWriteTransfer(u32 transfer_descriptor) + { + return (transfer_descriptor & 0x40000000); + } + static inline bool IsModemTransfer(u32 transfer_descriptor) + { + return (transfer_descriptor & 0x20000000); + } + static inline u16 GetModemTransferSize(u32 transfer_descriptor) + { + return ((transfer_descriptor >> 8) & 0xFFFF); + } + static inline u32 SetModemTransferSize(u32 transfer_descriptor, u16 new_size) + { + return (transfer_descriptor & 0xFF000000) | (new_size << 8); + } + + class NetworkInterface + { + protected: + CEXIModem* m_modem_ref = nullptr; + explicit NetworkInterface(CEXIModem* modem_ref) : m_modem_ref{modem_ref} {} + + public: + virtual bool Activate() { return false; } + virtual void Deactivate() {} + virtual bool IsActivated() { return false; } + virtual bool SendFrames() { return false; } + virtual bool RecvInit() { return false; } + virtual void RecvStart() {} + virtual void RecvStop() {} + + virtual ~NetworkInterface() = default; + }; + + class TAPServerNetworkInterface : public NetworkInterface + { + public: + explicit TAPServerNetworkInterface(CEXIModem* modem_ref, const std::string& destination) + : NetworkInterface(modem_ref), m_destination(destination) + { + } + + public: + bool Activate() override; + void Deactivate() override; + bool IsActivated() override; + bool SendFrames() override; + bool RecvInit() override; + void RecvStart() override; + void RecvStop() override; + + private: + std::string m_destination; + Common::SocketContext m_socket_context; + + int m_fd = -1; + std::thread m_read_thread; + Common::Flag m_read_enabled; + Common::Flag m_read_shutdown; + + void ReadThreadHandler(); + }; + + std::unique_ptr 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/TAPServer.cpp b/Source/Core/Core/HW/EXI/Modem/TAPServer.cpp new file mode 100644 index 0000000000..9db6e5e76e --- /dev/null +++ b/Source/Core/Core/HW/EXI/Modem/TAPServer.cpp @@ -0,0 +1,324 @@ +// Copyright 2020 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/HW/EXI/EXI_DeviceModem.h" + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#include +#include +#include +#endif + +#include "Common/CommonFuncs.h" +#include "Common/Logging/Log.h" +#include "Common/StringUtil.h" +#include "Core/HW/EXI/EXI_Device.h" + +namespace ExpansionInterface +{ + +#ifdef _WIN32 +static constexpr auto pi_close = &closesocket; +using ws_ssize_t = int; +#else +static constexpr auto pi_close = &close; +using ws_ssize_t = ssize_t; +#endif + +#ifdef __LINUX__ +#define SEND_FLAGS MSG_NOSIGNAL +#else +#define SEND_FLAGS 0 +#endif + +static int ConnectToDestination(const std::string& destination) +{ + if (destination.empty()) + { + ERROR_LOG_FMT(SP1, "Cannot connect: destination is empty\n"); + return -1; + } + + int ss_size; + struct sockaddr_storage ss; + memset(&ss, 0, sizeof(ss)); + if (destination[0] != '/') + { + // IP address or hostname + size_t colon_offset = destination.find(':'); + if (colon_offset == std::string::npos) + { + ERROR_LOG_FMT(SP1, "Destination IP address does not include port\n"); + return -1; + } + + struct sockaddr_in* sin = reinterpret_cast(&ss); + sin->sin_addr.s_addr = htonl(sf::IpAddress(destination.substr(0, colon_offset)).toInteger()); + sin->sin_family = AF_INET; + sin->sin_port = htons(stoul(destination.substr(colon_offset + 1))); + ss_size = sizeof(*sin); +#ifndef _WIN32 + } + else + { + // UNIX socket + struct sockaddr_un* sun = reinterpret_cast(&ss); + if (destination.size() + 1 > sizeof(sun->sun_path)) + { + ERROR_LOG_FMT(SP1, "Socket path is too long, unable to init BBA\n"); + return -1; + } + sun->sun_family = AF_UNIX; + strcpy(sun->sun_path, destination.c_str()); + ss_size = sizeof(*sun); +#else + } + else + { + ERROR_LOG_FMT(SP1, "UNIX sockets are not supported on Windows\n"); + return -1; +#endif + } + + int fd = socket(ss.ss_family, SOCK_STREAM, (ss.ss_family == AF_INET) ? IPPROTO_TCP : 0); + if (fd == -1) + { + ERROR_LOG_FMT(SP1, "Couldn't create socket; unable to init BBA\n"); + return -1; + } + +#ifdef __APPLE__ + int opt_no_sigpipe = 1; + if (setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &opt_no_sigpipe, sizeof(opt_no_sigpipe)) < 0) + INFO_LOG_FMT(SP1, "Failed to set SO_NOSIGPIPE on socket\n"); +#endif + + if (connect(fd, reinterpret_cast(&ss), ss_size) == -1) + { + std::string s = Common::LastStrerrorString(); + INFO_LOG_FMT(SP1, "Couldn't connect socket ({}), unable to init BBA\n", s.c_str()); + pi_close(fd); + return -1; + } + + return fd; +} + +bool CEXIModem::TAPServerNetworkInterface::Activate() +{ + if (IsActivated()) + return true; + + m_fd = ConnectToDestination(m_destination); + if (m_fd < 0) + { + return false; + } + + INFO_LOG_FMT(SP1, "Modem initialized."); + return RecvInit(); +} + +void CEXIModem::TAPServerNetworkInterface::Deactivate() +{ + if (m_fd >= 0) + { + pi_close(m_fd); + } + m_fd = -1; + + m_read_enabled.Clear(); + m_read_shutdown.Set(); + if (m_read_thread.joinable()) + { + m_read_thread.join(); + } + m_read_shutdown.Clear(); +} + +bool CEXIModem::TAPServerNetworkInterface::IsActivated() +{ + return (m_fd >= 0); +} + +bool CEXIModem::TAPServerNetworkInterface::RecvInit() +{ + m_read_thread = std::thread(&CEXIModem::TAPServerNetworkInterface::ReadThreadHandler, this); + return true; +} + +void CEXIModem::TAPServerNetworkInterface::RecvStart() +{ + m_read_enabled.Set(); +} + +void CEXIModem::TAPServerNetworkInterface::RecvStop() +{ + m_read_enabled.Clear(); +} + +bool CEXIModem::TAPServerNetworkInterface::SendFrames() +{ + while (!m_modem_ref->m_send_buffer.empty()) + { + size_t start_offset = m_modem_ref->m_send_buffer.find(0x7E); + if (start_offset == std::string::npos) + { + break; + } + size_t end_sentinel_offset = m_modem_ref->m_send_buffer.find(0x7E, start_offset + 1); + if (end_sentinel_offset == std::string::npos) + { + break; + } + size_t end_offset = end_sentinel_offset + 1; + size_t size = end_offset - start_offset; + + uint8_t size_bytes[2] = {static_cast(size), static_cast(size >> 8)}; + if (send(m_fd, size_bytes, 2, SEND_FLAGS) != 2) + { + ERROR_LOG_FMT(SP1, "SendFrames(): could not write size field"); + return false; + } + int written_bytes = + send(m_fd, m_modem_ref->m_send_buffer.data() + start_offset, size, SEND_FLAGS); + if (u32(written_bytes) != size) + { + ERROR_LOG_FMT(SP1, "SendFrames(): expected to write {} bytes, instead wrote {}", size, + written_bytes); + return false; + } + else + { + m_modem_ref->m_send_buffer = m_modem_ref->m_send_buffer.substr(end_offset); + m_modem_ref->SendComplete(); + } + } + return true; +} + +void CEXIModem::TAPServerNetworkInterface::ReadThreadHandler() +{ + enum class ReadState + { + SIZE, + SIZE_HIGH, + DATA, + SKIP, + }; + ReadState read_state = ReadState::SIZE; + + size_t frame_bytes_received = 0; + size_t frame_bytes_expected = 0; + std::string frame_data; + + while (!m_read_shutdown.IsSet()) + { + fd_set rfds; + FD_ZERO(&rfds); + FD_SET(m_fd, &rfds); + + timeval timeout; + timeout.tv_sec = 0; + timeout.tv_usec = 50000; + if (select(m_fd + 1, &rfds, nullptr, nullptr, &timeout) <= 0) + continue; + + // The tapserver protocol is very simple: there is a 16-bit little-endian + // size field, followed by that many bytes of packet data + switch (read_state) + { + case ReadState::SIZE: + { + u8 size_bytes[2]; + ws_ssize_t bytes_read = recv(m_fd, reinterpret_cast(size_bytes), 2, 0); + if (bytes_read == 1) + { + read_state = ReadState::SIZE_HIGH; + frame_bytes_expected = size_bytes[0]; + } + else if (bytes_read == 2) + { + frame_bytes_expected = size_bytes[0] | (size_bytes[1] << 8); + frame_data.resize(frame_bytes_expected, '\0'); + if (frame_bytes_expected > MODEM_RECV_SIZE) + { + ERROR_LOG_FMT(SP1, "Packet is too large ({} bytes); dropping it", frame_bytes_expected); + read_state = ReadState::SKIP; + } + else + { + read_state = ReadState::DATA; + } + } + else + { + ERROR_LOG_FMT(SP1, "Failed to read size field from destination: {}", + Common::LastStrerrorString()); + } + break; + } + case ReadState::SIZE_HIGH: + { + // This handles the annoying case where only one byte of the size field + // was available earlier. + u8 size_high = 0; + ws_ssize_t bytes_read = recv(m_fd, reinterpret_cast(&size_high), 1, 0); + if (bytes_read == 1) + { + frame_bytes_expected |= (size_high << 8); + frame_data.resize(frame_bytes_expected, '\0'); + if (frame_bytes_expected > MODEM_RECV_SIZE) + { + ERROR_LOG_FMT(SP1, "Packet is too large ({} bytes); dropping it", frame_bytes_expected); + read_state = ReadState::SKIP; + } + else + { + read_state = ReadState::DATA; + } + } + else + { + ERROR_LOG_FMT(SP1, "Failed to read split size field from destination: {}", + Common::LastStrerrorString()); + } + break; + } + case ReadState::DATA: + case ReadState::SKIP: + { + ws_ssize_t bytes_read = recv(m_fd, frame_data.data() + frame_bytes_received, + frame_data.size() - frame_bytes_received, 0); + if (bytes_read <= 0) + { + ERROR_LOG_FMT(SP1, "Failed to read data from destination: {}", + Common::LastStrerrorString()); + } + else + { + frame_bytes_received += bytes_read; + if (frame_bytes_received == frame_bytes_expected) + { + if (read_state == ReadState::DATA) + { + m_modem_ref->AddToReceiveBuffer(std::move(frame_data)); + } + frame_data.clear(); + frame_bytes_received = 0; + frame_bytes_expected = 0; + read_state = ReadState::SIZE; + } + } + break; + } + } + } +} + +} // namespace ExpansionInterface diff --git a/Source/Core/DolphinQt/Settings/BroadbandAdapterSettingsDialog.cpp b/Source/Core/DolphinQt/Settings/BroadbandAdapterSettingsDialog.cpp index 9716d8f211..27579b5ac8 100644 --- a/Source/Core/DolphinQt/Settings/BroadbandAdapterSettingsDialog.cpp +++ b/Source/Core/DolphinQt/Settings/BroadbandAdapterSettingsDialog.cpp @@ -49,7 +49,12 @@ void BroadbandAdapterSettingsDialog::InitControls() break; case Type::TapServer: - current_address = QString::fromStdString(Config::Get(Config::MAIN_BBA_TAPSERVER_DESTINATION)); + case Type::ModemTapServer: + { + bool is_modem = (m_bba_type == Type::ModemTapServer); + current_address = + QString::fromStdString(Config::Get(is_modem ? Config::MAIN_MODEM_TAPSERVER_DESTINATION : + Config::MAIN_BBA_TAPSERVER_DESTINATION)); #ifdef _WIN32 address_label = new QLabel(tr("Destination (address:port):")); address_placeholder = QStringLiteral(""); @@ -58,12 +63,24 @@ void BroadbandAdapterSettingsDialog::InitControls() #else address_label = new QLabel(tr("Destination (UNIX socket path or address:port):")); address_placeholder = QStringLiteral("/tmp/dolphin-tap"); - description = new QLabel(tr( - "The default value \"/tmp/dolphin-tap\" will work with a local tapserver and newserv. You " - "can also enter a network location (address:port) to connect to a remote tapserver.")); + if (is_modem) + { + description = new QLabel( + tr("The default value \"/tmp/dolphin-modem-tap\" will work with a local tapserver and " + "newserv. You " + "can also enter a network location (address:port) to connect to a remote tapserver.")); + } + else + { + description = new QLabel( + tr("The default value \"/tmp/dolphin-tap\" will work with a local tapserver and newserv. " + "You " + "can also enter a network location (address:port) to connect to a remote tapserver.")); + } #endif window_title = tr("BBA destination address"); break; + } case Type::BuiltIn: address_label = new QLabel(tr("Enter the DNS server to use:")); @@ -134,6 +151,9 @@ void BroadbandAdapterSettingsDialog::SaveAddress() case Type::TapServer: Config::SetBaseOrCurrent(Config::MAIN_BBA_TAPSERVER_DESTINATION, bba_new_address); break; + case Type::ModemTapServer: + Config::SetBaseOrCurrent(Config::MAIN_MODEM_TAPSERVER_DESTINATION, bba_new_address); + break; case Type::BuiltIn: Config::SetBaseOrCurrent(Config::MAIN_BBA_BUILTIN_DNS, bba_new_address); break; diff --git a/Source/Core/DolphinQt/Settings/BroadbandAdapterSettingsDialog.h b/Source/Core/DolphinQt/Settings/BroadbandAdapterSettingsDialog.h index 53863f1d15..08a8793a08 100644 --- a/Source/Core/DolphinQt/Settings/BroadbandAdapterSettingsDialog.h +++ b/Source/Core/DolphinQt/Settings/BroadbandAdapterSettingsDialog.h @@ -16,7 +16,8 @@ public: Ethernet, XLinkKai, TapServer, - BuiltIn + 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 944efa7a9f..c268625959 100644 --- a/Source/Core/DolphinQt/Settings/GameCubePane.cpp +++ b/Source/Core/DolphinQt/Settings/GameCubePane.cpp @@ -151,6 +151,7 @@ void GameCubePane::CreateWidgets() EXIDeviceType::EthernetXLink, EXIDeviceType::EthernetTapServer, EXIDeviceType::EthernetBuiltIn, + EXIDeviceType::ModemTapServer, }) { m_slot_combos[ExpansionInterface::Slot::SP1]->addItem(tr(fmt::format("{:n}", device).c_str()), @@ -354,7 +355,8 @@ void GameCubePane::UpdateButton(ExpansionInterface::Slot slot) has_config = (device == ExpansionInterface::EXIDeviceType::Ethernet || device == ExpansionInterface::EXIDeviceType::EthernetXLink || device == ExpansionInterface::EXIDeviceType::EthernetTapServer || - device == ExpansionInterface::EXIDeviceType::EthernetBuiltIn); + device == ExpansionInterface::EXIDeviceType::EthernetBuiltIn || + device == ExpansionInterface::EXIDeviceType::ModemTapServer); break; } @@ -406,6 +408,14 @@ void GameCubePane::OnConfigPressed(ExpansionInterface::Slot slot) dialog.exec(); return; } + case ExpansionInterface::EXIDeviceType::ModemTapServer: + { + BroadbandAdapterSettingsDialog dialog(this, + BroadbandAdapterSettingsDialog::Type::ModemTapServer); + SetQWidgetWindowDecorations(&dialog); + dialog.exec(); + return; + } case ExpansionInterface::EXIDeviceType::EthernetBuiltIn: { BroadbandAdapterSettingsDialog dialog(this, BroadbandAdapterSettingsDialog::Type::BuiltIn);