45 Commits
Author SHA1 Message Date
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
Brian Degenhardt 912b1d8f95 PINE: listen on loopback TCP on Android
PINE has never worked on Android. Every non-Windows platform binds an AF_UNIX
socket under XDG_RUNTIME_DIR, falling back to /tmp; Android sets neither and has
no /tmp, so Initialize() failed at bind() and the server simply never started.

Nor is there anywhere better to put the socket. The writable directories on
Android are app-private, and every client that would want to connect -- adb, a
shell, another process -- runs under a different uid, so a socket placed there
binds successfully and then admits nobody.

Loopback TCP is the transport Android does support reaching into: adb forward
bridges a device port to a workstation. PINE already speaks TCP because Windows
has always needed it, and the wire format is identical, so this is a matter of
selecting the existing branch rather than writing a new one -- hence
PINE_TCP_TRANSPORT, which separates "which socket family" from the two Windows
API questions (the SOCKET handle type, winsock startup) that _WIN32 still owns.

SO_REUSEADDR comes along on the POSIX side: relaunching the app is the normal
debugging loop on a handheld, and without it a killed process leaves the port in
TIME_WAIT and PINE looks broken for a minute with nothing explaining why.
2026-08-09 13:02:40 -07:00
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
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
Brian Degenhardt 7f93a80dd7 PerformanceMetrics: count the GS back thread
Under GSBackThreadMode >= Lockstep roughly half the GS work moves to a second
thread, and every surface that reports GS cost -- OSD, PerfLog, the Qt status
bar, PINE stats, gsrunner's @HWSTAT@ block -- measured the MTGS thread alone.
So the split read as a large GS saving. It is not: on a Rogue Galaxy savestate
here, mode 0 costs 15.8% / 2.63 ms and mode 3 costs 17.0% / 2.84 ms plus
14.2% / 2.37 ms on the back thread -- about twice the total GS CPU time, bought
to halve the critical path. That is a real trade, but nobody could see it.

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

The figure is omitted, not reported as zero, wherever a back thread does not
exist -- otherwise a mode 0 vs mode 3 comparison reads a permanent 0% as
meaningful. gsrunner latches the presence flag during the run because DumpStats
executes after VMManager::Shutdown, by which point the thread has joined.
2026-07-30 21:55:58 -07:00
Brian Degenhardt 975e408ed5 PINE: add a GS-dump opcode so a script can capture without a hotkey
MsgGSDump (0x14, ARMSX2-local) queues a GS dump of the next N frames:
[u32 frames][u32 path_len][path bytes], where frames == 0 stops a recording
dump and UINT32_MAX records until stopped -- the same press/release pair the
GSDumpMultiFrame hotkey binds. The reply is JSON carrying the resolved dump
path, so a client knows the file to wait for instead of guessing at the
snapshots folder's auto-naming.

Three things the naive version of this gets wrong, all found by testing it
against a live Dragon Quest VIII:

QueueSnapshot honours a caller-supplied path only when it ends in .png, and
silently substitutes an auto-named file otherwise -- a scripted client would
write somewhere it never looks. Normalise the path up front instead, dropping
a .gs/.gs.xz/.gs.zst/.png suffix if the caller spelled one out so that naming
the file you want does not earn a doubled extension.

A request that arrives while a dump is already recording creates no second
dump: the VSync handler only opens one when none exists. It writes a stray
screenshot, and worse, overwrites the running dump's remaining frame count and
cuts it short. The first version of this replied with a path for a file that
was never created and truncated the recording that was. Refuse instead, with
reason "already recording"; the caller can stop the running dump first. The
same defect reachable via the Screenshot hotkey is left alone here -- it is a
renderer behaviour change and belongs in its own commit.

The PINE thread cannot push MTGS packets: the ring is single-producer and that
producer is the EE thread. Take the same two-hop route BuildStatsJson already
documents -- Host::RunOnCPUThread, then RunOnGSThread -- and read GSConfig's
compression method on the GS thread, since it decides the extension.

QueueSnapshot and GSQueueSnapshot now return whether they took the request;
existing callers ignore it. GSIsDumpRecording and GSHasFrontParser expose the
two pieces of GS-thread state the reply needs. pipelined_incomplete surfaces
the known GV7-2 gap rather than letting a script collect corrupt dumps.

Verified live: every promised path was written, refusals produced no files,
and all three dump shapes replay in gsrunner -- single-frame as 4 (2) frames,
a stopped multi-frame recording as 186 (91).
2026-07-30 21:55:58 -07:00
Brian Degenhardt 2e9762d1d6 Rebrand user-facing PCSX2 references to ARMSX2; fix Help menu
Help menu (Linux desktop and everywhere):
- GitHub Repository pointed at a dead branch (/tree/macOS); now the repo root.
- Removed the PCSX2 Wiki and Documentation items (they linked pcsx2.net /
  wiki.pcsx2.net, impersonating upstream) and replaced them with a single
  ARMSX2 Website item pointing at armsx2.net.
- Removed Check for Updates (the auto-updater is disabled, so it only ever
  errored) and About Qt.
- About dialog body reworded to describe ARMSX2 (crediting PCSX2 as upstream).

Wrong-destination / impersonation fixes:
- PINE MsgVersion reply now identifies as "ARMSX2" (buffer sized accordingly).
- Bug-report links (GS unknown-video-mode / invalid-lod, EE COP2 warnings) now
  point at github.com/ARMSX2/ARMSX2/issues.
- "download a fresh copy" recovery messages (VMManager, SaveState, Windows
  updater, Win32 update-not-supported dialog) now point at armsx2.net.
- Auto-updater release/compare endpoints and staging-dir name de-PCSX2'd.

