Files
ARMSX2/pcsx2/PINE.cpp
T
Brian Degenhardt 7f93a80dd7 PerformanceMetrics: count the GS back thread
Under GSBackThreadMode >= Lockstep roughly half the GS work moves to a second
thread, and every surface that reports GS cost -- OSD, PerfLog, the Qt status
bar, PINE stats, gsrunner's @HWSTAT@ block -- measured the MTGS thread alone.
So the split read as a large GS saving. It is not: on a Rogue Galaxy savestate
here, mode 0 costs 15.8% / 2.63 ms and mode 3 costs 17.0% / 2.84 ms plus
14.2% / 2.37 ms on the back thread -- about twice the total GS CPU time, bought
to halve the critical path. That is a real trade, but nobody could see it.

The back thread registers its own handle at entry, as the SW rasterizer workers
do; StopBackThread clears it after the join. Unlike every other handle here it
is written by a thread other than the one sampling it, so the handle and its
running total sit behind a mutex taken twice a second. Installing a handle
rebases the total off it, so the first window after a GSreopen respawn measures
the new thread rather than its difference against the retired one's.

The figure is omitted, not reported as zero, wherever a back thread does not
exist -- otherwise a mode 0 vs mode 3 comparison reads a permanent 0% as
meaningful. gsrunner latches the presence flag during the run because DumpStats
executes after VMManager::Shutdown, by which point the thread has joined.
2026-07-30 21:55:58 -07:00

1125 lines
35 KiB
C++

// 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/SettingsInterface.h"
#include "common/Threading.h"
#include <atomic>
#include <cstdio>
#include <cstdlib>
#include <span>
#include <sys/types.h>
#include <thread>
#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 <WinSock2.h>
#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 <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#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 <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#endif
#define PINE_EMULATOR_NAME "pcsx2"
#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;
#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
// absolute path of the socket. Stored in XDG_RUNTIME_DIR, if unset /tmp
static std::string s_socket_name;
static int s_sock = -1;
// the message socket used in thread's accept().
static int s_msgsock = -1;
#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<u8> s_ret_buffer;
/**
* IPC messages buffer.
* A preallocated buffer used to store all IPC messages.
*/
static std::vector<u8> 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. */
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<u8> 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<u8> buf, std::vector<u8>& 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<u8>& MakeOkIPC(std::vector<u8>& ret_buffer, uint32_t size);
static std::vector<u8>& MakeFailIPC(std::vector<u8>& 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 <typename T>
static void ToResultVector(std::vector<u8>& 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 <typename T>
static T FromSpan(std::span<u8> 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<u8> buf, u32& buf_cnt, u32 buf_size, std::string* out)
{
if ((buf_cnt + 4) > buf_size)
return false;
const u32 len = FromSpan<u32>(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<const char*>(&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\":{},"
"\"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),
gs_memory.view(), PerformanceMetrics::GetFrameNumber(),
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;
}
/**
* 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, write no dump, and overwrite the frame counter of the dump
* already running, cutting it short. 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.
*/
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, incomplete = 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;
}
incomplete = GSHasFrontParser();
});
MTGS::WaitGS(false);
},
true);
*reply = fmt::format(
"{{\"queued\":{},\"stopped\":{},\"frames\":{},\"path\":\"{}\","
"\"pipelined_incomplete\":{},\"reason\":\"{}\"}}",
queued ? "true" : "false", stopped ? "true" : "false", frames,
queued ? JsonEscape(base + dump_ext) : std::string(), incomplete ? "true" : "false", 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;
}
s_sock = socket(AF_INET, SOCK_STREAM, 0);
if ((s_sock == INVALID_SOCKET) || slot > 65536)
{
Console.WriteLn(Color_Red, "PINE: Cannot open socket! Shutting down...");
Deinitialize();
return false;
}
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)) == SOCKET_ERROR)
{
Console.WriteLn(Color_Red, "PINE: Error while binding to socket! Shutting down...");
Deinitialize();
return false;
}
#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<u8>& PINEServer::MakeOkIPC(std::vector<u8>& ret_buffer, uint32_t size = 5)
{
ToResultVector<uint32_t>(ret_buffer, size, 0);
ret_buffer[4] = IPC_OK;
return ret_buffer;
}
std::vector<u8>& PINEServer::MakeFailIPC(std::vector<u8>& ret_buffer, uint32_t size = 5)
{
ToResultVector<uint32_t>(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<u8> 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<u32>(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);
#ifndef _WIN32
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<u8> buf, std::vector<u8>& 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<u32>(buf, buf_cnt);
const u8 res = vtlb_ramRead<mem8_t>(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<u32>(buf, buf_cnt);
const u16 res = vtlb_ramRead<mem16_t>(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<u32>(buf, buf_cnt);
const u32 res = vtlb_ramRead<mem32_t>(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<u32>(buf, buf_cnt);
const u64 res = vtlb_ramRead<mem64_t>(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<u32>(buf, buf_cnt);
vtlb_ramWrite<mem8_t>(a, FromSpan<u8>(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<u32>(buf, buf_cnt);
vtlb_ramWrite<mem16_t>(a, FromSpan<u16>(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<u32>(buf, buf_cnt);
vtlb_ramWrite<mem32_t>(a, FromSpan<u32>(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<u32>(buf, buf_cnt);
vtlb_ramWrite<mem64_t>(a, FromSpan<u64>(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<char*>(&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<u8>(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<u8>(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, &section) ||
!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 MsgSetSetting:
{
std::string section, key, value;
if (!ReadLengthPrefixedString(buf, buf_cnt, buf_size, &section) ||
!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<u32>(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)};
}