diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 4710da5af..e4311dab4 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -91,6 +91,8 @@ add_library(core
pad.h
pcdrv.cpp
pcdrv.h
+ pine_server.cpp
+ pine_server.h
playstation_mouse.cpp
playstation_mouse.h
psf_loader.cpp
diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj
index 10f20d7e0..71008348a 100644
--- a/src/core/core.vcxproj
+++ b/src/core/core.vcxproj
@@ -74,6 +74,7 @@
Create
+
@@ -152,6 +153,7 @@
+
diff --git a/src/core/core.vcxproj.filters b/src/core/core.vcxproj.filters
index 5543de332..3dc3f95a1 100644
--- a/src/core/core.vcxproj.filters
+++ b/src/core/core.vcxproj.filters
@@ -67,6 +67,7 @@
+
@@ -140,5 +141,6 @@
+
\ No newline at end of file
diff --git a/src/core/pine_server.cpp b/src/core/pine_server.cpp
new file mode 100644
index 000000000..569095c41
--- /dev/null
+++ b/src/core/pine_server.cpp
@@ -0,0 +1,555 @@
+// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team, Connor McLaughlin
+// SPDX-License-Identifier: LGPL-3.0+
+
+#include "pine_server.h"
+#include "cpu_core.h"
+#include "host.h"
+#include "settings.h"
+#include "system.h"
+
+#include "scmversion/scmversion.h"
+
+#include "util/platform_misc.h"
+#include "util/sockets.h"
+
+#include "common/binary_span_reader_writer.h"
+#include "common/error.h"
+#include "common/file_system.h"
+#include "common/log.h"
+#include "common/path.h"
+#include "common/small_string.h"
+
+#include "fmt/format.h"
+
+Log_SetChannel(PINEServer);
+
+namespace PINEServer {
+static std::shared_ptr s_listen_socket;
+
+#ifndef _WIN32
+static std::string s_socket_path;
+#endif
+
+/**
+ * Maximum memory used by an IPC message request.
+ * Equivalent to 50,000 Write64 requests.
+ */
+static constexpr u32 MAX_IPC_SIZE = 650000;
+
+/**
+ * Maximum memory used by an IPC message reply.
+ * Equivalent to 50,000 Read64 replies.
+ */
+static constexpr u32 MAX_IPC_RETURN_SIZE = 450000;
+
+/**
+ * IPC Command messages opcodes.
+ * A list of possible operations possible by the IPC.
+ * Each one of them is what we call an "opcode" and is the first
+ * byte sent by the IPC to differentiate between commands.
+ */
+enum IPCCommand : u8
+{
+ MsgRead8 = 0, /**< Read 8 bit value to memory. */
+ MsgRead16 = 1, /**< Read 16 bit value to memory. */
+ MsgRead32 = 2, /**< Read 32 bit value to memory. */
+ MsgRead64 = 3, /**< Read 64 bit value to memory. */
+ MsgWrite8 = 4, /**< Write 8 bit value to memory. */
+ MsgWrite16 = 5, /**< Write 16 bit value to memory. */
+ MsgWrite32 = 6, /**< Write 32 bit value to memory. */
+ MsgWrite64 = 7, /**< Write 64 bit value to memory. */
+ MsgVersion = 8, /**< Returns PCSX2 version. */
+ MsgSaveState = 9, /**< Saves a savestate. */
+ MsgLoadState = 0xA, /**< Loads a savestate. */
+ MsgTitle = 0xB, /**< Returns the game title. */
+ MsgID = 0xC, /**< Returns the game ID. */
+ MsgUUID = 0xD, /**< Returns the game UUID. */
+ MsgGameVersion = 0xE, /**< Returns the game verion. */
+ MsgStatus = 0xF, /**< Returns the emulator status. */
+ MsgUnimplemented = 0xFF /**< Unimplemented IPC message. */
+};
+
+/**
+ * Emulator status enum.
+ * A list of possible emulator statuses.
+ */
+enum class EmuStatus : u32
+{
+ Running = 0, /**< Game is running */
+ Paused = 1, /**< Game is paused */
+ Shutdown = 2 /**< Game is shutdown */
+};
+
+/**
+ * IPC result codes.
+ * A list of possible result codes the IPC can send back.
+ * Each one of them is what we call an "opcode" or "tag" and is the
+ * first byte sent by the IPC to differentiate between results.
+ */
+using IPCStatus = u8;
+static constexpr IPCStatus IPC_OK = 0; /**< IPC command successfully completed. */
+static constexpr IPCStatus IPC_FAIL = 0xFF; /**< IPC command failed to complete. */
+
+namespace {
+class PINESocket final : public BufferedStreamSocket
+{
+public:
+ PINESocket(SocketMultiplexer& multiplexer, SocketDescriptor descriptor);
+ ~PINESocket() override;
+
+protected:
+ void OnConnected() override;
+ void OnDisconnected(const Error& error) override;
+ void OnRead() override;
+ void OnWrite() override;
+
+private:
+ void ProcessCommandsInBuffer();
+ bool HandleCommand(IPCCommand command, BinarySpanReader rdbuf);
+
+ bool BeginReply(BinarySpanWriter& wrbuf, size_t required_bytes);
+ bool EndReply(const BinarySpanWriter& sw);
+
+ bool SendErrorReply();
+};
+} // namespace
+} // namespace PINEServer
+
+bool PINEServer::IsRunning()
+{
+ return static_cast(s_listen_socket);
+}
+
+bool PINEServer::Initialize(u16 slot)
+{
+ Error error;
+ std::optional address;
+#ifdef _WIN32
+ address = SocketAddress::Parse(SocketAddress::Type::IPv4, "127.0.0.1", slot, &error);
+#else
+#ifdef __APPLE__
+ const char* runtime_dir = std::getenv("TMPDIR");
+#else
+ const char* runtime_dir = std::getenv("XDG_RUNTIME_DIR");
+#endif
+ // fallback in case macOS or other OSes don't implement the XDG base spec
+ runtime_dir = runtime_dir ? runtime_dir : "/tmp";
+
+ std::string socket_path;
+ if (slot != Settings::DEFAULT_PINE_SLOT)
+ socket_path = fmt::format("{}/duckstation.sock.{}", runtime_dir, slot);
+ else
+ socket_path = fmt::format("{}/duckstation.sock", runtime_dir);
+
+ // we unlink the socket so that when releasing this thread the socket gets
+ // freed even if we didn't close correctly the loop
+ FileSystem::DeleteFile(socket_path.c_str());
+
+ address = SocketAddress::Parse(SocketAddress::Type::Unix, socket_path.c_str(), 0, &error);
+#endif
+
+ if (!address.has_value())
+ {
+ ERROR_LOG("PINE: Failed to resolve listen address: {}", error.GetDescription());
+ return false;
+ }
+
+ SocketMultiplexer* multiplexer = System::GetSocketMultiplexer();
+ if (!multiplexer)
+ return false;
+
+ s_listen_socket = multiplexer->CreateListenSocket(address.value(), &error);
+ if (!s_listen_socket)
+ {
+ ERROR_LOG("PINE: Failed to create listen socket: {}", error.GetDescription());
+ System::ReleaseSocketMultiplexer();
+ return false;
+ }
+
+#ifndef _WIN32
+ s_socket_path = std::move(socket_path);
+#endif
+
+ return true;
+}
+
+void PINEServer::Shutdown()
+{
+ // also closes the listener
+ if (s_listen_socket)
+ {
+ s_listen_socket.reset();
+ System::ReleaseSocketMultiplexer();
+ }
+
+ // unlink the socket so nobody tries to connect to something no longer existent
+#ifndef _WIN32
+ if (!s_socket_path.empty())
+ {
+ FileSystem::DeleteFile(s_socket_path.c_str());
+ s_socket_path = {};
+ }
+#endif
+}
+
+PINEServer::PINESocket::PINESocket(SocketMultiplexer& multiplexer, SocketDescriptor descriptor)
+ : BufferedStreamSocket(multiplexer, descriptor, MAX_IPC_SIZE, MAX_IPC_RETURN_SIZE)
+{
+}
+
+PINEServer::PINESocket::~PINESocket() = default;
+
+void PINEServer::PINESocket::OnConnected()
+{
+ INFO_LOG("PINE: New client at {} connected.", GetRemoteAddress().ToString());
+}
+
+void PINEServer::PINESocket::OnDisconnected(const Error& error)
+{
+ INFO_LOG("PINE: Client {} disconnected: {}", GetRemoteAddress().ToString(), error.GetDescription());
+}
+
+void PINEServer::PINESocket::OnRead()
+{
+ ProcessCommandsInBuffer();
+}
+
+void PINEServer::PINESocket::OnWrite()
+{
+ ProcessCommandsInBuffer();
+}
+
+void PINEServer::PINESocket::ProcessCommandsInBuffer()
+{
+ std::span rdbuf = AcquireReadBuffer();
+ if (rdbuf.empty())
+ return;
+
+ size_t position = 0;
+ size_t remaining = rdbuf.size();
+ while (remaining >= sizeof(u32))
+ {
+ u32 packet_size;
+ std::memcpy(&packet_size, &rdbuf[position], sizeof(u32));
+ if (packet_size > MAX_IPC_SIZE || packet_size < 5)
+ {
+ ERROR_LOG("PINE: Received invalid packet size {}", packet_size);
+ Close();
+ return;
+ }
+
+ // whole thing received yet yet?
+ if (packet_size > remaining)
+ break;
+
+ const IPCCommand command = static_cast(rdbuf[position + sizeof(u32)]);
+ if (!HandleCommand(command, BinarySpanReader(rdbuf.subspan(position + sizeof(u32) + sizeof(u8),
+ packet_size - sizeof(u32) - sizeof(u8)))))
+ {
+ // Out of write buffer space, abort.
+ break;
+ }
+
+ position += packet_size;
+ remaining -= packet_size;
+ }
+
+ ReleaseReadBuffer(position);
+ ReleaseWriteBuffer(0, true);
+}
+
+bool PINEServer::PINESocket::HandleCommand(IPCCommand command, BinarySpanReader rdbuf)
+{
+ // example IPC messages: MsgRead/Write
+ // refer to the client doc for more info on the format
+ // IPC Message event (1 byte)
+ // | Memory address (4 byte)
+ // | | argument (VLE)
+ // | | |
+ // format: XX YY YY YY YY ZZ ZZ ZZ ZZ
+ // reply code: 00 = OK, FF = NOT OK
+ // | return value (VLE)
+ // | |
+ // reply: XX ZZ ZZ ZZ ZZ
+
+ BinarySpanWriter reply;
+ switch (command)
+ {
+ case MsgRead8:
+ {
+ if (!rdbuf.CheckRemaining(sizeof(PhysicalMemoryAddress)) || !System::IsValid())
+ return SendErrorReply();
+ else if (!BeginReply(reply, sizeof(u8))) [[unlikely]]
+ return false;
+
+ const PhysicalMemoryAddress addr = rdbuf.ReadU32();
+ u8 res = 0;
+ reply << (CPU::SafeReadMemoryByte(addr, &res) ? IPC_OK : IPC_FAIL);
+ reply << res;
+ return EndReply(reply);
+ }
+
+ case MsgRead16:
+ {
+ if (!rdbuf.CheckRemaining(sizeof(PhysicalMemoryAddress)) || !System::IsValid())
+ return SendErrorReply();
+ else if (!BeginReply(reply, sizeof(u16))) [[unlikely]]
+ return false;
+
+ const PhysicalMemoryAddress addr = rdbuf.ReadU32();
+ u16 res = 0;
+ reply << (CPU::SafeReadMemoryHalfWord(addr, &res) ? IPC_OK : IPC_FAIL);
+ reply << res;
+ return EndReply(reply);
+ }
+
+ case MsgRead32:
+ {
+ if (!rdbuf.CheckRemaining(sizeof(PhysicalMemoryAddress)) || !System::IsValid())
+ return SendErrorReply();
+ else if (!BeginReply(reply, sizeof(u32))) [[unlikely]]
+ return false;
+
+ const PhysicalMemoryAddress addr = rdbuf.ReadU32();
+ u32 res = 0;
+ reply << (CPU::SafeReadMemoryWord(addr, &res) ? IPC_OK : IPC_FAIL);
+ reply << res;
+ return EndReply(reply);
+ }
+
+ case MsgRead64:
+ {
+ if (!rdbuf.CheckRemaining(sizeof(PhysicalMemoryAddress)) || !System::IsValid())
+ return SendErrorReply();
+ else if (!BeginReply(reply, sizeof(u64))) [[unlikely]]
+ return false;
+
+ const PhysicalMemoryAddress addr = rdbuf.ReadU32();
+ u32 res_low = 0, res_high = 0;
+ reply << ((!CPU::SafeReadMemoryWord(addr, &res_low) || !CPU::SafeReadMemoryWord(addr + sizeof(u32), &res_high)) ?
+ IPC_FAIL :
+ IPC_OK);
+ reply << ((ZeroExtend64(res_high) << 32) | ZeroExtend64(res_low));
+ return EndReply(reply);
+ }
+
+ case MsgWrite8:
+ {
+ // Don't do the actual write until we have space for the response, otherwise we might do it twice when we come
+ // back around.
+ if (!rdbuf.CheckRemaining(sizeof(PhysicalMemoryAddress) + sizeof(u8)) || !System::IsValid())
+ return SendErrorReply();
+ else if (!BeginReply(reply, 0)) [[unlikely]]
+ return false;
+
+ const PhysicalMemoryAddress addr = rdbuf.ReadU32();
+ const u8 value = rdbuf.ReadU8();
+ reply << (CPU::SafeWriteMemoryByte(addr, value) ? IPC_OK : IPC_FAIL);
+ return EndReply(reply);
+ }
+
+ case MsgWrite16:
+ {
+ if (!rdbuf.CheckRemaining(sizeof(PhysicalMemoryAddress) + sizeof(u16)) || !System::IsValid())
+ return SendErrorReply();
+ else if (!BeginReply(reply, 0)) [[unlikely]]
+ return false;
+
+ const PhysicalMemoryAddress addr = rdbuf.ReadU32();
+ const u16 value = rdbuf.ReadU16();
+ reply << (CPU::SafeWriteMemoryHalfWord(addr, value) ? IPC_OK : IPC_FAIL);
+ return EndReply(reply);
+ }
+
+ case MsgWrite32:
+ {
+ if (!rdbuf.CheckRemaining(sizeof(PhysicalMemoryAddress) + sizeof(u32)) || !System::IsValid())
+ return SendErrorReply();
+ else if (!BeginReply(reply, 0)) [[unlikely]]
+ return false;
+
+ const PhysicalMemoryAddress addr = rdbuf.ReadU32();
+ const u32 value = rdbuf.ReadU32();
+ reply << (CPU::SafeWriteMemoryWord(addr, value) ? IPC_OK : IPC_FAIL);
+ return EndReply(reply);
+ }
+
+ case MsgWrite64:
+ {
+ if (!rdbuf.CheckRemaining(sizeof(PhysicalMemoryAddress) + sizeof(u32)) || !System::IsValid())
+ return SendErrorReply();
+ else if (!BeginReply(reply, 0)) [[unlikely]]
+ return false;
+
+ const PhysicalMemoryAddress addr = rdbuf.ReadU32();
+ const u64 value = rdbuf.ReadU64();
+ reply << ((!CPU::SafeWriteMemoryWord(addr, Truncate32(value)) ||
+ !CPU::SafeWriteMemoryWord(addr + sizeof(u32), Truncate32(value >> 32))) ?
+ IPC_FAIL :
+ IPC_OK);
+ return EndReply(reply);
+ }
+
+ case MsgVersion:
+ {
+ const TinyString version = TinyString::from_format("DuckStation {}", g_scm_tag_str);
+ if (!BeginReply(reply, version.length() + 1)) [[unlikely]]
+ return false;
+
+ reply << IPC_OK << version;
+ return EndReply(reply);
+ }
+
+ case MsgSaveState:
+ {
+ if (!rdbuf.CheckRemaining(sizeof(u8)) || !System::IsValid())
+ return SendErrorReply();
+
+ const std::string& serial = System::GetGameSerial();
+ if (!serial.empty())
+ return SendErrorReply();
+
+ if (!BeginReply(reply, 0)) [[unlikely]]
+ return false;
+
+ std::string state_filename = System::GetGameSaveStateFileName(serial, rdbuf.ReadU8());
+ Host::RunOnCPUThread([state_filename = std::move(state_filename)] {
+ Error error;
+ if (!System::SaveState(state_filename.c_str(), &error, false))
+ ERROR_LOG("PINE: Save state failed: {}", error.GetDescription());
+ });
+
+ reply << IPC_OK;
+ return EndReply(reply);
+ }
+
+ case MsgLoadState:
+ {
+ if (!rdbuf.CheckRemaining(sizeof(u8)) || !System::IsValid())
+ return SendErrorReply();
+
+ const std::string& serial = System::GetGameSerial();
+ if (!serial.empty())
+ return SendErrorReply();
+
+ std::string state_filename = System::GetGameSaveStateFileName(serial, rdbuf.ReadU8());
+ if (!FileSystem::FileExists(state_filename.c_str()))
+ return SendErrorReply();
+
+ if (!BeginReply(reply, 0)) [[unlikely]]
+ return false;
+
+ Host::RunOnCPUThread([state_filename = std::move(state_filename)] {
+ Error error;
+ if (!System::LoadState(state_filename.c_str(), &error))
+ ERROR_LOG("PINE: Load state failed: {}", error.GetDescription());
+ });
+
+ reply << IPC_OK;
+ return EndReply(reply);
+ }
+
+ case MsgTitle:
+ {
+ if (!System::IsValid())
+ return SendErrorReply();
+
+ const std::string& name = System::GetGameTitle();
+ if (!BeginReply(reply, name.length() + 1)) [[unlikely]]
+ return false;
+
+ reply << IPC_OK << name;
+ return EndReply(reply);
+ }
+
+ case MsgID:
+ {
+ if (!System::IsValid())
+ return SendErrorReply();
+
+ const std::string& serial = System::GetGameSerial();
+ if (!BeginReply(reply, serial.length() + 1)) [[unlikely]]
+ return false;
+
+ reply << IPC_OK << serial;
+ return EndReply(reply);
+ }
+
+ case MsgUUID:
+ {
+ if (!System::IsValid())
+ return SendErrorReply();
+
+ const TinyString crc = TinyString::from_format("{:016x}", System::GetGameHash());
+ if (!BeginReply(reply, crc.length() + 1)) [[unlikely]]
+ return false;
+
+ reply << IPC_OK << crc;
+ return EndReply(reply);
+ }
+
+ case MsgGameVersion:
+ {
+ ERROR_LOG("PINE: MsgGameVersion not supported.");
+ return SendErrorReply();
+ }
+
+ case MsgStatus:
+ {
+ EmuStatus status;
+ switch (System::GetState())
+ {
+ case System::State::Running:
+ status = EmuStatus::Running;
+ break;
+ case System::State::Paused:
+ status = EmuStatus::Paused;
+ break;
+ default:
+ status = EmuStatus::Shutdown;
+ break;
+ }
+
+ if (!BeginReply(reply, sizeof(u32))) [[unlikely]]
+ return false;
+
+ reply << IPC_OK << static_cast(status);
+ return EndReply(reply);
+ }
+
+ default:
+ {
+ ERROR_LOG("PINE: Unhandled IPC command {:02X}", static_cast(command));
+ return SendErrorReply();
+ }
+ }
+}
+
+bool PINEServer::PINESocket::BeginReply(BinarySpanWriter& wrbuf, size_t required_bytes)
+{
+ wrbuf = (AcquireWriteBuffer(sizeof(u32) + sizeof(IPCStatus) + required_bytes, false));
+ if (!wrbuf.IsValid()) [[unlikely]]
+ return false;
+
+ wrbuf << static_cast(0); // size placeholder
+ return true;
+}
+
+bool PINEServer::PINESocket::EndReply(const BinarySpanWriter& sw)
+{
+ DebugAssert(sw.IsValid());
+ const size_t total_size = sw.GetBufferWritten();
+ std::memcpy(&sw.GetSpan()[0], &total_size, sizeof(u32));
+ ReleaseWriteBuffer(sw.GetBufferWritten(), false);
+ return true;
+}
+
+bool PINEServer::PINESocket::SendErrorReply()
+{
+ BinarySpanWriter reply;
+ if (!BeginReply(reply, 0)) [[unlikely]]
+ return false;
+
+ reply << IPC_FAIL;
+ return EndReply(reply);
+}
diff --git a/src/core/pine_server.h b/src/core/pine_server.h
new file mode 100644
index 000000000..4f2f1842c
--- /dev/null
+++ b/src/core/pine_server.h
@@ -0,0 +1,13 @@
+// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team, Connor McLaughlin
+// SPDX-License-Identifier: LGPL-3.0+
+
+/* A reference client implementation for interfacing with PINE is available
+ * here: https://code.govanify.com/govanify/pine/ */
+
+#pragma once
+
+namespace PINEServer {
+bool IsRunning();
+bool Initialize(u16 slot);
+void Shutdown();
+} // namespace PINEServer
diff --git a/src/core/settings.cpp b/src/core/settings.cpp
index 5c88343cb..231f8fd5a 100644
--- a/src/core/settings.cpp
+++ b/src/core/settings.cpp
@@ -160,6 +160,10 @@ void Settings::Load(SettingsInterface& si)
rewind_save_slots = static_cast(si.GetIntValue("Main", "RewindSaveSlots", 10));
runahead_frames = static_cast(si.GetIntValue("Main", "RunaheadFrameCount", 0));
+ pine_enable = si.GetBoolValue("PINE", "Enabled", false);
+ pine_slot = static_cast(
+ std::min(si.GetUIntValue("PINE", "Slot", DEFAULT_PINE_SLOT), std::numeric_limits::max()));
+
cpu_execution_mode =
ParseCPUExecutionMode(
si.GetStringValue("CPU", "ExecutionMode", GetCPUExecutionModeName(DEFAULT_CPU_EXECUTION_MODE)).c_str())
@@ -456,6 +460,9 @@ void Settings::Save(SettingsInterface& si, bool ignore_base) const
si.SetIntValue("Main", "RewindSaveSlots", rewind_save_slots);
si.SetIntValue("Main", "RunaheadFrameCount", runahead_frames);
+ si.SetBoolValue("PINE", "Enabled", pine_enable);
+ si.SetUIntValue("PINE", "Slot", pine_slot);
+
si.SetStringValue("CPU", "ExecutionMode", GetCPUExecutionModeName(cpu_execution_mode));
si.SetBoolValue("CPU", "OverclockEnable", cpu_overclock_enable);
si.SetIntValue("CPU", "OverclockNumerator", cpu_overclock_numerator);
diff --git a/src/core/settings.h b/src/core/settings.h
index 8dfcdc99b..d808f9234 100644
--- a/src/core/settings.h
+++ b/src/core/settings.h
@@ -92,11 +92,13 @@ struct Settings
bool enable_cheats : 1 = false;
bool disable_all_enhancements : 1 = false;
bool enable_discord_presence : 1 = false;
+ bool pine_enable : 1 = false;
bool rewind_enable : 1 = false;
float rewind_save_frequency = 10.0f;
u32 rewind_save_slots = 10;
u32 runahead_frames = 0;
+ u16 pine_slot = DEFAULT_PINE_SLOT;
GPURenderer gpu_renderer = DEFAULT_GPU_RENDERER;
std::string gpu_adapter;
@@ -510,6 +512,12 @@ struct Settings
static constexpr bool DEFAULT_SAVE_STATE_BACKUPS = false;
static constexpr bool DEFAULT_FAST_BOOT_VALUE = true;
#endif
+
+ // PINE uses a concept of "slot" to be able to communicate with multiple
+ // emulators at the same time, each slot should be unique to each emulator to
+ // allow PnP and configurable by the end user so that several runs don't
+ // conflict with each others
+ static constexpr u16 DEFAULT_PINE_SLOT = 28011;
};
extern Settings g_settings;
diff --git a/src/core/system.cpp b/src/core/system.cpp
index 92c6f98a9..e6f6b4e55 100644
--- a/src/core/system.cpp
+++ b/src/core/system.cpp
@@ -43,6 +43,7 @@
#include "util/iso_reader.h"
#include "util/platform_misc.h"
#include "util/postprocessing.h"
+#include "util/sockets.h"
#include "util/state_wrapper.h"
#include "common/align.h"
@@ -79,6 +80,13 @@ Log_SetChannel(System);
#include "discord_rpc.h"
#endif
+#ifndef __ANDROID__
+#define ENABLE_PINE_SERVER 1
+// #define ENABLE_GDB_SERVER 1
+#define ENABLE_SOCKET_MULTIPLEXER 1
+#include "pine_server.h"
+#endif
+
// #define PROFILE_MEMORY_SAVE_STATES 1
SystemBootParameters::SystemBootParameters() = default;
@@ -254,6 +262,10 @@ static u32 s_runahead_replay_frames = 0;
// Used to track play time. We use a monotonic timer here, in case of clock changes.
static u64 s_session_start_time = 0;
+#ifdef ENABLE_SOCKET_MULTIPLEXER
+static std::unique_ptr s_socket_multiplexer;
+#endif
+
#ifdef ENABLE_DISCORD_PRESENCE
static bool s_discord_presence_active = false;
static time_t s_discord_presence_time_epoch;
@@ -339,11 +351,20 @@ bool System::Internal::CPUThreadInitialize(Error* error)
InitializeDiscordPresence();
#endif
+#ifdef ENABLE_PINE_SERVER
+ if (g_settings.pine_enable)
+ PINEServer::Initialize(g_settings.pine_slot);
+#endif
+
return true;
}
void System::Internal::CPUThreadShutdown()
{
+#ifdef ENABLE_PINE_SERVER
+ PINEServer::Shutdown();
+#endif
+
#ifdef ENABLE_DISCORD_PRESENCE
ShutdownDiscordPresence();
#endif
@@ -369,6 +390,11 @@ void System::Internal::IdlePollUpdate()
#endif
Achievements::IdleUpdate();
+
+#ifdef ENABLE_SOCKET_MULTIPLEXER
+ if (s_socket_multiplexer)
+ s_socket_multiplexer->PollEventsWithTimeout(0);
+#endif
}
System::State System::GetState()
@@ -1924,6 +1950,11 @@ void System::FrameDone()
PollDiscordPresence();
#endif
+#ifdef ENABLE_SOCKET_MULTIPLEXER
+ if (s_socket_multiplexer)
+ s_socket_multiplexer->PollEventsWithTimeout(0);
+#endif
+
Host::FrameDone();
if (s_frame_step_request)
@@ -4089,6 +4120,17 @@ void System::CheckForSettingsChanges(const Settings& old_settings)
}
#endif
+#ifdef ENABLE_PINE_SERVER
+ if (g_settings.pine_enable != old_settings.pine_enable || g_settings.pine_slot != old_settings.pine_slot)
+ {
+ PINEServer::Shutdown();
+ if (g_settings.pine_enable)
+ PINEServer::Initialize(g_settings.pine_slot);
+ else
+ ReleaseSocketMultiplexer();
+ }
+#endif
+
if (g_settings.log_level != old_settings.log_level || g_settings.log_filter != old_settings.log_filter ||
g_settings.log_timestamps != old_settings.log_timestamps ||
g_settings.log_to_console != old_settings.log_to_console ||
@@ -5250,6 +5292,37 @@ u64 System::GetSessionPlayedTime()
return static_cast(std::round(Common::Timer::ConvertValueToSeconds(ctime - s_session_start_time)));
}
+SocketMultiplexer* System::GetSocketMultiplexer()
+{
+#ifdef ENABLE_SOCKET_MULTIPLEXER
+ if (s_socket_multiplexer)
+ return s_socket_multiplexer.get();
+
+ Error error;
+ s_socket_multiplexer = SocketMultiplexer::Create(&error);
+ if (s_socket_multiplexer)
+ INFO_LOG("Created socket multiplexer.");
+ else
+ ERROR_LOG("Failed to create socket multiplexer: {}", error.GetDescription());
+
+ return s_socket_multiplexer.get();
+#else
+ ERROR_LOG("This build does not support sockets.");
+ return nullptr;
+#endif
+}
+
+void System::ReleaseSocketMultiplexer()
+{
+#ifdef ENABLE_SOCKET_MULTIPLEXER
+ if (!s_socket_multiplexer || s_socket_multiplexer->HasAnyOpenSockets())
+ return;
+
+ INFO_LOG("Destroying socket multiplexer.");
+ s_socket_multiplexer.reset();
+#endif
+}
+
#ifdef ENABLE_DISCORD_PRESENCE
void System::InitializeDiscordPresence()
diff --git a/src/core/system.h b/src/core/system.h
index 5915d51af..1b4f8c048 100644
--- a/src/core/system.h
+++ b/src/core/system.h
@@ -19,6 +19,7 @@ class CDImage;
class Error;
class SmallStringBase;
class StateWrapper;
+class SocketMultiplexer;
enum class GPUVSyncMode : u8;
@@ -489,6 +490,10 @@ void UpdateMemorySaveStateSettings();
bool LoadRewindState(u32 skip_saves = 0, bool consume_state = true);
void SetRunaheadReplayFlag();
+/// Shared socket multiplexer, used by PINE/GDB/etc.
+SocketMultiplexer* GetSocketMultiplexer();
+void ReleaseSocketMultiplexer();
+
#ifdef ENABLE_DISCORD_PRESENCE
/// Called when rich presence changes.
void UpdateDiscordPresence(bool update_session_time);
diff --git a/src/duckstation-qt/advancedsettingswidget.cpp b/src/duckstation-qt/advancedsettingswidget.cpp
index 52486df62..73afc2125 100644
--- a/src/duckstation-qt/advancedsettingswidget.cpp
+++ b/src/duckstation-qt/advancedsettingswidget.cpp
@@ -263,6 +263,10 @@ void AdvancedSettingsWidget::addTweakOptions()
addBooleanTweakOption(m_dialog, m_ui.tweakOptionTable, tr("Allow Booting Without SBI File"), "CDROM",
"AllowBootingWithoutSBIFile", false);
+ addBooleanTweakOption(m_dialog, m_ui.tweakOptionTable, tr("Enable PINE"), "PINE", "Enabled", false);
+ addIntRangeTweakOption(m_dialog, m_ui.tweakOptionTable, tr("PINE Slot"), "PINE", "Slot", 0, 65535,
+ Settings::DEFAULT_PINE_SLOT);
+
addBooleanTweakOption(m_dialog, m_ui.tweakOptionTable, tr("Enable PCDrv"), "PCDrv", "Enabled", false);
addBooleanTweakOption(m_dialog, m_ui.tweakOptionTable, tr("Enable PCDrv Writes"), "PCDrv", "EnableWrites", false);
addDirectoryOption(m_dialog, m_ui.tweakOptionTable, tr("PCDrv Root Directory"), "PCDrv", "Root");
@@ -292,12 +296,14 @@ void AdvancedSettingsWidget::onResetToDefaultClicked()
setChoiceTweakOption(m_ui.tweakOptionTable, i++,
Settings::DEFAULT_CPU_FASTMEM_MODE); // Recompiler fastmem mode
setChoiceTweakOption(m_ui.tweakOptionTable, i++,
- Settings::DEFAULT_CDROM_MECHACON_VERSION); // CDROM Mechacon Version
- setBooleanTweakOption(m_ui.tweakOptionTable, i++, false); // CDROM Region Check
- setBooleanTweakOption(m_ui.tweakOptionTable, i++, false); // Allow booting without SBI file
- setBooleanTweakOption(m_ui.tweakOptionTable, i++, false); // Enable PCDRV
- setBooleanTweakOption(m_ui.tweakOptionTable, i++, false); // Enable PCDRV Writes
- setDirectoryOption(m_ui.tweakOptionTable, i++, ""); // PCDrv Root Directory
+ Settings::DEFAULT_CDROM_MECHACON_VERSION); // CDROM Mechacon Version
+ setBooleanTweakOption(m_ui.tweakOptionTable, i++, false); // CDROM Region Check
+ setBooleanTweakOption(m_ui.tweakOptionTable, i++, false); // Allow booting without SBI file
+ setBooleanTweakOption(m_ui.tweakOptionTable, i++, false); // Enable PINE
+ setIntRangeTweakOption(m_ui.tweakOptionTable, i++, Settings::DEFAULT_PINE_SLOT); // PINE Slot
+ setBooleanTweakOption(m_ui.tweakOptionTable, i++, false); // Enable PCDRV
+ setBooleanTweakOption(m_ui.tweakOptionTable, i++, false); // Enable PCDRV Writes
+ setDirectoryOption(m_ui.tweakOptionTable, i++, ""); // PCDrv Root Directory
return;
}
@@ -322,6 +328,8 @@ void AdvancedSettingsWidget::onResetToDefaultClicked()
sif->DeleteValue("CDROM", "MechaconVersion");
sif->DeleteValue("CDROM", "RegionCheck");
sif->DeleteValue("CDROM", "AllowBootingWithoutSBIFile");
+ sif->DeleteValue("PINE", "Enabled");
+ sif->DeleteValue("PINE", "Slot");
sif->DeleteValue("PCDrv", "Enabled");
sif->DeleteValue("PCDrv", "EnableWrites");
sif->DeleteValue("PCDrv", "Root");