Files
ARMSX2/pcsx2/PINE.cpp
T
Brian Degenhardt 17b2be058a GS: record a complete dump under the pipelined back-thread split
The dump's transfer and ReadFIFO hooks sit on the parse path, and its initial
state came from Freeze() on the renderer. Under GSBackThreadMode=Pipelined the
parse path belongs to the front object, so both were reading the wrong object:
the front's transfers never reached the dump at all. A Rogue Galaxy capture that
should be 39.4 MB of packets came out with 90 KB -- 0.2% of the stream, the
ReadFIFO and VSync packets alone -- and replayed as nothing. GSQueueSnapshot
warned about it rather than fixing it (GV7-2).

The dump stays owned by the renderer, which opens and closes it on the present
path; the parse side reaches it through GetDumpSink(), which routes via
m_mem_target, and the initial freeze goes through a new m_parse_target, the
inverse pointer. Both paths run on the MTGS thread -- the front's runahead is
over the back thread, not over the thread handling vsync -- so the front writes
straight into the back's dump with no synchronisation. m_parse_target->Freeze()
is the same call GSfreeze makes for a savestate, which already drains and
already takes registers from the front and local memory from the back.

Verified on a Rogue Galaxy savestate, frame-stepped over PINE so both arms start
from the identical state: the mode 3 dump is byte-identical to the mode 0 dump,
4.2 MB of initial state and 39.4 MB of packets, and it replays in gsrunner to
ten frames identical under both modes. Two mode 0 runs are likewise identical,
so the harness has no slack. Reverting just the transfer sink reproduces the
90 KB dump, so the comparison has teeth.

Two bytes of bookkeeping ride along: GSQueueSnapshot loses the warning, and the
MsgGSDump reply loses pipelined_incomplete, which now has nothing to report.
Whether the split engaged is a genuine question, so it moves to the stats reply
as gs_front_parser, next to gs_back_thread_pct where it belongs.
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\":{},\"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;
}
/**
* 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;
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;
}
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)};
}