From 4311e08726ed1faa18208ebf48de9107e637b86c Mon Sep 17 00:00:00 2001 From: Stenzek Date: Sat, 25 May 2024 23:49:19 +1000 Subject: [PATCH] System: Implement PINE server --- src/core/CMakeLists.txt | 2 + src/core/core.vcxproj | 2 + src/core/core.vcxproj.filters | 2 + src/core/pine_server.cpp | 555 ++++++++++++++++++ src/core/pine_server.h | 13 + src/core/settings.cpp | 7 + src/core/settings.h | 8 + src/core/system.cpp | 73 +++ src/core/system.h | 5 + src/duckstation-qt/advancedsettingswidget.cpp | 20 +- 10 files changed, 681 insertions(+), 6 deletions(-) create mode 100644 src/core/pine_server.cpp create mode 100644 src/core/pine_server.h 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");