UI strings: setup wizard, cover downloader, and the Settings widgets (Qt) plus
their Big-Picture/Fullscreen ImGui twins now say ARMSX2. The "PCSX2Blue" theme
settings key is left unchanged (persisted / matched in code); only its display
name changes.

Interop identifiers deliberately left as-is: the PINE socket name (pcsx2.sock,
for PINE-client compatibility) and the RetroAchievements client name.
2026-07-23 19:48:13 -07:00
Brian Degenhardt d2bce662d4 PINE: marshal GS-stats sampling onto the CPU thread to avoid MTGS deadlock
The PINE server thread called MTGS::RunOnGSThread + WaitGS directly to sample
GS memory stats and device/driver info. The MTGS ring is single-producer:
m_WritePos is owned by the EE/CPU thread, so pushing a packet from the PINE
thread creates 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) against the GS thread (asleep in WaitForWork).

Marshal the sample onto the CPU thread first -- the legitimate ring producer --
and block until the GS-owned data has been gathered.
2026-07-23 19:46:52 -07:00
Brian Degenhardt 9a4bd0f56f GS tooling: report GPU device and driver identity in stats output
Both `gsctl stats` and `gsrunner -stats-json` reported counters with no
indication of which GPU or driver produced them, so a stats blob from a
tester was not attributable — and driver identity is the axis nearly every
mobile GPU behaviour turns on.

This matters beyond provenance. Several GS features are force-overridden
per-driver (framebuffer fetch and texture barriers are forced on for Adreno
regardless of INI), so without knowing the driver you cannot tell whether a
settings A/B was applied at all or silently compared a config against itself.

gsrunner captures the strings on the GS thread at first present rather than
at shutdown, where the device may already be gone. Both emitters escape the
values, since GetDriverInfo() is multi-line on Vulkan.
2026-07-22 21:16:45 -07:00
Brian Degenhardt 4fc1fa7c5f PINE: add statistics and settings opcodes, plus a gsctl client
Debugging a GS performance problem meant restarting the emulator and reloading a
savestate for every "did you try setting X?", and the only way to read the
statistics that drive that decision was to look at the OSD. PINE already
provides a unix socket, a thread, framing, a config key and the RunOnCPUThread
marshalling pattern, but its opcodes stop at guest-RAM peek/poke plus savestates
and game identity -- no host statistics, no settings.

Adds four ARMSX2-local opcodes at 0x10+ (upstream PINE ends at 0xF, so a generic
client will never send them):

  MsgGetStats     PerformanceMetrics, all GSPerfMon counters and the texture
                  cache memory figures, as JSON.
  MsgGetSetting   read a setting by section/key.
  MsgSetSetting   write a setting, apply it, and report whether the key forces a
                  GS device reopen.
  MsgFrameAdvance step a paused VM.

MsgSetSetting writes the persisted key rather than poking EmuConfig directly,
because a direct poke is silently reverted by the next ApplySettings, which
re-derives EmuConfig from the INI layer stack. The restart_required answer comes
from a new GSOptions::IsRestartOption, sitting next to RestartOptionsAreEqual so
the two lists stay in sync.

Statistics are gathered on the PINE thread. PerformanceMetrics and g_perfmon are
benign scalar reads, but GSgetMemoryStats dereferences g_texture_cache and
g_gs_device, which are GS-thread owned, so that one is marshalled through
RunOnGSThread.

tools/gsctl.py is a stdlib-only client emitting JSON on stdout.

Verified against a headless gsrunner replay: toggling accurate_blending_unit
between 0 and 5 over the socket moves barriers 1.0 <-> 91.5 and draw calls
55.5 <-> 101.5, repeatably, with no restart.
2026-07-22 21:16:45 -07:00
chaoticgd b67e793283 PINE: Disallow access to pages with custom VTLB handlers 2026-06-28 16:46:13 -04:00
chaoticgd 3df128d9f9 PINE: Ignore SIGPIPE properly on macOS 2026-06-06 09:59:21 -04:00
chaoticgd b3834427bc PINE: Don't test for a valid VM when checking emulator version 2026-06-01 23:55:19 +02:00
SternXD d983b2b066 Copyright: Change year from 2002-2025 to 2002-2026 2026-01-15 00:22:32 +01:00
chaoticgd e8c2cfa843 SaveState: Rework error handling when saving states 2025-12-13 21:42:52 -05:00
chaoticgd 764875ddbf Qt: Add setting to show state load errors using a dialog or OSD message 2025-12-13 21:42:52 -05:00
chaoticgd 8d30e8cee8 FullscreenUI: Fix save state loading 2025-11-28 10:50:41 -05:00
chaoticgd bf10b55aa1 PINE: Use the correct naming convention for static globals 2025-09-13 14:45:10 -04:00
chaoticgd 43d9ea99b0 PINE: Don't crash with SIGPIPE if the client dies while writing 2025-09-13 14:45:10 -04:00
chaoticgd 62fa768e60 PINE: Give the server thread a name 2025-07-09 04:19:09 +02:00
TheTechnician27 23fd57f641 Copyright: Change year from 2002-2024 to 2002-2025 2025-01-20 05:07:26 +01:00
chaoticgd f69d5835b8 PINE: Fix stack buffer overflow for long XDG_RUNTIME_DIR paths 2024-11-22 14:10:32 -05:00
chaoticgd eeb919325e Misc: Minimise the amount of work done when svnrev.h is updated 2024-11-12 09:14:39 -05:00
GovanifY 132431b7c8 headers: relicense to GPL-3.0+
also update to 2024 while i'm at it
2024-07-30 17:17:13 -04:00
GovanifY 325e219bb1 PINE: fix save state regression introduced during the switch to Qt
Ensure thread safety when loading/saving state
2024-01-25 20:05:44 +10:00