Files
ARMSX2/pcsx2/PINE.cpp
T
Brian Degenhardt 9e34dc20a3 PINE: report what a setting is actually running as, not what the INI says
A settings query answered the wrong question. GameDB hardware fixes are applied
to the live config after the settings load and are never written back to the
file, so on any game carrying them the persisted value and the running value
disagree — and the query only ever knew about the first. On Rogue Galaxy it
reported autoflush off and preload off while the renderer was running autoflush
at 2 and preload on. That cost real time during the Rogue Galaxy work.

The confusion is the smaller half. The real damage is to measurement: a settings
A/B that writes a key to some value measures the GameDB value in BOTH arms,
because GameDB re-applies it after every settings load, while the two arms
report two different settings. That is a wrong answer with no symptom, on
exactly the titles worth investigating.

So add an opcode that reports both values side by side. Effective values come
from serialising the live config back out through the same wrapper that writes
the INI, which means they land under the identical section/key names a caller
already uses and every setting is covered without a key map — a hand-written map
would need extending by every future setting, and the one that got missed would
be the one somebody trusted. It also fixes a smaller lie: keys absent from the
INI came back as empty strings, reading as "unset" rather than as their default.

The reply says the two strings differ; it does not say why, because from here
that is not knowable. A GameDB fix, safe-mode masking and a settings layer this
query does not read are indistinguishable at the point of comparison, and naming
one of them would be inventing the reason.

The existing read is left alone, so anything speaking the old opcode keeps
working. gsctl's `get` now reports the running value, prints the discrepancy to
stderr where a human cannot miss it and a pipeline does not have to care, and
keeps the on-disk value available behind --persisted.
2026-08-02 19:29:36 -07:00

1205 lines
39 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/FPControl.h"
#include "common/MemorySettingsInterface.h"
#include "common/SettingsInterface.h"
#include "common/SettingsWrapper.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. */
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<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;
}
/**
* 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, 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 MsgGetEffectiveSetting:
{
std::string section, key;
if (!ReadLengthPrefixedString(buf, buf_cnt, buf_size, &section) ||
!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, &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)};
}