Files
Brian Degenhardt b5415c8105 GS: stop a screenshot ending a GS dump that is already recording
A snapshot request and a running recording shared one frame counter. The
screenshot hotkey asks for zero dump frames, so pressing it mid-recording
zeroed the budget of the dump in progress and the next VSync closed it as
though the user had asked it to stop. A single-frame dump request did the
same thing one frame later. Both were silent; the file simply ended early.

Two fields now, so a request cannot reach into a recording at all: one for
what the queued request asked for, one for what the open dump still owes,
written only when that dump is created.

The two branches were also alternatives rather than independent, so the
frame a screenshot landed on never reached the dump and two guest frames
merged into one on replay. A recording now takes every frame it is open
for -- except the one it was opened on, whose state went into the dump's
header and whose replay therefore starts from the frame after.

A dump request arriving while one records still cannot open a second dump,
but it says so on the OSD instead of quietly writing only the screenshot.

The decision is extracted to a header-only policy with the usual
static_asserts, pinned by eight cases riding the GS test target. The
truncation is reachable only from the hotkeys and the Big Picture button --
PINE's dump opcode was written to refuse rather than trip over it -- so the
policy suite is the regression gate. Its refusal comment is updated: it now
rests on not handing back a path for a file that will never appear, which
was always the better half of the argument.
2026-08-09 14:39:29 -07:00

