Core/HW: Support for BBA (IPC) in macOS.

Follow-up to #13870, which introduced a new **Broadband Adapter (IPC)**
in the **SP1** slot in the GameCube section in the settings that allows
local play with multiple Dolphin instances running in the same system.

Back then, only support for Linux and Windows was introduced due to
[cpp-ipc](https://github.com/mutouyun/cpp-ipc) only offering support in
those platforms. Since #14208, the adapter is supported also on FreeBSD.

This new work adds support for macOS using a separate implementation
with [ZeroMQ](https://zeromq.org).

Summary table:

| System  | Supported |       Backend          |
| :-----: | :-------: | :--------------------- |
|  Linux  |         | cpp-ipc (since #13870) |
| Windows |         | cpp-ipc (since #13870) |
| FreeBSD |         | cpp-ipc (since #14208) |
|  macOS  |         |         ZeroMQ         |
| Android |         |                        |

Although adding support for Android may be technically feasible,
launching two Dolphin instances within the same Android system may be
both challenging and impractical.
This commit is contained in:
cristian64 2025-08-16 18:50:33 +01:00
parent bd6ea9a9a1
commit bf79b641a1
10 changed files with 290 additions and 5 deletions

8
.gitmodules vendored
View File

@ -14,6 +14,10 @@
path = Externals/libusb/libusb
url = https://github.com/libusb/libusb.git
shallow = true
[submodule "Externals/libzmq/libzmq"]
path = Externals/libzmq/libzmq
url = https://github.com/zeromq/libzmq.git
shallow = true
[submodule "Externals/spirv_cross/SPIRV-Cross"]
path = Externals/spirv_cross/SPIRV-Cross
url = https://github.com/KhronosGroup/SPIRV-Cross.git
@ -34,6 +38,10 @@
path = Externals/VulkanMemoryAllocator
url = https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git
shallow = true
[submodule "Externals/Cross-Compatible-FileLock-Windows-and-Linux"]
path = Externals/Cross-Compatible-FileLock-Windows-and-Linux
url = https://github.com/KaganCanSit/Cross-Compatible-FileLock-Windows-and-Linux.git
shallow = true
[submodule "Externals/cubeb/cubeb"]
path = Externals/cubeb/cubeb
url = https://github.com/mozilla/cubeb.git

View File

@ -788,6 +788,8 @@ add_subdirectory(Externals/watcher)
if(NOT ANDROID AND NOT APPLE)
add_subdirectory(Externals/cpp-ipc)
elseif(APPLE)
add_subdirectory(Externals/libzmq)
endif()
########################################

@ -0,0 +1 @@
Subproject commit 42ffdc24b06a85383e1b7a20e09931d7849533e7

17
Externals/libzmq/CMakeLists.txt vendored Normal file
View File

@ -0,0 +1,17 @@
set(ENABLE_DRAFTS OFF)
set(BUILD_TESTS OFF)
set(WITH_DOCS OFF)
set(BUILD_SHARED OFF)
set(BUILD_STATIC ON)
add_subdirectory(libzmq)
dolphin_disable_warnings(objects)
dolphin_disable_warnings(libzmq-static)
if (NOT MSVC)
target_compile_options(objects PRIVATE "-fexceptions")
target_compile_options(libzmq-static PRIVATE "-fexceptions")
endif ()
add_library(libzmq::libzmq ALIAS libzmq-static)

1
Externals/libzmq/libzmq vendored Submodule

@ -0,0 +1 @@
Subproject commit 7a7bfa10e6b0e99210ed9397369b59f9e69cef8e

View File

@ -10,6 +10,8 @@ Dolphin includes or links code of the following third-party software projects:
[bzip2 license](https://www.sourceware.org/git/?p=bzip2.git;a=blob;f=LICENSE;hb=HEAD) (similar to 3-clause BSD)
- [cpp-ipc](https://github.com/mutouyun/cpp-ipc):
[MIT](https://github.com/mutouyun/cpp-ipc/blob/master/LICENSE)
- [Cross-Compatible-FileLock-Windows-and-Linux](https://github.com/KaganCanSit/Cross-Compatible-FileLock-Windows-and-Linux):
[MIT](https://github.com/KaganCanSit/Cross-Compatible-FileLock-Windows-and-Linux/blob/main/LICENSE)
- [cubeb](https://github.com/kinetiknz/cubeb):
[ISC](https://github.com/kinetiknz/cubeb/blob/master/LICENSE)
- [Discord-RPC](https://github.com/discordapp/discord-rpc):
@ -44,6 +46,8 @@ Dolphin includes or links code of the following third-party software projects:
[University of Illinois/NCSA Open Source license](http://llvm.org/docs/DeveloperPolicy.html#license)
- [LZO](http://www.oberhumer.com/opensource/lzo/):
[GPLv2+](http://www.oberhumer.com/opensource/gpl.html)
- [libzmq](https://github.com/zeromq/libzmq):
[MPL 2.0](https://github.com/zeromq/libzmq/blob/master/LICENSE)
- [mGBA](http://mgba.io)
[MPL 2.0](https://github.com/mgba-emu/mgba/blob/master/LICENSE)
- [MiniUPnPc](http://miniupnp.free.fr/):

View File

@ -804,6 +804,10 @@ endif()
if(NOT ANDROID AND NOT APPLE)
target_sources(core PRIVATE HW/EXI/BBA/IPC.cpp)
target_link_libraries(core PRIVATE cpp-ipc::ipc)
elseif(APPLE)
target_sources(core PRIVATE HW/EXI/BBA/IPC_zmq.cpp)
target_link_libraries(core PRIVATE libzmq::libzmq)
target_include_directories(core PRIVATE "${CMAKE_SOURCE_DIR}/Externals/Cross-Compatible-FileLock-Windows-and-Linux/include")
endif()
if(MSVC)

View File

@ -0,0 +1,209 @@
// Copyright 2008 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <bit>
#include <chrono>
#include <cstdlib>
#include <cstring>
#include <mutex>
#include <FileLockFactory.hpp>
#include <zmq.h>
#include "Core/HW/EXI/EXI_DeviceEthernet.h"
namespace ExpansionInterface
{
CEXIETHERNET::IPCBBAInterface::IPCBBAInterface(CEXIETHERNET* const eth_ref)
: CEXIETHERNET::NetworkInterface(eth_ref), m_context(zmq_ctx_new()),
m_proxy_thread(&CEXIETHERNET::IPCBBAInterface::ProxyThreadHandler, this)
{
}
CEXIETHERNET::IPCBBAInterface::~IPCBBAInterface()
{
m_proxy_thread_shutdown.Set();
{
std::lock_guard<std::mutex> lock{m_proxy_mutex};
if (m_proxy_publisher)
{
zmq_close(m_proxy_publisher);
m_proxy_publisher = nullptr;
}
if (m_proxy_subscriber)
{
zmq_close(m_proxy_subscriber);
m_proxy_subscriber = nullptr;
}
}
zmq_ctx_term(m_context);
m_context = nullptr;
if (m_proxy_thread.joinable())
{
m_proxy_thread.join();
}
}
bool CEXIETHERNET::IPCBBAInterface::Activate()
{
if (m_active)
return false;
const u32 linger{0};
m_publisher = zmq_socket(m_context, ZMQ_PUB);
zmq_setsockopt(m_publisher, ZMQ_LINGER, &linger, sizeof(linger));
zmq_connect(m_publisher, "ipc:///tmp/dolphin-bba-outbox");
m_subscriber = zmq_socket(m_context, ZMQ_SUB);
zmq_setsockopt(m_subscriber, ZMQ_LINGER, &linger, sizeof(linger));
zmq_setsockopt(m_subscriber, ZMQ_SUBSCRIBE, "", 0);
zmq_connect(m_subscriber, "ipc:///tmp/dolphin-bba-inbox");
m_read_enabled.Clear();
m_read_thread_shutdown.Clear();
m_active = true;
return RecvInit();
}
void CEXIETHERNET::IPCBBAInterface::Deactivate()
{
if (!m_active)
return;
m_read_enabled.Clear();
m_read_thread_shutdown.Set();
if (m_read_thread.joinable())
{
m_read_thread.join();
}
zmq_close(m_publisher);
zmq_close(m_subscriber);
m_publisher = nullptr;
m_subscriber = nullptr;
m_active = false;
}
bool CEXIETHERNET::IPCBBAInterface::IsActivated()
{
return m_active;
}
bool CEXIETHERNET::IPCBBAInterface::SendFrame(const u8* const frame, const u32 size)
{
if (!m_active)
return false;
std::vector<u8> message;
const u64 self{std::bit_cast<u64>(this)};
message.resize(sizeof(self) + static_cast<u64>(size));
std::memcpy(message.data(), &self, sizeof(self));
std::memcpy(message.data() + sizeof(self), frame, static_cast<u64>(size));
zmq_send(m_publisher, message.data(), message.size(), 0);
m_eth_ref->SendComplete();
return true;
}
bool CEXIETHERNET::IPCBBAInterface::RecvInit()
{
m_read_thread = std::thread(&CEXIETHERNET::IPCBBAInterface::ReadThreadHandler, this);
return true;
}
void CEXIETHERNET::IPCBBAInterface::RecvStart()
{
m_read_enabled.Set();
}
void CEXIETHERNET::IPCBBAInterface::RecvStop()
{
m_read_enabled.Clear();
}
void CEXIETHERNET::IPCBBAInterface::ReadThreadHandler()
{
const u64 self{std::bit_cast<u64>(this)};
std::vector<u8> buffer;
buffer.resize(sizeof(self) + BBA_RECV_SIZE);
while (!m_read_thread_shutdown.IsSet())
{
zmq_pollitem_t pollitem{};
pollitem.socket = m_subscriber;
pollitem.events = ZMQ_POLLIN;
const int event_count{zmq_poll(&pollitem, 1, 50)};
for (int i{0}; i < event_count; ++i)
{
const int read{zmq_recv(m_subscriber, buffer.data(), buffer.size(), 0)};
if (read < static_cast<int>(sizeof(self)))
continue;
u64 id{};
std::memcpy(&id, buffer.data(), sizeof(self));
const bool self_message{id == self};
if (self_message)
continue;
if (!m_read_enabled.IsSet())
continue;
const u8* const frame{buffer.data() + sizeof(self)};
const u64 size{read - sizeof(self)};
std::memcpy(m_eth_ref->mRecvBuffer.get(), frame, size);
m_eth_ref->mRecvBufferLength = static_cast<u32>(size);
m_eth_ref->RecvHandlePacket();
}
}
}
void CEXIETHERNET::IPCBBAInterface::ProxyThreadHandler()
{
while (!m_proxy_thread_shutdown.IsSet())
{
// The first instance that acquires the filelock becomes the proxy.
const auto filelock = file_lock::FileLockFactory::CreateTimedLockContext(
"/tmp/dolphin-bba-filelock", std::chrono::seconds(1));
if (!filelock)
continue;
void* proxy_subscriber;
void* proxy_publisher;
{
std::lock_guard<std::mutex> lock{m_proxy_mutex};
const u32 linger{0};
m_proxy_subscriber = zmq_socket(m_context, ZMQ_XSUB);
zmq_setsockopt(m_proxy_subscriber, ZMQ_LINGER, &linger, sizeof(linger));
zmq_setsockopt(m_proxy_subscriber, ZMQ_SUBSCRIBE, "", 0);
zmq_bind(m_proxy_subscriber, "ipc:///tmp/dolphin-bba-outbox");
m_proxy_publisher = zmq_socket(m_context, ZMQ_XPUB);
zmq_setsockopt(m_proxy_publisher, ZMQ_LINGER, &linger, sizeof(linger));
zmq_bind(m_proxy_publisher, "ipc:///tmp/dolphin-bba-inbox");
proxy_subscriber = m_proxy_subscriber;
proxy_publisher = m_proxy_publisher;
}
zmq_proxy(proxy_subscriber, proxy_publisher, nullptr);
}
}
} // namespace ExpansionInterface

View File

@ -478,13 +478,13 @@ private:
const Common::MACAddress& ResolveAddress(u32 inet_ip);
};
#if !defined(__ANDROID__) && !defined(__APPLE__)
class IPCBBAInterface : public NetworkInterface
{
public:
explicit IPCBBAInterface(CEXIETHERNET* const eth_ref) : NetworkInterface(eth_ref) {}
#if !defined(__ANDROID__) && !defined(__APPLE__)
bool Activate() override;
void Deactivate() override;
bool IsActivated() override;
@ -501,9 +501,50 @@ private:
std::thread m_read_thread;
Common::Flag m_read_enabled;
Common::Flag m_read_thread_shutdown;
};
#elif defined(__APPLE__)
class IPCBBAInterface : public NetworkInterface
{
public:
explicit IPCBBAInterface(CEXIETHERNET* eth_ref);
~IPCBBAInterface() override;
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();
void ProxyThreadHandler();
bool m_active{};
void* m_context{};
void* m_publisher{};
void* m_subscriber{};
std::thread m_read_thread;
Common::Flag m_read_enabled;
Common::Flag m_read_thread_shutdown;
std::thread m_proxy_thread;
Common::Flag m_proxy_thread_shutdown;
std::mutex m_proxy_mutex;
void* m_proxy_publisher{};
void* m_proxy_subscriber{};
};
#else
class IPCBBAInterface : public NetworkInterface
{
public:
explicit IPCBBAInterface(CEXIETHERNET* const eth_ref) : NetworkInterface(eth_ref) {}
bool Activate() override { return false; }
void Deactivate() override {}
bool IsActivated() override { return false; }
@ -511,9 +552,9 @@ private:
bool RecvInit() override { return false; }
void RecvStart() override {}
void RecvStop() override {}
};
#endif
};
std::unique_ptr<NetworkInterface> m_network_interface;

View File

@ -143,9 +143,7 @@ void GameCubePane::CreateWidgets()
EXIDeviceType::EthernetXLink,
EXIDeviceType::EthernetTapServer,
EXIDeviceType::EthernetBuiltIn,
#if !defined(__APPLE__)
EXIDeviceType::EthernetIPC,
#endif
EXIDeviceType::ModemTapServer,
})
{