// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team // SPDX-License-Identifier: GPL-3.0+ #include "BuildVersion.h" #include "Common.h" #include "Host.h" #include "Elfheader.h" #include "GS.h" #include "GS/GSPerfMon.h" #include "GS/Renderers/Common/GSDevice.h" #include "MTGS.h" #include "PerformanceMetrics.h" #include "SaveState.h" #include "PINE.h" #include "VMManager.h" #include "vtlb.h" #include "common/Error.h" #include "common/FPControl.h" #include "common/MemorySettingsInterface.h" #include "common/SettingsInterface.h" #include "common/SettingsWrapper.h" #include "common/Threading.h" #include #include #include #include #include #include #include "fmt/format.h" #if defined(_WIN32) #define read_portable(a, b, c) (recv(a, (char*)b, c, 0)) #define write_portable(a, b, c) (send(a, (const char*)b, c, 0)) #define safe_close_portable(a) \ do \ { \ if ((a) >= 0) \ { \ closesocket((a)); \ (a) = INVALID_SOCKET; \ } \ } while (0) #include "common/RedtapeWindows.h" #include #elif defined(__linux__) || defined(__FreeBSD__) #define read_portable(a, b, c) (read(a, b, c)) #define write_portable(a, b, c) (send(a, b, c, MSG_NOSIGNAL)) #define safe_close_portable(a) \ do \ { \ if ((a) >= 0) \ { \ close((a)); \ (a) = -1; \ } \ } while (0) #include #include #include #else #define read_portable(a, b, c) (read(a, b, c)) #define write_portable(a, b, c) (write(a, b, c)) #define safe_close_portable(a) \ do \ { \ if ((a) >= 0) \ { \ close((a)); \ (a) = -1; \ } \ } while (0) #include #include #include #endif #define PINE_EMULATOR_NAME "pcsx2" // Which socket family PINE listens on is a property of the host, not of the protocol: the wire // format below is identical either way, and reference clients already speak both because Windows // has always needed TCP. // // Android joins Windows on the TCP side for a different reason. A Unix socket needs a path both // sides can name, and Android gives us nowhere to put one: there is no /tmp, XDG_RUNTIME_DIR is // unset, so the existing code would fall back to binding "/tmp/pcsx2.sock" and simply fail. The // writable directories that do exist are all app-private, and every plausible client -- adb, a // shell, another app -- runs under a different uid, so a socket placed there binds successfully // and then admits nobody. Loopback TCP is the transport Android actually supports reaching into: // "adb forward" bridges a device port to the workstation, which is what makes a handheld // debuggable from a desk at all. #if defined(_WIN32) || defined(__ANDROID__) #define PINE_TCP_TRANSPORT 1 #endif // sockaddr_in and htons()/htonl(). Windows already has them via WinSock2.h above; the POSIX // spelling lives in headers the Unix-socket path never needed, so it is pulled in only here. #if defined(PINE_TCP_TRANSPORT) && !defined(_WIN32) #include #include #endif // The TCP setup below is now shared, so its failure comparisons have to be too. Winsock spells // these as named constants and POSIX returns -1 from both socket() and bind(); neither name // exists on the other platform. #ifdef _WIN32 #define PINE_INVALID_SOCKET INVALID_SOCKET #define PINE_SOCKET_ERROR SOCKET_ERROR #else #define PINE_INVALID_SOCKET (-1) #define PINE_SOCKET_ERROR (-1) #endif #ifdef _WIN32 static bool InitializeWinsock() { static bool initialized = false; if (initialized) return true; WSADATA wsa = {}; if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) return false; initialized = true; std::atexit([]() { WSACleanup(); }); return true; } #endif namespace PINEServer { static std::thread s_thread; static int s_slot; // Two independent splits meet here, which is why the guards differ. The socket HANDLE type is // a Windows API question -- SOCKET is an opaque handle there, a file descriptor everywhere // else -- while the socket NAME only exists when the transport is a Unix socket, and Android // is POSIX with no socket name at all. #ifdef _WIN32 // windows claim to have support for AF_UNIX sockets but that is a blatant lie, // their SDK won't even run their own examples, so we go on TCP sockets. static SOCKET s_sock = INVALID_SOCKET; // the message socket used in thread's accept(). static SOCKET s_msgsock = INVALID_SOCKET; #else static int s_sock = -1; // the message socket used in thread's accept(). static int s_msgsock = -1; #endif #ifndef PINE_TCP_TRANSPORT // absolute path of the socket. Stored in XDG_RUNTIME_DIR, if unset /tmp static std::string s_socket_name; #endif // Whether the socket processing thread should stop executing/is stopped. static std::atomic_bool s_end{true}; /** * Maximum memory used by an IPC message request. * Equivalent to 50,000 Write64 requests. */ #define MAX_IPC_SIZE 650000 /** * Maximum memory used by an IPC message reply. * Equivalent to 50,000 Read64 replies. */ #define MAX_IPC_RETURN_SIZE 450000 /** * IPC return buffer. * A preallocated buffer used to store all IPC replies. * to the size of 50.000 MsgWrite64 IPC calls. */ static std::vector s_ret_buffer; /** * IPC messages buffer. * A preallocated buffer used to store all IPC messages. */ static std::vector s_ipc_buffer; /** * 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 : unsigned char { 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 ARMSX2 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. */ // ARMSX2-local extensions. Upstream PINE stops at 0xF; these are private to // this fork, so a generic PINE client will simply never send them. MsgGetStats = 0x10, /**< Returns host-side performance statistics as JSON. */ MsgGetSetting = 0x11, /**< Reads a setting by section/key. */ MsgSetSetting = 0x12, /**< Writes a setting by section/key and applies it. */ MsgFrameAdvance = 0x13, /**< Advances a paused VM by one frame. */ MsgGSDump = 0x14, /**< Records a GS dump of the next N frames. */ MsgGetEffectiveSetting = 0x15, /**< Reads what a setting is actually running as. */ MsgUnimplemented = 0xFF /**< Unimplemented IPC message. */ }; /** * Emulator status enum. * A list of possible emulator statuses. */ enum EmuStatus : uint32_t { Running = 0, /**< Game is running */ Paused = 1, /**< Game is paused */ Shutdown = 2 /**< Game is shutdown */ }; /** * IPC message buffer. * A list of all needed fields to store an IPC message. */ struct IPCBuffer { int size; /**< Size of the buffer. */ std::vector buffer; /**< Buffer. */ }; /** * 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. */ enum IPCResult : unsigned char { IPC_OK = 0, /**< IPC command successfully completed. */ IPC_FAIL = 0xFF /**< IPC command failed to complete. */ }; // Thread used to relay IPC commands. void MainLoop(); void ClientLoop(); /** * Internal function, Parses an IPC command. * buf: buffer containing the IPC command. * buf_size: size of the buffer announced. * ret_buffer: buffer that will be used to send the reply. * return value: IPCBuffer containing a buffer with the result * of the command and its size. */ static IPCBuffer ParseCommand(std::span buf, std::vector& ret_buffer, u32 buf_size); /** * Formats an IPC buffer * ret_buffer: return buffer to use. * size: size of the IPC buffer. * return value: buffer containing the status code allocated of size */ static std::vector& MakeOkIPC(std::vector& ret_buffer, uint32_t size); static std::vector& MakeFailIPC(std::vector& ret_buffer, uint32_t size); /** * Initializes an open socket for IPC communication. */ bool AcceptClient(); /** * Converts a primitive value to bytes in little endian * res_vector: the vector to modify * res: the value to convert * i: where to insert it into the vector * NB: implicitely inlined */ template static void ToResultVector(std::vector& res_vector, T res, int i) { memcpy(&res_vector[i], (char*)&res, sizeof(T)); } /** * Converts bytes in little endian to a primitive value * span: the span to convert * i: where to load it from the span * return value: the converted value * NB: implicitely inlined */ template static T FromSpan(std::span span, int i) { return *(T*)(&span[i]); } /** * Ensures an IPC message isn't too big. * return value: false if checks failed, true otherwise. */ static inline bool SafetyChecks(u32 command_len, int command_size, u32 reply_len, int reply_size = 0, u32 buf_size = MAX_IPC_SIZE - 1) { return !((command_len + command_size) > buf_size || (reply_len + reply_size) >= MAX_IPC_RETURN_SIZE); } /** * Reads a length-prefixed ([u32 len][len bytes], no NUL) string argument, advancing * buf_cnt past it. Returns false if the declared length runs off the end of the * request, in which case the caller must fail the command. */ static bool ReadLengthPrefixedString(std::span buf, u32& buf_cnt, u32 buf_size, std::string* out) { if ((buf_cnt + 4) > buf_size) return false; const u32 len = FromSpan(buf, buf_cnt); buf_cnt += 4; // Bound the allocation by what the request could actually contain. if (len > buf_size || (buf_cnt + len) > buf_size) return false; out->assign(reinterpret_cast(&buf[buf_cnt]), len); buf_cnt += len; return true; } /** * Builds the host-side statistics document. Called on the PINE thread. * * PerformanceMetrics and g_perfmon are plain scalar reads -- racy against the GS * thread but benign, since every field is an independently-meaningful number and a * torn sample only costs one stale stat. GSgetStats/GSgetMemoryStats are NOT safe * here: they dereference g_texture_cache and g_gs_device, which are GS-thread * owned, so the texture-cache memory figures are gathered via RunOnGSThread. */ static std::string BuildStatsJson() { SmallString gs_memory; // Device identity is the axis nearly every mobile GPU bug turns on, so a stats // blob without it is not attributable to a driver. Adreno tiers run drivers // (Mesa Turnip vs Qualcomm proprietary) that behave oppositely for fbfetch and // push descriptors, so the driver string matters more than the device name. std::string device_name, driver_info; if (MTGS::IsOpen()) { // The MTGS ring is single-producer: m_WritePos is owned by the EE/CPU // thread, so only it may push packets. RunOnGSThread pushes a packet, and // WaitGS drains the ring -- calling them from the PINE thread makes a second // producer that races the EE thread's own ring writes, desyncing the // pending-packet count. That lost-wakeup-deadlocks the EE thread (parked in // WaitGS waiting for empty) against the GS thread (asleep in WaitForWork on // no work). Marshal onto the CPU thread first -- the legitimate producer -- // and block until the GS-owned sample has been gathered. Host::RunOnCPUThread( [&gs_memory, &device_name, &driver_info]() { MTGS::RunOnGSThread([&gs_memory, &device_name, &driver_info]() { GSgetMemoryStats(gs_memory); if (g_gs_device) { device_name = g_gs_device->GetName(); driver_info = g_gs_device->GetDriverInfo(); } }); MTGS::WaitGS(false); }, true); } // Newlines and quotes would break the JSON; GetDriverInfo() is multi-line on Vulkan. const auto sanitize = [](std::string& s) { for (char& c : s) { if (c == '\n' || c == '\r' || c == '\t') c = ' '; else if (c == '"' || c == '\\') c = '\''; } }; sanitize(device_name); sanitize(driver_info); const auto counter = [](GSPerfMon::counter_t c) { return g_perfmon.Get(c); }; return fmt::format( "{{" "\"fps\":{:.3f},\"internal_fps\":{:.3f},\"speed\":{:.3f}," "\"frame_ms_avg\":{:.3f},\"frame_ms_min\":{:.3f},\"frame_ms_max\":{:.3f}," "\"cpu_thread_pct\":{:.3f},\"cpu_thread_ms\":{:.3f}," "\"gs_thread_pct\":{:.3f},\"gs_thread_ms\":{:.3f}," "\"gs_back_thread_pct\":{:.3f},\"gs_back_thread_ms\":{:.3f}," "\"vu_thread_pct\":{:.3f},\"vu_thread_ms\":{:.3f}," "\"gpu_pct\":{:.3f},\"gpu_ms_avg\":{:.3f},\"gpu_ms_last\":{:.3f}," "\"gpu_vs_invocations\":{:.0f},\"gpu_ps_invocations\":{:.0f}," "\"prims\":{:.1f},\"draws\":{:.1f},\"draw_calls\":{:.1f}," "\"render_passes\":{:.1f},\"barriers\":{:.1f},\"readbacks\":{:.1f}," "\"texture_copies\":{:.1f},\"texture_uploads\":{:.1f}," "\"draw_calls_rov\":{:.1f},\"barriers_rov\":{:.1f},\"texture_copies_rov\":{:.1f}," "\"tc_source_hit\":{:.1f},\"tc_source_miss\":{:.1f}," "\"tc_target_hit\":{:.1f},\"tc_target_miss\":{:.1f}," "\"hash_cache_hit\":{:.1f},\"hash_cache_miss\":{:.1f}," "\"gs_memory\":\"{}\",\"frame_number\":{},\"gs_front_parser\":{}," "\"renderer\":\"{}\",\"device_name\":\"{}\",\"driver_info\":\"{}\"" "}}", PerformanceMetrics::GetFPS(), PerformanceMetrics::GetInternalFPS(), PerformanceMetrics::GetSpeed(), PerformanceMetrics::GetAverageFrameTime(), PerformanceMetrics::GetMinimumFrameTime(), PerformanceMetrics::GetMaximumFrameTime(), PerformanceMetrics::GetCPUThreadUsage(), PerformanceMetrics::GetCPUThreadAverageTime(), PerformanceMetrics::GetGSThreadUsage(), PerformanceMetrics::GetGSThreadAverageTime(), // Zero unless GSBackThreadMode >= Lockstep. gs_thread_* is the MTGS thread only, // so under the split the two have to be read together to see the GS cost. PerformanceMetrics::GetGSBackThreadUsage(), PerformanceMetrics::GetGSBackThreadAverageTime(), PerformanceMetrics::GetVUThreadUsage(), PerformanceMetrics::GetVUThreadAverageTime(), PerformanceMetrics::GetGPUUsage(), PerformanceMetrics::GetGPUAverageTime(), PerformanceMetrics::GetLastGPUTime(), PerformanceMetrics::GetGPUAverageVSInvocations(), PerformanceMetrics::GetGPUAveragePSInvocations(), counter(GSPerfMon::Prim), counter(GSPerfMon::Draw), counter(GSPerfMon::DrawCalls), counter(GSPerfMon::RenderPasses), counter(GSPerfMon::Barriers), counter(GSPerfMon::Readbacks), counter(GSPerfMon::TextureCopies), counter(GSPerfMon::TextureUploads), counter(GSPerfMon::DrawCallsROV), counter(GSPerfMon::BarriersROV), counter(GSPerfMon::TextureCopiesROV), counter(GSPerfMon::TCSourceHit), counter(GSPerfMon::TCSourceMiss), counter(GSPerfMon::TCTargetHit), counter(GSPerfMon::TCTargetMiss), counter(GSPerfMon::HashCacheHit), counter(GSPerfMon::HashCacheMiss), // Whether the two-object split actually engaged, which the BackThreadMode setting // alone does not tell you -- it downgrades to lockstep on an unsupported config. // True is also what makes gs_back_thread_* worth reading next to gs_thread_*. gs_memory.view(), PerformanceMetrics::GetFrameNumber(), GSHasFrontParser() ? "true" : "false", Pcsx2Config::GSOptions::GetRendererName(EmuConfig.GS.Renderer), device_name, driver_info); } /** * Escapes a string for embedding in a JSON string literal. Only paths go through this * -- a Windows path is full of backslashes, which would otherwise leave the reply * unparseable. BuildStatsJson's sanitizer is a different job: it flattens driver blurb * that is allowed to lose fidelity, whereas a path the client is about to open is not. */ static std::string JsonEscape(const std::string_view str) { std::string out; out.reserve(str.size()); for (const char c : str) { if (c == '"' || c == '\\') out.push_back('\\'); out.push_back(c); } return out; } /** * Reports what a setting is actually running as, which on any game carrying GameDB fixes is * NOT what the INI says: applyGSHardwareFixes runs after the settings load and rewrites * EmuConfig in memory without ever touching the file. Reading the persisted value alone gets * you told that autoflush is off while the renderer is running it at 2. * * The measurement hazard is worse than the confusion. A settings A/B that writes a key to * "off" measures the GameDB value in BOTH arms -- because GameDB re-applies it after every * settings load -- while the two arms report different settings. That is a wrong answer with * no symptom, on exactly the titles worth investigating. * * Effective values come from serialising the live EmuConfig back out through the same wrapper * that writes the INI, so they land under the identical section/key names the caller already * uses, and every setting is covered for free. A hand-written key map would need extending by * every future setting, and the one that got missed would be the one somebody trusted. * * `known` false means the key is not part of Pcsx2Config at all -- EnableFastBoot, the UI * section, anything host-side. That is not an error: it says the persisted value is the whole * truth for that key, which is worth telling apart from "both agree". * * `differs` is deliberately a claim about the two STRINGS and not about the cause. Something * mutated the config after it was loaded -- a GameDB fix, safe-mode masking, or a settings * layer this query does not read -- and which one it was is not knowable from here. Naming it * "overridden" would be inventing the reason. */ static std::string BuildEffectiveSettingJson(const std::string& section, const std::string& key) { MemorySettingsInterface effective_si; { // Serialise a copy, so nothing the wrapper does can reach the live config. The FPCR // backup matches every other Pcsx2Config round-trip in the tree: the struct carries FP // control state, and this runs on the PINE thread, whose FPCR is its own to restore. FPControlRegisterBackup fpcr_backup(FPControlRegister::GetDefault()); Pcsx2Config snapshot = EmuConfig; SettingsSaveWrapper wrapper(effective_si); snapshot.LoadSave(wrapper); } std::string effective; const bool known = effective_si.GetStringValue(section.c_str(), key.c_str(), &effective); std::string persisted; { auto lock = Host::GetSettingsLock(); persisted = Host::GetSettingsInterface()->GetStringValue(section.c_str(), key.c_str(), ""); } // An unpersisted key reads back empty, which means "never written", not "set to empty". // Counting that as a difference would flag most of the config the moment anyone asked, // since the INI only stores what has been changed away from its default. const bool differs = known && !persisted.empty() && persisted != effective; return fmt::format( "{{\"section\":\"{}\",\"key\":\"{}\",\"effective\":\"{}\",\"persisted\":\"{}\"," "\"known\":{},\"differs\":{}}}", JsonEscape(section), JsonEscape(key), JsonEscape(effective), JsonEscape(persisted), known ? "true" : "false", differs ? "true" : "false"); } /** * Queues a GS dump of the next `frames` frames, or stops a dump already recording when * `frames` is zero -- the same pair of actions the GSDumpMultiFrame hotkey binds to press * and release. `path` may be empty for the usual auto-named file under the snapshots * folder. Returns false only if there is no GS to dump, which fails the command. * * Marshalled the same way as BuildStatsJson and for the same reason: the MTGS ring is * single-producer and the PINE thread is not that producer, so pushing a packet from * here races the EE thread's own writes and can deadlock the two of them. Hop to the CPU * thread, push from there, and block until the GS thread has taken the request. * * The reply describes the request, not a finished file. The dump is opened on the next * VSync and a multi-frame dump is closed some frames after that, so a client that waits * for the file has to keep the VM running or step it with MsgFrameAdvance -- a paused VM * never reaches the VSync that writes anything. A screenshot lands next to the dump too; * that is inherent to the snapshot path, not something this command adds. * * `queued` false carries a `reason`, because both ways a request can be turned away are * served commands that quietly did nothing rather than socket-level failures. The reasons * are not symmetric: "snapshot pending" is a request that arrived between another one and * the VSync servicing it, and retrying a frame later works. "already recording" is a * refusal -- the snapshot path creates a dump only when none exists, so a second request * would take a screenshot and write no dump. Handing back a path for a file that will never * appear is the worst of the available answers, so stop the running dump first if you mean * to. (Refusing here also predates the fix for the truncation that used to follow: the * request and the recording shared a frame counter, so a second one cut the first short. * They are separate fields now -- see GSSnapshotPolicy.h -- and the refusal stands on the * unanswerable-path argument alone.) */ static bool QueueGSDump(u32 frames, std::string path, std::string* reply) { if (!MTGS::IsOpen()) return false; // QueueSnapshot honours a caller-supplied path only when it ends in .png, which it // strips to get the base name shared by the screenshot and the dump; anything else is // silently discarded in favour of an auto-named file in the snapshots folder. Silence // is the wrong failure for a scripted client -- it writes somewhere the script never // looks -- so meet that contract here instead. Drop a dump or image suffix if the // caller spelled one out, so naming the file you want does not earn you a doubled // extension, and let what is left be the base name. if (!path.empty()) { for (const std::string_view suffix : {".gs.zst", ".gs.xz", ".gs", ".png"}) { if (StringUtil::EndsWithNoCase(path, suffix)) { path.erase(path.size() - suffix.size()); break; } } } bool queued = false, stopped = false; std::string base; const char* dump_ext = ""; const char* reason = ""; Host::RunOnCPUThread( [&]() { MTGS::RunOnGSThread([&]() { if (frames == 0) { GSStopGSDump(); stopped = true; return; } if (GSIsDumpRecording()) { reason = "already recording"; return; } // Resolve the auto-name here rather than leaving it to QueueSnapshot, so the // reply can name the exact file the client is about to wait for. base = path.empty() ? GSGetBaseSnapshotFilename() : path; queued = GSQueueSnapshot(base + ".png", frames); if (!queued) { reason = "snapshot pending"; return; } // GSConfig is the GS thread's copy of the config, so read the compression // method here -- it decides the extension the dump writer will append. switch (GSConfig.GSDumpCompression) { case GSDumpCompressionMethod::Uncompressed: dump_ext = ".gs"; break; case GSDumpCompressionMethod::LZMA: dump_ext = ".gs.xz"; break; default: dump_ext = ".gs.zst"; break; } }); MTGS::WaitGS(false); }, true); *reply = fmt::format( "{{\"queued\":{},\"stopped\":{},\"frames\":{},\"path\":\"{}\",\"reason\":\"{}\"}}", queued ? "true" : "false", stopped ? "true" : "false", frames, queued ? JsonEscape(base + dump_ext) : std::string(), reason); return true; } } // namespace PINEServer bool PINEServer::Initialize(int slot) { s_end.store(false, std::memory_order_release); s_slot = slot; #ifdef _WIN32 if (!InitializeWinsock()) { Console.WriteLn(Color_Red, "PINE: Cannot initialize winsock! Shutting down..."); Deinitialize(); return false; } #endif #ifdef PINE_TCP_TRANSPORT s_sock = socket(AF_INET, SOCK_STREAM, 0); if ((s_sock == PINE_INVALID_SOCKET) || slot > 65536) { Console.WriteLn(Color_Red, "PINE: Cannot open socket! Shutting down..."); Deinitialize(); return false; } // A listener that survives its own process is worse than one that fails to start: the port // sits in TIME_WAIT after a crash or a fast restart, bind() refuses, and PINE looks broken // for a minute or so for reasons nothing reports. This matters far more on a handheld than // on a desktop, because killing and relaunching the app is the normal debugging loop there. #ifndef _WIN32 const int reuse = 1; setsockopt(s_sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); #endif sockaddr_in server = {}; server.sin_family = AF_INET; server.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // localhost only server.sin_port = htons(slot); if (bind(s_sock, (struct sockaddr*)&server, sizeof(server)) == PINE_SOCKET_ERROR) { Console.WriteLn(Color_Red, "PINE: Error while binding to socket! Shutting down..."); Deinitialize(); return false; } // Loopback is not reachable from another machine, so on a handheld the port is useless until // something bridges it. Name the bridge in the log rather than leaving the reader to work it // out: on Android this line is the entire setup instruction for driving the device from a // workstation. Desktop hosts get the address only -- there the client is already local, and // telling a Windows user to run adb would just be wrong. #ifdef __ANDROID__ Console.WriteLnFmt("PINE: Listening on 127.0.0.1:{} -- bridge it with: adb forward tcp:{} tcp:{}", slot, slot, slot); #else Console.WriteLnFmt("PINE: Listening on 127.0.0.1:{}.", slot); #endif #else char* runtime_dir = nullptr; #ifdef __APPLE__ runtime_dir = std::getenv("TMPDIR"); #else runtime_dir = std::getenv("XDG_RUNTIME_DIR"); #endif // fallback in case macOS or other OSes don't implement the XDG base // spec if (runtime_dir == nullptr) s_socket_name = "/tmp/" PINE_EMULATOR_NAME ".sock"; else { s_socket_name = runtime_dir; s_socket_name += "/" PINE_EMULATOR_NAME ".sock"; } if (slot != PINE_DEFAULT_SLOT) s_socket_name += "." + std::to_string(slot); struct sockaddr_un server; s_sock = socket(AF_UNIX, SOCK_STREAM, 0); if (s_sock < 0) { Console.WriteLn(Color_Red, "PINE: Cannot open socket! Shutting down..."); Deinitialize(); return false; } server.sun_family = AF_UNIX; StringUtil::Strlcpy(server.sun_path, s_socket_name, sizeof(server.sun_path)); // we unlink the socket so that when releasing this thread the socket gets // freed even if we didn't close correctly the loop unlink(s_socket_name.c_str()); if (bind(s_sock, (struct sockaddr*)&server, sizeof(struct sockaddr_un))) { Console.WriteLn(Color_Red, "PINE: Error while binding to socket! Shutting down..."); Deinitialize(); return false; } #endif // maximum queue of 4096 commands before refusing, approximated to the // nearest legal value. We do not use SOMAXCONN as windows have this idea // that a "reasonable" value is 5, which is not. if (listen(s_sock, 4096)) { Console.WriteLn(Color_Red, "PINE: Cannot listen for connections! Shutting down..."); Deinitialize(); return false; } // we allocate once buffers to not have to do mallocs for each IPC // request, as malloc is expansive when we optimize for µs. s_ret_buffer.resize(MAX_IPC_RETURN_SIZE); s_ipc_buffer.resize(MAX_IPC_SIZE); // we start the thread s_thread = std::thread(&PINEServer::MainLoop); return true; } bool PINEServer::IsInitialized() { return !s_end.load(std::memory_order_acquire); } int PINEServer::GetSlot() { return s_slot; } std::vector& PINEServer::MakeOkIPC(std::vector& ret_buffer, uint32_t size = 5) { ToResultVector(ret_buffer, size, 0); ret_buffer[4] = IPC_OK; return ret_buffer; } std::vector& PINEServer::MakeFailIPC(std::vector& ret_buffer, uint32_t size = 5) { ToResultVector(ret_buffer, size, 0); ret_buffer[4] = IPC_FAIL; return ret_buffer; } bool PINEServer::AcceptClient() { s_msgsock = accept(s_sock, 0, 0); if (s_msgsock < 0) { // everything else is non recoverable in our scope // we also mark as recoverable socket errors where it would block a // non blocking socket, even though our socket is blocking, in case // we ever have to implement a non blocking socket. #ifdef _WIN32 const int errno_w = WSAGetLastError(); if (!(errno_w == WSAECONNRESET || errno_w == WSAEINTR || errno_w == WSAEINPROGRESS || errno_w == WSAEMFILE || errno_w == WSAEWOULDBLOCK) && s_sock != INVALID_SOCKET) Console.Error("PINE: accept() returned error %d", errno_w); #else if (!(errno == ECONNABORTED || errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) && s_sock >= 0) Console.Error("PINE: accept() returned error %d", errno); #endif return false; } #ifdef __APPLE__ int nosigpipe = 1; setsockopt(s_msgsock, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); #endif // Gross C-style cast, but SOCKET is a handle on Windows. Console.WriteLn("PINE: New client with FD %d connected.", (int)s_msgsock); return true; } void PINEServer::MainLoop() { Threading::SetNameOfCurrentThread("PINE Server"); while (!s_end.load(std::memory_order_acquire)) { if (!AcceptClient()) continue; ClientLoop(); Console.WriteLn("PINE: Client disconnected."); safe_close_portable(s_msgsock); } } void PINEServer::ClientLoop() { while (!s_end.load(std::memory_order_acquire)) { // either int or ssize_t depending on the platform, so we have to // use a bunch of auto auto receive_length = 0; auto end_length = 4; const std::span ipc_buffer_span(s_ipc_buffer); // while we haven't received the entire packet, maybe due to // socket datagram splittage, we continue to read while (receive_length < end_length) { const auto tmp_length = read_portable(s_msgsock, &ipc_buffer_span[receive_length], MAX_IPC_SIZE - receive_length); // we recreate the socket if an error happens if (tmp_length <= 0) return; receive_length += tmp_length; // if we got at least the final size then update if (end_length == 4 && receive_length >= 4) { end_length = FromSpan(ipc_buffer_span, 0); // we'd like to avoid a client trying to do OOB if (end_length > MAX_IPC_SIZE || end_length < 4) { receive_length = 0; break; } } } PINEServer::IPCBuffer res; // we remove 4 bytes to get the message size out of the IPC command // size in ParseCommand. // also, if we got a failed command, let's reset the state so we don't // end up deadlocking by getting out of sync, eg when a client // disconnects if (receive_length != 0) { res = ParseCommand(ipc_buffer_span.subspan(4), s_ret_buffer, (u32)end_length - 4); // if we cannot send back our answer restart the socket if (write_portable(s_msgsock, res.buffer.data(), res.size) < 0) return; } } } void PINEServer::Deinitialize() { s_end.store(true, std::memory_order_release); // Removing the socket file is about the Unix-socket transport, not about being POSIX: a TCP // listener has no filesystem entry to clean up, and Android has no s_socket_name to read. #ifndef PINE_TCP_TRANSPORT if (!s_socket_name.empty()) { unlink(s_socket_name.c_str()); s_socket_name = {}; } #endif // shutdown() is needed, otherwise accept() will still block. #ifdef _WIN32 if (s_sock != INVALID_SOCKET) shutdown(s_sock, SD_BOTH); #else if (s_sock >= 0) shutdown(s_sock, SHUT_RDWR); #endif safe_close_portable(s_sock); safe_close_portable(s_msgsock); if (s_thread.joinable()) s_thread.join(); } PINEServer::IPCBuffer PINEServer::ParseCommand(std::span buf, std::vector& ret_buffer, u32 buf_size) { u32 ret_cnt = 5; u32 buf_cnt = 0; while (buf_cnt < buf_size) { if (!SafetyChecks(buf_cnt, 1, ret_cnt, 0, buf_size)) [[unlikely]] return IPCBuffer{5, MakeFailIPC(ret_buffer)}; buf_cnt++; // 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 switch ((IPCCommand)buf[buf_cnt - 1]) { case MsgRead8: { if (!VMManager::HasValidVM()) goto error; if (!SafetyChecks(buf_cnt, 4, ret_cnt, 1, buf_size)) [[unlikely]] goto error; const u32 a = FromSpan(buf, buf_cnt); const u8 res = vtlb_ramRead(a); ToResultVector(ret_buffer, res, ret_cnt); ret_cnt += 1; buf_cnt += 4; break; } case MsgRead16: { if (!VMManager::HasValidVM()) goto error; if (!SafetyChecks(buf_cnt, 4, ret_cnt, 2, buf_size)) [[unlikely]] goto error; const u32 a = FromSpan(buf, buf_cnt); const u16 res = vtlb_ramRead(a); ToResultVector(ret_buffer, res, ret_cnt); ret_cnt += 2; buf_cnt += 4; break; } case MsgRead32: { if (!VMManager::HasValidVM()) goto error; if (!SafetyChecks(buf_cnt, 4, ret_cnt, 4, buf_size)) [[unlikely]] goto error; const u32 a = FromSpan(buf, buf_cnt); const u32 res = vtlb_ramRead(a); ToResultVector(ret_buffer, res, ret_cnt); ret_cnt += 4; buf_cnt += 4; break; } case MsgRead64: { if (!VMManager::HasValidVM()) goto error; if (!SafetyChecks(buf_cnt, 4, ret_cnt, 8, buf_size)) [[unlikely]] goto error; const u32 a = FromSpan(buf, buf_cnt); const u64 res = vtlb_ramRead(a); ToResultVector(ret_buffer, res, ret_cnt); ret_cnt += 8; buf_cnt += 4; break; } case MsgWrite8: { if (!VMManager::HasValidVM()) goto error; if (!SafetyChecks(buf_cnt, 1 + 4, ret_cnt, 0, buf_size)) [[unlikely]] goto error; const u32 a = FromSpan(buf, buf_cnt); vtlb_ramWrite(a, FromSpan(buf, buf_cnt + 4)); buf_cnt += 5; break; } case MsgWrite16: { if (!VMManager::HasValidVM()) goto error; if (!SafetyChecks(buf_cnt, 2 + 4, ret_cnt, 0, buf_size)) [[unlikely]] goto error; const u32 a = FromSpan(buf, buf_cnt); vtlb_ramWrite(a, FromSpan(buf, buf_cnt + 4)); buf_cnt += 6; break; } case MsgWrite32: { if (!VMManager::HasValidVM()) goto error; if (!SafetyChecks(buf_cnt, 4 + 4, ret_cnt, 0, buf_size)) [[unlikely]] goto error; const u32 a = FromSpan(buf, buf_cnt); vtlb_ramWrite(a, FromSpan(buf, buf_cnt + 4)); buf_cnt += 8; break; } case MsgWrite64: { if (!VMManager::HasValidVM()) goto error; if (!SafetyChecks(buf_cnt, 8 + 4, ret_cnt, 0, buf_size)) [[unlikely]] goto error; const u32 a = FromSpan(buf, buf_cnt); vtlb_ramWrite(a, FromSpan(buf, buf_cnt + 4)); buf_cnt += 12; break; } case MsgVersion: { u32 size = strlen(BuildVersion::GitRev) + 8; if (!SafetyChecks(buf_cnt, 0, ret_cnt, size + 4, buf_size)) [[unlikely]] goto error; ToResultVector(ret_buffer, size, ret_cnt); ret_cnt += 4; snprintf(reinterpret_cast(&ret_buffer[ret_cnt]), size, "ARMSX2 %s", BuildVersion::GitRev); ret_cnt += size; break; } case MsgSaveState: { if (!VMManager::HasValidVM()) goto error; if (!SafetyChecks(buf_cnt, 1, ret_cnt, 0, buf_size)) [[unlikely]] goto error; Host::RunOnCPUThread([slot = FromSpan(buf, buf_cnt)] { VMManager::SaveStateToSlot(slot, true, [slot](const std::string& error) { SaveState_ReportSaveErrorOSD(error, slot); }); }); buf_cnt += 1; break; } case MsgLoadState: { if (!VMManager::HasValidVM()) goto error; if (!SafetyChecks(buf_cnt, 1, ret_cnt, 0, buf_size)) [[unlikely]] goto error; Host::RunOnCPUThread([slot = FromSpan(buf, buf_cnt)] { Error state_error; if (!VMManager::LoadStateFromSlot(slot, false, &state_error)) SaveState_ReportLoadErrorOSD(state_error.GetDescription(), slot, false); }); buf_cnt += 1; break; } case MsgTitle: { if (!VMManager::HasValidVM()) goto error; const std::string gameName = VMManager::GetTitle(false); const u32 size = gameName.size() + 1; if (!SafetyChecks(buf_cnt, 0, ret_cnt, size + 4, buf_size)) [[unlikely]] goto error; ToResultVector(ret_buffer, size, ret_cnt); ret_cnt += 4; memcpy(&ret_buffer[ret_cnt], gameName.c_str(), size); ret_cnt += size; break; } case MsgID: { if (!VMManager::HasValidVM()) goto error; const std::string gameSerial = VMManager::GetDiscSerial(); const u32 size = gameSerial.size() + 1; if (!SafetyChecks(buf_cnt, 0, ret_cnt, size + 4, buf_size)) [[unlikely]] goto error; ToResultVector(ret_buffer, size, ret_cnt); ret_cnt += 4; memcpy(&ret_buffer[ret_cnt], gameSerial.c_str(), size); ret_cnt += size; break; } case MsgUUID: { if (!VMManager::HasValidVM()) goto error; const std::string crc = fmt::format("{:08x}", VMManager::GetDiscCRC()); const u32 size = crc.size() + 1; if (!SafetyChecks(buf_cnt, 0, ret_cnt, size + 4, buf_size)) [[unlikely]] goto error; ToResultVector(ret_buffer, size, ret_cnt); ret_cnt += 4; memcpy(&ret_buffer[ret_cnt], crc.c_str(), size); ret_cnt += size; break; } case MsgGameVersion: { if (!VMManager::HasValidVM()) goto error; const std::string ElfVersion = VMManager::GetDiscVersion(); const u32 size = ElfVersion.size() + 1; if (!SafetyChecks(buf_cnt, 0, ret_cnt, size + 4, buf_size)) [[unlikely]] goto error; ToResultVector(ret_buffer, size, ret_cnt); ret_cnt += 4; memcpy(&ret_buffer[ret_cnt], ElfVersion.c_str(), size); ret_cnt += size; break; } case MsgStatus: { if (!SafetyChecks(buf_cnt, 0, ret_cnt, 4, buf_size)) [[unlikely]] goto error; EmuStatus status; switch (VMManager::GetState()) { case VMState::Running: status = EmuStatus::Running; break; case VMState::Paused: status = EmuStatus::Paused; break; default: status = EmuStatus::Shutdown; break; } ToResultVector(ret_buffer, status, ret_cnt); ret_cnt += 4; break; } case MsgGetStats: { const std::string stats = BuildStatsJson(); const u32 size = stats.size() + 1; if (!SafetyChecks(buf_cnt, 0, ret_cnt, size + 4, buf_size)) [[unlikely]] goto error; ToResultVector(ret_buffer, size, ret_cnt); ret_cnt += 4; memcpy(&ret_buffer[ret_cnt], stats.c_str(), size); ret_cnt += size; break; } case MsgGetSetting: { std::string section, key; if (!ReadLengthPrefixedString(buf, buf_cnt, buf_size, §ion) || !ReadLengthPrefixedString(buf, buf_cnt, buf_size, &key)) [[unlikely]] goto error; std::string value; { auto lock = Host::GetSettingsLock(); value = Host::GetSettingsInterface()->GetStringValue(section.c_str(), key.c_str(), ""); } const u32 size = value.size() + 1; if (!SafetyChecks(buf_cnt, 0, ret_cnt, size + 4, buf_size)) [[unlikely]] goto error; ToResultVector(ret_buffer, size, ret_cnt); ret_cnt += 4; memcpy(&ret_buffer[ret_cnt], value.c_str(), size); ret_cnt += size; break; } case MsgGetEffectiveSetting: { std::string section, key; if (!ReadLengthPrefixedString(buf, buf_cnt, buf_size, §ion) || !ReadLengthPrefixedString(buf, buf_cnt, buf_size, &key)) [[unlikely]] goto error; const std::string reply = BuildEffectiveSettingJson(section, key); const u32 size = reply.size() + 1; if (!SafetyChecks(buf_cnt, 0, ret_cnt, size + 4, buf_size)) [[unlikely]] goto error; ToResultVector(ret_buffer, size, ret_cnt); ret_cnt += 4; memcpy(&ret_buffer[ret_cnt], reply.c_str(), size); ret_cnt += size; break; } case MsgSetSetting: { std::string section, key, value; if (!ReadLengthPrefixedString(buf, buf_cnt, buf_size, §ion) || !ReadLengthPrefixedString(buf, buf_cnt, buf_size, &key) || !ReadLengthPrefixedString(buf, buf_cnt, buf_size, &value)) [[unlikely]] goto error; // Whether this change tears down the GS device, so the caller knows // whether it just paid for a reopen. Everything outside this set is // applied in place by GSUpdateConfig. const bool restart_required = Pcsx2Config::GSOptions::IsRestartOption(key.c_str()); // Write the persisted key rather than poking EmuConfig directly: a direct // poke is silently reverted by the next ApplySettings, which re-derives // EmuConfig from the INI layer stack. Host::SetBaseStringSettingValue(section.c_str(), key.c_str(), value.c_str()); Host::CommitBaseSettingChanges(); Host::RunOnCPUThread([]() { VMManager::ApplySettings(); }); const std::string reply = fmt::format("{{\"restart_required\":{}}}", restart_required ? "true" : "false"); const u32 size = reply.size() + 1; if (!SafetyChecks(buf_cnt, 0, ret_cnt, size + 4, buf_size)) [[unlikely]] goto error; ToResultVector(ret_buffer, size, ret_cnt); ret_cnt += 4; memcpy(&ret_buffer[ret_cnt], reply.c_str(), size); ret_cnt += size; break; } case MsgFrameAdvance: { if (!VMManager::HasValidVM()) goto error; if (!SafetyChecks(buf_cnt, 0, ret_cnt, 0, buf_size)) [[unlikely]] goto error; Host::RunOnCPUThread([]() { VMManager::FrameAdvance(1); }); break; } case MsgGSDump: { // [u32 frames][u32 path_len][path_len bytes]. frames == 0 stops a dump that is // already recording; UINT32_MAX records until something stops it. if (!VMManager::HasValidVM()) goto error; if (!SafetyChecks(buf_cnt, 4, ret_cnt, 0, buf_size)) [[unlikely]] goto error; const u32 frames = FromSpan(buf, buf_cnt); buf_cnt += 4; std::string path; if (!ReadLengthPrefixedString(buf, buf_cnt, buf_size, &path)) [[unlikely]] goto error; std::string reply; if (!QueueGSDump(frames, std::move(path), &reply)) [[unlikely]] goto error; const u32 size = reply.size() + 1; if (!SafetyChecks(buf_cnt, 0, ret_cnt, size + 4, buf_size)) [[unlikely]] goto error; ToResultVector(ret_buffer, size, ret_cnt); ret_cnt += 4; memcpy(&ret_buffer[ret_cnt], reply.c_str(), size); ret_cnt += size; break; } default: { error: return IPCBuffer{5, MakeFailIPC(ret_buffer)}; } } } return IPCBuffer{(int)ret_cnt, MakeOkIPC(ret_buffer, ret_cnt)}; }