1274 lines
43 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"
// Which socket family PINE listens on is a property of the host, not of the protocol: the wire
// format below is identical either way, and reference clients already speak both because Windows
// has always needed TCP.
//
// Android joins Windows on the TCP side for a different reason. A Unix socket needs a path both
// sides can name, and Android gives us nowhere to put one: there is no /tmp, XDG_RUNTIME_DIR is
// unset, so the existing code would fall back to binding "/tmp/pcsx2.sock" and simply fail. The
// writable directories that do exist are all app-private, and every plausible client -- adb, a
// shell, another app -- runs under a different uid, so a socket placed there binds successfully
// and then admits nobody. Loopback TCP is the transport Android actually supports reaching into:
// "adb forward" bridges a device port to the workstation, which is what makes a handheld
// debuggable from a desk at all.
#if defined(_WIN32) || defined(__ANDROID__)
#define PINE_TCP_TRANSPORT 1
#endif
// sockaddr_in and htons()/htonl(). Windows already has them via WinSock2.h above; the POSIX
// spelling lives in headers the Unix-socket path never needed, so it is pulled in only here.
#if defined(PINE_TCP_TRANSPORT) && !defined(_WIN32)
#include <arpa/inet.h>
#include <netinet/in.h>
#endif
// The TCP setup below is now shared, so its failure comparisons have to be too. Winsock spells
// these as named constants and POSIX returns -1 from both socket() and bind(); neither name
// exists on the other platform.
#ifdef _WIN32
#define PINE_INVALID_SOCKET INVALID_SOCKET
#define PINE_SOCKET_ERROR SOCKET_ERROR
#else
#define PINE_INVALID_SOCKET (-1)
#define PINE_SOCKET_ERROR (-1)
#endif
#ifdef _WIN32
static bool InitializeWinsock()
{
static bool initialized = false;
if (initialized)
return true;
WSADATA wsa = {};
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
return false;
initialized = true;
std::atexit([]() { WSACleanup(); });
return true;
}
#endif
namespace PINEServer
{
static std::thread s_thread;
static int s_slot;
// Two independent splits meet here, which is why the guards differ. The socket HANDLE type is
// a Windows API question -- SOCKET is an opaque handle there, a file descriptor everywhere
// else -- while the socket NAME only exists when the transport is a Unix socket, and Android
// is POSIX with no socket name at all.
#ifdef _WIN32
// windows claim to have support for AF_UNIX sockets but that is a blatant lie,
// their SDK won't even run their own examples, so we go on TCP sockets.
static SOCKET s_sock = INVALID_SOCKET;
// the message socket used in thread's accept().
static SOCKET s_msgsock = INVALID_SOCKET;
#else
static int s_sock = -1;
// the message socket used in thread's accept().
static int s_msgsock = -1;
#endif
#ifndef PINE_TCP_TRANSPORT
// absolute path of the socket. Stored in XDG_RUNTIME_DIR, if unset /tmp
static std::string s_socket_name;
#endif
// Whether the socket processing thread should stop executing/is stopped.
static std::atomic_bool s_end{true};
/**
* Maximum memory used by an IPC message request.
* Equivalent to 50,000 Write64 requests.
*/
#define MAX_IPC_SIZE 650000
/**
* Maximum memory used by an IPC message reply.
* Equivalent to 50,000 Read64 replies.
*/
#define MAX_IPC_RETURN_SIZE 450000
/**
* IPC return buffer.
* A preallocated buffer used to store all IPC replies.
* to the size of 50.000 MsgWrite64 IPC calls.
*/
static std::vector<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 and write no dump. Handing back a path for a file that will never
* appear is the worst of the available answers, so stop the running dump first if you mean
* to. (Refusing here also predates the fix for the truncation that used to follow: the
* request and the recording shared a frame counter, so a second one cut the first short.
* They are separate fields now -- see GSSnapshotPolicy.h -- and the refusal stands on the
* unanswerable-path argument alone.)
*/
static bool QueueGSDump(u32 frames, std::string path, std::string* reply)
{
if (!MTGS::IsOpen())
return false;
// QueueSnapshot honours a caller-supplied path only when it ends in .png, which it
// strips to get the base name shared by the screenshot and the dump; anything else is
// silently discarded in favour of an auto-named file in the snapshots folder. Silence
// is the wrong failure for a scripted client -- it writes somewhere the script never
// looks -- so meet that contract here instead. Drop a dump or image suffix if the
// caller spelled one out, so naming the file you want does not earn you a doubled
// extension, and let what is left be the base name.
if (!path.empty())
{
for (const std::string_view suffix : {".gs.zst", ".gs.xz", ".gs", ".png"})
{
if (StringUtil::EndsWithNoCase(path, suffix))
{
path.erase(path.size() - suffix.size());
break;
}
}
}
bool queued = false, stopped = false;
std::string base;
const char* dump_ext = "";
const char* reason = "";
Host::RunOnCPUThread(
[&]() {
MTGS::RunOnGSThread([&]() {
if (frames == 0)
{
GSStopGSDump();
stopped = true;
return;
}
if (GSIsDumpRecording())
{
reason = "already recording";
return;
}
// Resolve the auto-name here rather than leaving it to QueueSnapshot, so the
// reply can name the exact file the client is about to wait for.
base = path.empty() ? GSGetBaseSnapshotFilename() : path;
queued = GSQueueSnapshot(base + ".png", frames);
if (!queued)
{
reason = "snapshot pending";
return;
}
// GSConfig is the GS thread's copy of the config, so read the compression
// method here -- it decides the extension the dump writer will append.
switch (GSConfig.GSDumpCompression)
{
case GSDumpCompressionMethod::Uncompressed:
dump_ext = ".gs";
break;
case GSDumpCompressionMethod::LZMA:
dump_ext = ".gs.xz";
break;
default:
dump_ext = ".gs.zst";
break;
}
});
MTGS::WaitGS(false);
},
true);
*reply = fmt::format(
"{{\"queued\":{},\"stopped\":{},\"frames\":{},\"path\":\"{}\",\"reason\":\"{}\"}}",
queued ? "true" : "false", stopped ? "true" : "false", frames,
queued ? JsonEscape(base + dump_ext) : std::string(), reason);
return true;
}
} // namespace PINEServer
bool PINEServer::Initialize(int slot)
{
s_end.store(false, std::memory_order_release);
s_slot = slot;
#ifdef _WIN32
if (!InitializeWinsock())
{
Console.WriteLn(Color_Red, "PINE: Cannot initialize winsock! Shutting down...");
Deinitialize();
return false;
}
#endif
#ifdef PINE_TCP_TRANSPORT
s_sock = socket(AF_INET, SOCK_STREAM, 0);
if ((s_sock == PINE_INVALID_SOCKET) || slot > 65536)
{
Console.WriteLn(Color_Red, "PINE: Cannot open socket! Shutting down...");
Deinitialize();
return false;
}
// A listener that survives its own process is worse than one that fails to start: the port
// sits in TIME_WAIT after a crash or a fast restart, bind() refuses, and PINE looks broken
// for a minute or so for reasons nothing reports. This matters far more on a handheld than
// on a desktop, because killing and relaunching the app is the normal debugging loop there.
#ifndef _WIN32
const int reuse = 1;
setsockopt(s_sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
#endif
sockaddr_in server = {};
server.sin_family = AF_INET;
server.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // localhost only
server.sin_port = htons(slot);
if (bind(s_sock, (struct sockaddr*)&server, sizeof(server)) == PINE_SOCKET_ERROR)
{
Console.WriteLn(Color_Red, "PINE: Error while binding to socket! Shutting down...");
Deinitialize();
return false;
}
// Loopback is not reachable from another machine, so on a handheld the port is useless until
// something bridges it. Name the bridge in the log rather than leaving the reader to work it
// out: on Android this line is the entire setup instruction for driving the device from a
// workstation. Desktop hosts get the address only -- there the client is already local, and
// telling a Windows user to run adb would just be wrong.
#ifdef __ANDROID__
Console.WriteLnFmt("PINE: Listening on 127.0.0.1:{} -- bridge it with: adb forward tcp:{} tcp:{}",
slot, slot, slot);
#else
Console.WriteLnFmt("PINE: Listening on 127.0.0.1:{}.", slot);
#endif
#else
char* runtime_dir = nullptr;
#ifdef __APPLE__
runtime_dir = std::getenv("TMPDIR");
#else
runtime_dir = std::getenv("XDG_RUNTIME_DIR");
#endif
// fallback in case macOS or other OSes don't implement the XDG base
// spec
if (runtime_dir == nullptr)
s_socket_name = "/tmp/" PINE_EMULATOR_NAME ".sock";
else
{
s_socket_name = runtime_dir;
s_socket_name += "/" PINE_EMULATOR_NAME ".sock";
}
if (slot != PINE_DEFAULT_SLOT)
s_socket_name += "." + std::to_string(slot);
struct sockaddr_un server;
s_sock = socket(AF_UNIX, SOCK_STREAM, 0);
if (s_sock < 0)
{
Console.WriteLn(Color_Red, "PINE: Cannot open socket! Shutting down...");
Deinitialize();
return false;
}
server.sun_family = AF_UNIX;
StringUtil::Strlcpy(server.sun_path, s_socket_name, sizeof(server.sun_path));
// we unlink the socket so that when releasing this thread the socket gets
// freed even if we didn't close correctly the loop
unlink(s_socket_name.c_str());
if (bind(s_sock, (struct sockaddr*)&server, sizeof(struct sockaddr_un)))
{
Console.WriteLn(Color_Red, "PINE: Error while binding to socket! Shutting down...");
Deinitialize();
return false;
}
#endif
// maximum queue of 4096 commands before refusing, approximated to the
// nearest legal value. We do not use SOMAXCONN as windows have this idea
// that a "reasonable" value is 5, which is not.
if (listen(s_sock, 4096))
{
Console.WriteLn(Color_Red, "PINE: Cannot listen for connections! Shutting down...");
Deinitialize();
return false;
}
// we allocate once buffers to not have to do mallocs for each IPC
// request, as malloc is expansive when we optimize for µs.
s_ret_buffer.resize(MAX_IPC_RETURN_SIZE);
s_ipc_buffer.resize(MAX_IPC_SIZE);
// we start the thread
s_thread = std::thread(&PINEServer::MainLoop);
return true;
}
bool PINEServer::IsInitialized()
{
return !s_end.load(std::memory_order_acquire);
}
int PINEServer::GetSlot()
{
return s_slot;
}
std::vector<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);
// Removing the socket file is about the Unix-socket transport, not about being POSIX: a TCP
// listener has no filesystem entry to clean up, and Android has no s_socket_name to read.
#ifndef PINE_TCP_TRANSPORT
if (!s_socket_name.empty())
{
unlink(s_socket_name.c_str());
s_socket_name = {};
}
#endif
// shutdown() is needed, otherwise accept() will still block.
#ifdef _WIN32
if (s_sock != INVALID_SOCKET)
shutdown(s_sock, SD_BOTH);
#else
if (s_sock >= 0)
shutdown(s_sock, SHUT_RDWR);
#endif
safe_close_portable(s_sock);
safe_close_portable(s_msgsock);
if (s_thread.joinable())
s_thread.join();
}
PINEServer::IPCBuffer PINEServer::ParseCommand(std::span<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)};
}