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