mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
7f93a80dd7f07d6f0e2e52edf430630c81455822
100
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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). |
||
|
|
bf65e8604b |
GameDB: drop autoFlush on Rogue Galaxy — a deliberate speed/accuracy trade
Rogue Galaxy is the slowest title we track on handhelds and users report it as such. Turning autoFlush off is the largest lever we have found for it: render passes -38%, texture copies -74%. On the Adreno 610, which has no headroom, that is -1.82 ms/frame and +2.6 fps. On the Adreno 650 it is -1.67 ms banked as headroom, both arms already at 100% speed. This is not free and should not be recorded as if it were. The software renderer, an exact per-pixel GS model and an independent oracle here because AutoFlushSW is a separate setting, scores level 0 3.4x further from truth than level 2 on the contested pixels (mean error 9.436 vs 2.808; level 2 is closer on 17312 of 22424). What degrades is the light a lamp contributes to nearby lit surfaces, so chests, blades and floors read slightly bright and warm. The glow cones themselves are pixel-identical. It is taken because the error is imperceptible in practice: bounded at 21-23/255 in all three captured scenes, diffuse rather than a missing object, and four independent side-by-side looks at 1:1 failed to distinguish the two. Revert to level 1 -- not 2 -- if anyone reports a regression: level 1 is pixel- and cost-identical to 2 on Rogue Galaxy at 1x, 3x and 6x, and since 381bc41ded it is also worth -5.8% of GS-thread cycles because it moves the game's non-sprite prims onto the direct vertex kick. Level 2 buys nothing measurable over level 1 here. Seven serials, which is every Rogue Galaxy entry the overlay carries. The Korean release is SCKA-30005 and upstream gives it no gsHWFixes at all, so it is absent here too rather than newly missed. ⚠ SLKA-25372 is Black, not Rogue Galaxy -- it is Criterion's Burnout engine, which is why it carries OI_BurnoutGames. An earlier working copy had it in the Rogue Galaxy set and flipped it to 0; it stays at 2. |
||
|
|
f0aa0f1949 |
GS: take the direct vertex kick for non-sprite prims at autoflush SpritesOnly
The auto_flush instantiations of the vertex handlers exist to feed HandleAutoFlush, which reads the incoming vertex out of m_v. To do that they stage every vertex through m_v instead of keeping it in registers, which is why SetPrimHandlers hands the same auto_flush argument to every primitive type. At SpritesOnly that is wasted on everything that is not a sprite. IsAutoFlushDraw early-outs on the prim before it looks at anything else, so those prims write a staged vertex, read it once, and discard it. Narrow the template argument per prim so they take the fused direct kick instead, mirroring IsAutoFlushDraw's early-out exactly -- it keys on the level alone and not on the renderer, so the software path narrows in step. Dragon Quest VIII renders identically at levels 1 and 2 (same draws, passes and copies), so level 2 is an exact staged control for level 1's direct path with no rendering difference to confound it. GS-thread cycles over 3 runs each, 240 frames: 2043.2M staged against 1947.4M direct, ranges disjoint, -4.7%. The parse handler itself goes 272.5M -> 156.2M, so it accounts for essentially the whole delta. Rebuilding the old handler table and diffing against it agrees: -5.1%. Output is unchanged, as it must be: prims, draws, render passes and copies are identical on Dragon Quest VIII and Rogue Galaxy, and all four dumped frames are pixel-identical under both the hardware and the software renderer. 581 GameDB entries ship autoFlush: 1. |
||
|
|
80e4d09b99 |
GS/VK: arm the mid-frame submit kick in frames, not render passes
The kick's arming window exists to answer one question -- has this game read
back recently enough to be worth kicking for -- so that titles which never read
back see zero change. It counted render passes, and 128 passes means completely
different things in different titles: about three frames of OutRun 2006, but
only about three quarters of a Rogue Galaxy frame. So RG armed the window at its
one readback per frame, spent it partway through, and then ran the rest of every
frame with the kick silently switched off. Nothing asked for that; it fell out of
the unit.
Count the window in frames since the last readback instead, which is the unit the
comment already claimed ("~a few frames' worth of render passes") and the unit the
decision is actually about. The cadence stays in render passes, where a uniform
interval is what you want. The never-read-back guarantee is unchanged and still
carried by the ~0u sentinel.
Measured on M2/Honeykrisp, 60-90 frames per dump, gsrunner without -perf: total
GPU stall (readback wait plus command-buffer activate stall) is unmoved --
Rogue Galaxy 554ms before and 558ms after, OutRun 2006 320ms and 319ms, both
inside run-to-run spread. Shadow of the Colossus and Black, which never read
back, take zero kicks before and after. So this is not a speed change here; it
removes a scene-dependent cliff that a device where the kick matters more could
land on.
While measuring, the threshold's cost model turned out to be badly wrong, so
correct the comment. "RPs-per-frame / threshold extra submits" predicts ~14
kicks/frame for Rogue Galaxy; the real figure is 2, because the fence gate -- not
the threshold -- is what binds. With three command buffers only two submissions
can be in flight, and ~3300 of ~3400 offers to kick find the next command buffer
still executing. Sweeping the threshold 8->16 measured -2% stall on Rogue Galaxy
and +12% on OutRun 2006, so it is left alone.
|
||
|
|
639b317dbc |
gsrunner: stop the Wayland message pump blocking past the shutdown flag
The pump polls the display fd with a 16 ms cap so it can re-test the shutdown flag between polls, but on POLLIN it called wl_display_dispatch(), which reads the queued events and then waits for more. A window nobody is drawing to gets no further events, so the flag was never re-tested and the process never exited -- gsrunner would print its whole stats block and then hang forever, leaving every automated run to be killed by a timeout. Switch to the non-blocking read sequence: prepare_read, flush, poll, then read_events or cancel_read, then dispatch_pending. Nothing in the loop can block now, so the cap does what its comment claims. |
||
|
|
b2d7a8d0a6 |
GS: serve 1:1 same-format StretchRects as image copies
A StretchRect is a draw, so it needs a render pass of its own and the pass it interrupted has to be restarted afterwards -- two pass boundaries. When the stretch is really a plain 1:1 copy between identically-formatted textures, the backend's image-copy path does the same work for one. The texture cache hits this constantly. A target-backed source is destroyed outright whenever anything writes its target, so every autoFlush split re-copies the sampled region of the render target it has just written. The gate is narrow enough that the two paths cannot disagree on any pixel: plain COPY/DEPTH_COPY with a full write mask, identical formats, depth-vs-colour aspect agreeing on both sides, a source that actually holds contents rather than a pending clear, rects that land on the texel grid at 1:1, and both rects in bounds -- the draw path scissors an out-of-range destination and edge-clamps out-of-range source coordinates, and a copy can do neither. Render passes over a 5-loop gsrunner replay, Vulkan / OpenGL: Rogue Galaxy 2411 -> 1741 / 2283 -> 1373 OutRun 2006 1240 -> 1118 / 811 -> 657 Black 1080 -> 1010 / 282 -> 202 God of War II 1278 -> 1238 Draw counts are unchanged everywhere. Colour output is bit-identical on Vulkan across all six staged dumps at 1x and 3x, and on OpenGL for the dumps that render deterministically there. |
||
|
|
365c0e2eaf |
Merge pull request #402 from ARMSX2/help-menu-and-branding-fixes
Fix Help menu and rebrand user-facing PCSX2 references to ARMSX2 |
||
|
|
a8c5521c66 |
GameDB: Rogue Galaxy no longer preloads frame data
preloadFrameData primes every newly created render target from the game's
GS local memory. Where the frame's geometry doesn't cover the target, those
preloaded pixels stay visible: in the church interior the outdoor town shows
through the rear wall, washed out and semi-transparent. Bisected on a
Snapdragon 865 with the other six fixes held either way — the artifact
tracks preloadFrameData alone, and the three alignment fixes are innocent.
Upstream added it in
|
||
|
|
355a1a8739 |
tests: reset GIF PATH1 per replay — escaped wrap-head bytes filled the ring
The vurunner/VuReplay PATH1 sink covers Gif_Unit::TransferGSPacketData, but microVU's XGKICK wrap path sends the pre-wrap head through Gif_Path::CopyGSPacketData directly (the same harness blind spot pstef found landing the console XGKICK cases). Those bytes land in the REAL gifPath[1] ring, which nothing drains in a runner with no GS thread: across a few hundred wrapped-kick captures in one process the ring fills, CopyGSPacketData calls mtgsReadWait, and MTGS::WaitGS trips its devel closed-thread assert — aborting corpus sweeps mid-batch (release would early-return instead and lose the wait). Backtrace: mVU_XGKICK_ → CopyGSPacketData → mtgsReadWait → WaitGS, cap ~360 of a 400-cap batch. Reset gifPath[1] at each replay entry so escaped bytes can never accumulate across captures. Still correct once the sink covers both entry points — then it's just belt-and-suspenders. The 400-cap batch that aborted now completes; suite stays green. |
||
|
|
efeab3d35b |
tests: seed the E-bit delay slot in SeedVu0Microprogram
Architectural E-bit cleanup executes one more pair after the E-bit pair. VuTestHarness::LoadProgram has always appended a NOP pair for that delay slot, but EeRecTestHarness::SeedVu0Microprogram — the path the EeVu0Vcallms tests seed through — did not. VU0 micro mem is shared, never-reset global state, so the unseeded delay slot executed whatever pair a previous test left there: at one --gtest_shuffle ordering the Vu0SpecialBits T-bit branch programs leave 'vi3 = 0x333' at pair 2, and both engines faithfully ran it right after the victim's own program wrote vi3 — agreeing with each other, so only the expected-value assertions caught it (seed-2 EeVu0Vcallms pair). Mirror LoadProgram: when the caller's final pair carries the E bit, write a NOP pair into the delay slot too. Verified 60/60 shuffle seeds green. |
||
|
|
9b1f9992b8 |
IOP: collapse RAM mirrors in the HWADDR domain — cross-alias SMC ran stale code
The recLUT shares BASEBLOCK slots across the four RAM mirrors (guest page i maps physical page i & 0x1f in the 2MB config), so a block compiled at one alias stays dispatchable through every other — but psxhwLUT only stripped the segment base, leaving block registration, coverage, recBlocks and the clear-path range guard keyed by the un-collapsed address. A store through a different alias of a compiled page then missed every invalidation structure while the shared slot kept executing the stale block. Found by --gtest_shuffle: IopIrxHle leaves a block at canonical 0x14000, and the RAM-mirror SMC test then loads its victim program through 0x214000 — the C-path clear missed the stale block and the JIT ran the IRX test's code. Collapse the whole domain instead: the psxhwLUT entries for the RAM window fold the mirror bits (identity in the 8MB config), recClearIOP canonicalizes caller addresses up front so the g_psxMaxRecMem guard and everything HWADDR-keyed below agree, g_psxMaxRecMem itself tracks HWADDR(psxpc), and the store stub probes the collapsed offset it already computed for the store. Cross-alias SMC is pinned in iop_smc_tests.cpp in both orientations plus the JIT store-stub path. |
||
|
|
5d4184c2c1 |
tests: pin the VU0 run-ahead floor divergence, reset inherited VU0 control state
With VU0 left running and fewer than 16 cycles from its E-bit, a following non-interlocked COP2 transfer legitimately diverges JIT-vs-interp: interp transfer ops sync exactly (vu0Sync, no floor) while both recompilers floor the grant at 16 cycles (vu0SyncRunAheadThin / x86 CalculateMinRunCycles), so the JIT drains the leftover program where interp leaves it in flight. Pin that window per-engine in ee_vu0_runahead_floor_tests.cpp with deliberately constructed running state, alongside the two convergent cases (interlocked access, delta >= remaining). EnableVu0Capture now resets the VU0 control state that used to inherit from the previous test (VI[24..31], flags, cycle, interp resume sentinels) — the source of the order-dependent EeVu0* shuffle failures recorded 2026-07-25. Verified across 40 shuffle seeds: the inheritance class is gone. The remaining IopSmc and seed-2 EeVu0Vcallms shuffle failures reproduce without this change and are tracked separately. |
||
|
|
9fbb2d8cf2 |
GS: size the draw-staging arrays independently of the vertex buffers
GrowVertexBuffer listed m_draw_vertex/m_draw_index alongside the real vertex and index buffers and preserved their contents across the reallocation, copying sizeof(GSVertex) * m_vertex->tail bytes out of them. That length has no relationship to their allocation: the staging arrays are single per-object buffers sized by whichever growth happened to run last, while m_vertex and m_index point at a rotating set of independently sized draw slots and pooled draw-node arrays whose capacities are exchanged thousands of times a second. Two numbers maintained by unrelated mechanisms, assumed to track each other. God of War II crashed on Android 2.6.6 with SIGSEGV inside memcpy on the MTGS thread, in the GIF parse path, on exactly that copy: the buffer whose tail was read had grown to ~50k vertices while the staging array was still the 10k one from init, so the copy ran ~1.1MB past the end. Instrumenting the same scene from a savestate reproduces the mismatch locally at 49108 live vertices against a 10000-vertex staging array (1.19MB), plus 85398 indices against 60000. The over-read only faults where the heap layout puts an unmapped page in range, which is why it hit a tester and not the dev box. The staging arrays are write-then-consume: SetupIA overwrites the full range it stages before anything reads it back, so their contents are dead at growth time and never needed preserving. Drop them from GrowVertexBuffer and give them their own grow-only capacity, established at the point of use from what is actually being staged. That also closes the matching out-of-bounds write on channel-shuffle draws, and removes two dead allocations plus two large dead memcpys from every buffer growth. Rendering is unchanged: per-draw ledgers over two God of War II dumps are byte-identical before and after. gs_draw_staging_tests pins both properties -- growth must not touch the staging arrays, and staging capacity covers the request and never shrinks. Re-listing the arrays in GrowVertexBuffer turns the first test red, and under -DUSE_ASAN=ON it reports the original fault outright: heap-buffer-overflow, READ of size 319904, 0 bytes after a 128000-byte region, in GSState::GrowVertexBuffer. |
||
|
|
7abd063dc7 |
COP2: pre-clamp Fs on the MADDA broadcast row
NASCAR Thunder 2002 drew every car as a shredded wireframe under the EE recompiler; the EE interpreter drew them correctly, and VU0, VU1 and IOP were all identical to the JIT, so the fault was EE-side COP2 macro code. COP2_MADDA_BC multiplied Fs straight from its register. x86 specifies cFs for mVU_MADDAx/y/z/w, and the reason is that the PS2 VU has no infinities: an exponent-FF word is an ordinary large number, so against a zero broadcast lane the architectural answer is clamped(Fs) * 0 = 0. Taken unclamped it is the host's Inf * 0 = NaN, which the post-op result clamp then folds to +/-FLT_MAX. A transform accumulating that into ACC scatters the geometry it was positioning. The pre-clamp mirrors COP2_MADD_BC's existing clampFs. MSUBAx/y/z/w pass false: x86 gives them clampType 0, so their unclamped Fs is a shared, by-design divergence, not an arm64 defect. The game symptom needed the whole (lane x dest mask) grid to be right as well, so the test sweeps that for both halves of the family before pinning the clamp corner. recompiler_tests 1706/1706. |
||
|
|
06445dd641 |
eerunner: narrow --rec-fallback to a single VU macro op
Once a hunt reaches `cop2vu` it stops: that group is one dispatch bit covering the whole VU macro-mode instruction set, and there was no next axis. Finding which of them miscompiles meant hand-editing the classifier and rebuilding per hypothesis. Two additions close that gap. `--rec-fallback cop2vu:<mnemonic>` selects individual macro ops by name, over a flat 256-entry id space covering all three dispatch tables (BC2 by rt, SPECIAL1 by funct, SPECIAL2 by its packed index). And a compile-time census, printed after --mkstate, lists the macro ops the run actually emitted — an op that never compiles cannot be the bug, so it turns a 100-way search into a bisect over the handful a given game really uses. On NASCAR Thunder 2002 the census reported 58 distinct ops and the bisect reached one of them in eleven runs, no rebuilds. |
||
|
|
a88b4aa89c |
tests: cover every write-lane subset of the masked VIF unpack store
A masked unpack does not store its quadword with one instruction. doMaskWrite picks, from a sixteen-way switch, a hand-written sequence touching only the lanes that cycle actually writes, and those sequences differ in kind rather than just in offset: a 64-bit store for X+Y, a 64-bit lane store for Z+W, per-lane stores at hand-computed byte offsets for the scattered subsets, and a post-indexed pair for Y+Z. Each is its own chance to name the wrong lane. Only the three-lane subsets were reached. Measured, not assumed: of the sixteen cases, 7/11/13/14 executed and the other twelve had zero counts, because the existing mixed-mask cases happen to protect exactly one lane apiece. The subset is selected by which lanes carry the write-protect code, so ten new cases -- one per unreached subset -- name three protected lanes to reach a single-lane store and two to reach a pair. Protected lanes must come back holding the fill pattern while written lanes hold unpacked data, so a sequence that stores to a neighbouring lane fails on both halves at once. Two more cross the selector with a mode, where the mode merge runs on a partial lane set rather than the whole register. Validated by mutation, each bounded to exactly the predicted set: swapping the Z lane for W in the Y+Z sequence fails write_yz and write_yz_mode1 and nothing else; moving the single-lane Z store from offset 8 to 4 fails write_z alone. The remaining two switch arms stay unreached and are unreachable, which the new absolute test pins from the other side. A fully write-protected cycle is dropped by ProcessMasks before any store is emitted, so the "no lanes" arm is guarded, not exercised; the differential case for it would pass whatever the generator did, since it only has to agree with an oracle that also writes nothing. FullyProtectedBlockWritesNothing asserts the fact itself -- VU memory byte-identical to the fill pattern. The all-lanes arm is likewise dead: the caller emits a plain full-width store when no lane is protected. 1705 tests, 1703 pass, 2 pre-existing skips. |
||
|
|
7a5ed084c4 |
tests: cover the T-bit end-program Q/P commit on VU1
A T-bit stop on a branch does not go through the normal end-of-program routine; it has its own variant carrying a second copy of the Q/P commit. That copy matters because committing a double-buffered scalar out of a host vector means rotating lanes, and the rotate is not an involution — undoing a 4-byte rotate takes a 12-byte one. A duplicated rotate that no test ever runs is where that slip survives. Reaching it needs both scalars still in flight at the branch, so the end-of-program cycle advance is what retires them and flips the instance, and VU1, since P exists nowhere else. Mutation-checked: pinning either instance index to zero fails this case and nothing else. |
||
|
|
770e73ec28 |
tests: cover the XGKICK wrap seam and the XgKickHack drain
VU1 memory is circular and the kick address is a rolling double-buffer pointer, so a GIF packet straddling the top of memory is ordinary traffic. The transfer has to split at that seam and resume at offset 0; split it at the wrong offset and the GS receives the right byte count from the wrong place. Neither the non-hack split nor the hack path's two-pass equivalent had any coverage. With the XgKickHack gamefix on (the GameDB forces it for several titles) the drain changes shape entirely: a C helper meters the packet out against accumulated VU cycles, carrying a residual size and a rolling address across calls. That helper had never been executed by a test — the existing XgKickHack case deliberately issues no kick, since it is about register spilling around the sync site rather than the drain. Also covers the end-of-program drain of a kick issued in the delay slot of an E-bit branch. That kick is the last thing the block analyses, so its latency never elapses inside the block and the emit loop's own drain never runs for it. An ordinary E-bit doesn't reach the path — the appended delay-slot pair decrements the latency first. The non-hack wrap case can only assert its tail: the split's first half goes out through CopyGSPacketData, which the test sink does not hook. The tail is what pins the arithmetic anyway, since it must be exactly (packet size - distance to the top) bytes taken from offset 0. Mutation-checked: disabling the split fails only the wrap case, disabling the end-of-program drain only the delay-slot case, and dropping the helper's rolling-address advance only the two-chunk case. |
||
|
|
46307d2872 |
tests: cover the E/M/T-bit exit stubs on branches and jumps
When the E bit lands on a branch pair, the branch and the end-of-program delay slots coincide: the branch runs, its delay slot runs, and the program stops without executing the target. All the branch still decides is VI[REG_TPC] — where the next dispatch of this VU picks up. Naming that PC wrong doesn't crash anything, it silently restarts the microprogram in the wrong place. microVU handles each branch shape with its own hand-written exit stub and its own incPC arithmetic, and none of normBranch's, condBranch's or normJump's had any coverage. Each case here asserts the parked PC as an absolute pair index and pins which successor actually ran, since a stub that picks the wrong one still parks at a legal-looking PC. The backward unconditional case is separate because a stub deriving the parked PC from the fall-through still looks right on a forward branch. The M-bit cases cover the same stubs used as a mid-program sync rather than a terminator, and are scored per engine for the reason the T-bit cases already are: the JIT compiles branch and delay slot as one unit and parks at the resolved successor, while the interpreter's break fires on the branch pair and leaves TPC on a delay slot it never ran. Also covers the T-bit jump stub's INTC raise and the VU1 instantiation of the runtime jump-compile entry point, which had never been called. Mutation-checked: inverting condBranch's E-bit polarity fails exactly the two conditional E-bit cases, inverting its M-bit polarity exactly the two conditional M-bit cases, dropping normBranch's E-bit target exactly the two unconditional cases, and dropping normJump's TPC store exactly the jump case. |
||
|
|
f553eac8a6 |
tests: cover Q/P instance rotation across a branch
The VU's Q and P scalars are double-buffered, and microVU keeps both buffers live in one host vector. When a DIV or an EFU op's latency expires mid-block the current instance flips, but every compiled block is entered assuming instance #0 — so a branch out of that block has to physically swap the two lanes first. Nothing in the suite had ever branched with a Q or P value in flight, so that swap was unreached. A dropped swap is silent: the target block reads the previous quotient, which is an ordinary float that propagates through the rest of the microprogram. Each case therefore seeds the stale buffer with a distinct sentinel and asserts the absolute post-branch value — a JIT-vs-interp diff alone would also pass if the swap were dropped on both sides. Also covers mVUendProgram's division-flag transfer, which only runs when the program ends inside the FDIV flag latency. Every other Q test drains the pipe with VWAITQ first, so that path had never run either. STATUS is opted out of the cross-engine diff there: the console captures already settled that the sticky D/I bits accumulate, which microVU does and the shared interpreter does not (vu_sticky_console_conformance_tests.cpp). Mutation-checked: neutralising the Q swap fails exactly the three Q cases, the P swap exactly the P case, and dropping the end-program mVUdivSet exactly the two flag cases. |
||
|
|
28a94f9ec0 |
tests: reach the by-element FMUL fold on plain MULbc without a config change
The previous commit said MULbc never reaches the fold under the shipped clamp default. That is only true at the full xyzw mask: the Ft clamp that suppresses the fold is gated on the full mask, so any partial multi-lane mask -- the common shape in real microprograms -- takes the fold with the default config. Adds that case and corrects the comment. Mutation-checked: pinning the fold's lane operand to 0 fails it. |
||
|
|
1c9d82a8ca |
tests: pin VU MAX/MINI sign-magnitude ordering
The PS2 VU has no infinity and no NaN. An exponent-0xFF word is an ordinary very large number that MAX has to order like one, and a denormal is an ordinary very small number that MINI has to order like one. Neither engine uses a float compare: the interpreter branches on "are both operands negative" and picks a signed integer min/max, while microVU flips the low 31 bits of every negative lane so a single signed compare works. Two different derivations of the same order, which is what makes diffing them worth doing. Covers the packed helpers across exponent-0xFF words, both zeros, denormals, both-negative pairs and equal operands; both broadcast and I-register operand shapes; and the scalar single-destination-lane helpers, which had no coverage at all and are the ones that would be quietly replaced by an IEEE FMAX/FMIN by anyone simplifying the emitter. Every case carries the expected bit pattern, so the suite states the architectural answer rather than only asserting the two engines agree. Also pins a divergence found while writing this: microVU folds the I-bit immediate in as a constant and clamps an exponent-0xFF immediate down to max-finite while doing so, keeping its sign, where the interpreter stores the raw word. x86 mVU has the identical clamp, so this is upstream behaviour we share -- but it means the interpreter is not the oracle for MAXi/MINIi/ADDi/MULi with such an immediate, which is worth knowing before it costs someone a divergence hunt. Scored per engine, with a companion case showing agreement returns once the overflow clamp is off. Validated by mutation: neutralising the negative-lane bit flip in the packed helper fails exactly the two both-negative packed cases, and in the scalar helper exactly the two both-negative single-lane cases. Every mixed-sign and both-positive case stays green, since a plain signed compare is correct there. |
||
|
|
1733ad6542 |
tests: sweep every broadcast lane of the VU upper-pipe FMACs
Two thirds of the VU upper pipe is broadcast forms, and the lane they read is encoded in the opcode rather than an operand field, so the only thing separating VMULy from VMULz in the emitter is a table index. A transposed index produces a numerically plausible result that nothing asserts on -- it surfaces as subtly wrong geometry in one game. Before this, MAXx/y/z/w, MINIx/y/z/w, MADDx/y, MSUBy/z/w, MULw and SUBy/z had never been emitted by any test; microVU_Upper had executed 63 of its 119 functions. Each of the 48 cases carries a hand-computed expected vector, so the suite knows the right answer independently of both engines -- a diff-only test would pass vacuously if a mis-encoded instruction decoded to something inert in both. Ft holds four pairwise distinct values so every broadcast lane yields a distinct result. Also covers the by-element FMUL fold on all four lanes. MADDbc, MSUBbc and MULAbc reach it under the shipped clamp default; plain MULbc at a packed mask asks for an Ft clamp and so never does, and gets its own case with the overflow clamp off. Validated by mutation: pinning the fold's lane operand to 0 fails exactly the 14 non-x cases whose op reaches the fold, and no others. Adds the MAX/MINI and ADDA/SUBA broadcast encoders VuEncode.h was missing. |
||
|
|
05ff26bbe7 |
tests: cover COP2 macro broadcast MAX/MINI and the conversion family
recVMAXx/y/z/w, recVMINIx/y/z/w and most of recVITOF*/recVFTOI* had no coverage: 58 of the 140 functions in iR5900Misc-arm64.cpp were never executed, and the recCOP2_* implementations they forward to went with them. Both groups are worth more than the arithmetic ops that already have tests. MAX/MINI does not use a float compare at all -- the PS2 VU has no inf or NaN, so cop2EmitIntegerMax orders operands as sign-magnitude integers via CMGT corrected by a both-negative mask. That correction is invisible unless both operands are negative, and the decision to compare as integers rather than with Fmaxnm only shows up on exp-FF words, which is precisely what a QMTC2 leaves in a register. Both are pinned here. ITOF/FTOI carry their scale in the opcode, so a wrong shift is a silently wrong magnitude, and FTOI has to saturate where the host instructions disagree about out-of-range conversions. Oracle is the VU0 interpreter through EeRecTestHarness's JIT-vs-interp diff, with absolute expectations alongside wherever the architectural answer is unambiguous, so a failure says which side moved. Verified by mutation: dropping the both-negative correction fails exactly the two negative-operand cases and leaves the positive-only broadcasts green. The I- and Q-register broadcast variants (VMAXi, VMINIi, VADDi, VADDq and friends) are still uncovered -- EeRecTestHarness has no way to seed VU0's I or Q registers, and building that out belongs in its own change rather than half-done here. pcsx2/arm64 line coverage 76.58% -> 76.95%, functions 82.57% -> 83.91%; iCOP2-arm64.cpp 77.4% -> 82.0%, iR5900Misc-arm64.cpp 60.5% -> 65.2%. recompiler_tests 1569 -> 1587. |
||
|
|
bd6697f277 |
tests: cover the arm64 VIF unpack generators
Both NEON unpack generators were entirely untested: Vif_UnpackNEON.cpp sat at
0% line coverage and Vif_Dynarec.cpp at 1.9%, together ~680 lines of lane
shuffling, sign extension and mask merging that every game drives on every
frame. A transcription slip in there produces silently wrong geometry rather
than a crash, which is the worst failure mode to have no gate for.
The oracle is VIFfuncTable (Vif_Unpack.cpp) -- the scalar UNPACK_S/V2/V4/V4_5
templates, plain C++, architecture-neutral, shared verbatim with upstream.
Deliberately not _nVifUnpack: on arm64 that dispatches through the NEON
routines for mode 0, so it would compare our codegen against our codegen.
ReferenceUnpack drives the scalar table with _nVifUnpackLoop's addressing, and
both generators are checked against it.
54 cases grouped by the failure each would catch rather than by enumerating the
cross product: per-format expansion (both signedness values for every sub-32-bit
format), the four mask codes including cycle-indexed columns and write-protect,
MODE 1/2/3 with row write-back, CYCLE skip and fill, the num/wl 256 boundaries,
and VIF0 as well as VIF1.
The W lane of V2_32 and the V3_* formats is excluded from the comparison: both
generators zero it in cases the scalar table does not ("tested on ps2", and the
x86 SSE generator agrees), while Vif_Unpack.cpp routes V3 through UNPACK_V4 on
purpose for Ape Escape 3. Re-deriving the generators' iteration arithmetic in
the test would only restate the code under test, so W is instead pinned by the
one independently checkable fact -- an aligned V2_32 unpack zeroes it.
Verified by mutation rather than by passing: forcing the column register to
cycle 0 fails exactly the three multi-cycle column cases and nothing else, and
zero-extending the 8-bit signed path fails exactly S8/V2_8/V3_8/V4_8 while the
unsigned variants stay green.
pcsx2/arm64 line coverage 74.65% -> 76.58%; Vif_UnpackNEON.cpp 0% -> 91.8%,
Vif_Dynarec.cpp 1.9% -> 82.7%. recompiler_tests 1515 -> 1569.
|
||
|
|
47fa49f3d2 |
Build: add source-based coverage for the ARM64 recompilers
USE_COVERAGE instruments the build with clang's -fprofile-instr-generate
-fcoverage-mapping, exposed as the clang-coverage preset (build-coverage/,
inheriting clang-devel so the dev asserts stay on, Qt off since nothing in
the test path needs it).
The instrumentation is tree-wide rather than scoped to pcsx2/arm64: much of
the JIT is inline code living in headers that get pulled into core and common
translation units, so narrowing at build time would drop counters for exactly
the code we care about. tools/coverage.sh narrows at report time instead,
where the filter is exact.
The script builds the five gtest binaries, runs them with per-process profile
files, merges, and reports scoped to pcsx2/arm64/ (--scope all widens to the
other ARM64-only sources). Two hazards it defends against:
- cmake --preset takes the source dir from the working directory and ignores
-S, so running this through the /home/bmd/ARMSX2 symlink bakes the
symlinked path into every coverage mapping. It cds first.
- llvm-cov does not error when --sources matches nothing; it reports every
file it has data for, which reads as a plausible whole-tree number. The
filter prefix is read back from CMAKE_HOME_DIRECTORY so it always matches
what the compiler recorded, and a row-count tripwire fails the run if the
report escapes its scope anyway.
Baseline for pcsx2/arm64/: 74.65% lines, 79.94% functions, 77.70% regions.
|
||
|
|
737966bbad |
CI: publish the libretro core and SDL handheld build from the nightly
Both jobs have been in build-all.yml since it was written, so they build and get artifact-uploaded on every push, but neither was ever added to nightly.yml. The result is that the RetroArch core and the bare-kmsdrm handheld frontend are the two targets with no published download at all, which is backwards: those users are the least likely to build from source. Wire both into the nightly with the same inputs build-all.yml passes, and collect their .tar.zst into the release. Non-blocking, like the mobile jobs: publish's guard names only the PC jobs, so a failure here costs the asset rather than the release. Unlike the mobile jobs they cannot be marked continue-on-error, since that key is not permitted on a job that uses a reusable workflow, so a failure will still redden the run. That matches how they already behave in build-all.yml. Both build at OVERRIDE_HOST_PAGE_SIZE=4096, so the release notes say so. Neither is packaged as an AppImage on purpose: the AppImage runtime wants FUSE, and a bare-display handheld is exactly where that cannot be assumed. package-sdl.sh already bundles the libs it built and points the rpath at $ORIGIN/lib, so the tarball is self-contained without it. |
||
|
|
9ec5c46ab6 |
CI: give nightly release assets one dated, self-describing name
The nightly attached whatever filename each build job happened to produce,
and the job families use three unrelated conventions: the PC jobs share
name-artifacts.sh (armsx2-<target>-sha[<sha>]), Android bakes in a
versionCode derived from Unix seconds, and iOS ships a fixed
ARMSX2-iOS-unsigned.ipa. So a downloaded file carried no date at all (that
lives only in the release title), iOS carried no build identity whatsoever
(two nightlies collide as "(1)"), and GitHub rewrites the '[' and ']' of
sha[...] to '.' on asset upload, leaving names that read as though they
have a second file extension.
Rename in the publish job as assets are collected, to
ARMSX2-nightly-<YYYYMMDD>-<sha>-<platform>.<ext>
which keeps the per-workflow CI artifact names untouched for the Actions
tab and for build-all.yml, so the blast radius is the release page only.
A missing artifact (a failed non-blocking job) logs MISSING and the step
still exits clean, so it costs that asset rather than the release.
Also replace the one-line platform list in the release notes with a short
per-file legend, since which Linux AppImage to take is not something a
downloader can infer, and picking the wrong page size just fails to run.
|
||
|
|
55c22d3ca2 |
GS: fix missing draws on Adreno when texture replacements are loaded
Tales of the Abyss with an HD texture pack loses its entire 2D text layer on Vulkan/Adreno (#442). The replacement shifts the source alpha range, which flips those draws to require_one_barrier; the draw then reads the render target back while it is still bound as the colour attachment, and the driver silently drops it. Device A/B on Turnip/Mesa 26.1.2 + Adreno 650: both in-pass forms fail -- the subpassLoad input attachment and the feedback-loop-layout texelFetch sampler -- while reading a separate copy of the target renders correctly. Not tile-size related; the text is missing at 1x as well as 4x. Route this through the driver-bug database instead of another inline vendor test. That database was built for exactly this and had never been consulted: its sources were compiled only under if(ANDROID) and were missing from pcsx2.vcxproj, and every call site was #if defined(__ANDROID__) -- so every rule was dead on the ARM Linux handhelds we test on, including the device that reproduces this bug. Resolve the driver profile on all platforms and give it its first HasBug/UsesWorkaround consumer. The mobile-only consequences (runtime GPU profile, GS pool tuning) stay Android-gated on purpose: off Android the detector classifies every non-Mali GPU as Adreno, and desktop pool sizing is not this code's business. Non-Adreno targets resolve to zero rules and zero workarounds, verified on Apple/Honeykrisp. Narrow the workaround to when replacements are loaded. NFS Underground pushes 608 barrier draws per frame through the same in-tile self-read with no pack and renders correctly, so the read is fine for ordinary blending. Applying the copy unconditionally cost +38%/+40% frame time at 3x/4x on an NFSU dump replay (copies 5 -> 348 per frame, render passes 62 -> 391) for no correctness gain. This replaces an is_adreno block that forced the subpassLoad path on regardless of INI. Its own comment already recorded that the feedback-loop sampler drops content; what it missed is that subpassLoad drops it too, so it was choosing between two broken reads. Adreno joins vendor_allows_fbfetch so removing the force does not demote the proprietary blob to the per-primitive barrier path, and DisableFramebufferFetch now actually takes effect there instead of being eaten. LoadTextureReplacements joins RestartOptionsAreEqual: it now selects the tfx.glsl RT-read variant at shader-compile time, so toggling it in place would leave the feature flag and every compiled pipeline disagreeing with the setting. OverrideTextureBarriers still wins when set explicitly -- 1 restores the in-tile path, 0 forces the copy for anyone who hits this without a pack. |
||
|
|
b2d57d93f6 |
GS: correct the textureCompressionBC comment on the replacement decode path
The comment asserted as fact that Vulkan textureCompressionBC is false on "Adreno 650 / Snapdragon 865, and Mesa Turnip on any Adreno". Neither holds: Turnip reports it true, and on Adreno 650 the Qualcomm blob gained BC at driver 512.614 (vulkan.gpuinfo.org splits cleanly across that revision). It is a driver property, not a hardware one. No behaviour change -- the CPU decode is already gated on the runtime feature bits. The comment sent an investigation down the wrong path, which is the cost being fixed here. |
||
|
|
43e3430b61 |
SPU2: remove the SVE2 reverb FIR and its MT6899 tuning header
spu2_sve2_fir.h offered SVE2 versions of the reverb FIR behind SPU2_HAS_SVE2_COMPILER, which is off on every target we build. It has never been compiled by anyone, and it would not compile if tried: the upsample coefficient table declares 32768 in an int16_t initializer, which is a narrowing error, not a warning. Clang rejects it outright. The arithmetic is wrong too. ReverbDownsample_reference accumulates the products and then does out >>= 15; the SSE, AVX and NEON paths get that scale implicitly from mulhrs / vqrdmulhq_s16. The SVE2 version accumulates with svmlalb/svmlalt and hands the raw sum to clamp_mix with no shift at all, so every sample would saturate. The same coefficient the initializer rejects is one the reference clamps to 32767 in make_up_coefs, so the table was wrong on its own terms. That leaves spu2_mt6899_tuning.h unreferenced: its only consumer anywhere was GetFeatures() from inside the SVE2 block. It held Cortex-X925 cache geometry, prefetch distances and thread-pinning helpers for a device that is not one of our targets. RegisterNEONBackend now installs the NEON FIR unconditionally, which is what it already did in every build that exists. |
||
|
|
2ce8c27e13 |
EE/arm64: remove the FORCE_INTERP_* per-category bisect switches
Eleven commented-out defines in iR5900-arm64.h, each selecting the interpreter for one opcode category (branch, jump, move, shift, ALU, arith-imm, mult/div, memory, COP0, FPU, COP2), consumed by twelve #ifdef/#else/#endif pairs across eleven files. Using one meant editing the header and rebuilding. pcsx2-eerunner --rec-fallback <groups> does the same bisect at runtime with no rebuild and no source edit, over a full VM boot, so it stays game-faithful. The transform keeps every #else body byte for byte - the diff is deletions only, no added or reindented lines. Also drops two comments that referenced the defines. recompiler_tests: 1513 passed, 2 skipped, 0 failed. |
||
|
|
516650a066 |
IOP/arm64: remove the unused psxRecompileCodeConst templates
psxRecompileCodeConst0/1/2/3 and the five PSXRECOMPILE_CONSTCODE macros came across in the JIT transplant as x86 const-propagation dispatch templates. Nothing invokes them: the arm64 IOP recompiler emits through its own allocator-aware macros in iR3000Atables-arm64.cpp, which handle the const cases inline. The one PSXRECOMPILE_CONSTCODE0 mention left in that file is a comment noting the x86 form the arm64 macro corresponds to. The R3000AFNPTR / R3000AFNPTR_INFO typedefs went with them - they had no other users. psxRecompileIrxImport sat between these declarations and is very much live (iR3000Atables-arm64.cpp:114/123/125), so it is kept, unchanged, next to the branch-handling section. |
||
|
|
9eb25cc3f0 |
EE/arm64: remove VERIFY_NATIVE_CODEGEN
An in-JIT differential mode: snapshot the guest register file, run the native codegen, flush, then call back into the interpreter to compare. In practice it only ever covered COP2 (opcode 0x12), and VERIFY_NATIVE_CODEGEN was never defined anywhere, so all 224 lines compiled out of every build. The offline tools do this better. pcsx2-eerunner localizes JIT-vs-interp divergence over a full VM boot, --divtrace names the first divergent op, and --rec-fallback bisects by opcode group without a rebuild. None of them perturb codegen the way an inline verify hook does. Removing the #ifdef branch leaves the remaining else-body as a bare scope, so it is unwrapped and re-indented here. recompiler_tests: 1513 passed, 2 skipped, 0 failed. |
||
|
|
d1c483b2b3 |
SPU2: remove the unwired NEON mixer/reverb/DC-filter headers
These four headers came in with a contributor drop aimed at a MediaTek MT6899
(Cortex-X925). The useful part of that drop was kept: spu2_neon.cpp registers
the 39-tap NEON reverb FIR on every arm64 target. The helper headers were
never included by any translation unit, and spu2_neon.cpp carried a note
explaining why - they target mixer.cpp and a "ReaVerb.cpp" that does not exist
here, and they use the MSVC-only __forceinline unguarded, so they would not
compile as written.
They are also wrong where it counts. spu2_neon_mixer.h's GaussianInterpolate,
the one on the hot path at 24 voices x 48 kHz, disagrees with Mixer.cpp three
ways:
- Truncation order. GetVoiceValues shifts each tap ((coef * sample) >> 15,
four times, then sums). The helper sums the four products and shifts once.
Arithmetic shift is floor division and does not distribute over addition;
over 200k random tap sets the two forms differ 95.8% of the time. The
hardware truncates per tap, so this is less accurate, not more. Its own
scalar fallback has the same bug, so it does not match "original
behavior" either.
- Element type. It takes const int16_t* and does vld1_s16, but DecodeFifo is
s32[32].
- Addressing. It assumes four contiguous samples; the real index is
(DecPosRead + n) % 32, which wraps.
spu2_neon_dcfilter.h is merely pointless rather than wrong: a two-lane f32
operation on a serial IIR chain with no ILP to exploit, whose batch entry
point just loops the per-sample one, and whose combined convert/clamp/filter
path round-trips through memory.
spu2_optimize.h only reached the build through spu2_neon_reverb_ex.h.
spu2_sve2_fir.h and spu2_mt6899_tuning.h stay: spu2_neon.cpp includes both.
|
||
|
|
6e5770be8d |
iOS: remove the unreferenced TestHarness, QAProbe and SifRingBuffer
All three compiled into iOS builds with no caller anywhere - nothing in platforms/ios names them and no translation unit includes their headers. They came in with the iPSX2 runtime import and were never wired to the shared core. TestHarness (2513 lines) injected R5900 machine code straight into eeMem at 0x81F00000, pointed cpuRegs.pc at it, and had the vsync handler scrape pass/fail out of guest memory - a BIOS/SIF/IOP-independent way to check EE JIT instruction accuracy. tests/ctest/core/recompilers covers that ground now, and does it without a booted VM. It was also gated on an iPSX2_TEST_HARNESS environment variable, which shipped code is not supposed to carry. QAProbe drove scripted QA capture; SifRingBuffer held a standalone SIF ring mirror for hang diagnosis. The TestHarness mentions left in Gif_Unit.h, vu_capture.h and iR5900-arm64.cpp are comments about VuTestHarness and EeRecTestHarness under tests/ctest, which are unrelated to these files. |
||
|
|
fc55cd86f1 |
Android: remove the orphaned perf-bucket and PS1DRV trace headers
AndroidPerfBuckets.h wrapped hot EE/VU/GS/VIF paths in steady_clock reads and relaxed atomic adds, gated on ARMSX2_ANDROID_PERF_BUCKETS. It served its purpose: the 2026-06-17 run used it to pin the dominant EE cost on ee_interp_step. Its call sites are all gone now, so the header's claim that "the atomic counters and every call site stay compiled in either way" no longer holds, and nothing anywhere names AndroidPerfBuckets::. Its one remaining consumer was arm64/Vif_Dynarec.cpp, which included it twice on consecutive lines and used nothing from it - that duplicate include goes too. Perf jitdump covers this now. PS1DrvTrace.h supplied rate-limited PS1DRV_LOG_/RATE_/CHG_ macros for PS1-mode debugging, each gated on a PS1DRV_TRACE_<CAT> define. No translation unit ever included it. |
||
|
|
15eb94fb74 |
Remove EEDiffVerify: a live toggle in front of a dead diagnostic
EEDiffVerify was a throwaway EE recompiler-vs-interpreter differential
verifier, written to pin the True Crime NYC (SLUS-21106) texture-decompressor
corruption. It worked by emitting snapshot/verify hooks around each
straight-line op and re-running that op on the interpreter with stores
captured rather than applied.
Those hooks lived in the pre-transplant arm64/mac EE recompiler and in
vtlb.cpp, and
|
||
|
|
7aadd3ae64 |
Android: drop the duplicate vendored googletest
platforms/android/.../cpp/3rdparty/googletest was a byte-for-byte copy of the tree's own 3rdparty/googletest (66 files, 1.8 MB each; diff -r reports no differences). The root copy is the live one - the top-level CMakeLists adds it and tests/ctest links gtest from it. The Android copy existed only for the on-device test tree removed in the previous commit, and after that its sole remaining mention was a comment. |
||
|
|
c2bd7ce2a0 |
Android: remove the on-device JIT test tree superseded by the in-tree gates
platforms/android/.../cpp/tests/ held ~11,900 lines of EE, microVU, VIF, patch and ARM64 codegen tests, none of which any build compiled. Only the 35-line android_test_stubs.cpp was in the Android CMake, supplying no-op definitions so native-lib's JNI entry points stayed linkable. The tests drive the pre-transplant arm64/mac backend through EE_Test*, mVU0_Test* and mVU1_Test* hooks. Those fourteen symbols exist nowhere in this core, so the suite cannot compile against it. The stub file asked for exactly one thing - "restore the real tests once a compatible test backend is reconciled into this core" - and that reconciliation is what tests/ctest/core/recompilers now provides, alongside pcsx2-vurunner, pcsx2-eerunner and the DiffJitVsInterp harness. Also drops the surface that fed it: six JNI entry points, the ReportTestResults JNI callback, six NativeApp declarations, TestResult.kt and six mutableStateOf holders in MainActivityRuntime that nothing ever read. MainActivityRuntime called runEeJitTests/runEeSeqTests/runVifTests unconditionally during Android init, so every launch made three JNI round trips that only logged "recompiler self-tests are disabled in this build". |
||
|
|
1da102f45b |
Remove VU1Fingerprint: orphaned since the Android frontend refactor
VU1Fingerprint hashed VU1 microprograms at upload/dispatch so that known
shared libraries (sceVu*, RenderWare RpVU1*, ...) could later be swapped for
hand-written NEON kernels. Only the Phase 1.5 infrastructure was ever built;
the kernel database was intentionally left empty.
Its three call sites - Vif_Codes.cpp, MTVU.cpp and the pre-transplant
arm64/aVU.cpp - were dropped by
|
||
|
|
a5bf32fa92 |
GS: spin briefly before blocking on semaXGkick
MTGS blocked outright waiting for MTVU to finish a VU1 program, which drove the semaphore counter negative, so every MTVU-side Post() took the futex-wake syscall path. Use the existing UserspaceSemaphore spin-then- block wait so the common case -- MTVU posting within microseconds -- resolves in userspace. Only reachable in MTVU mode: semaXGkick is touched solely by this wait and MTVU's post, and WaitWithSpin() had no other caller. NFS Hot Pursuit 2 on an SM6115 handheld: 23.9 -> 34.2 fps, with MTVU syscall time falling from 11.8% to 0.06% of the thread. |
||
|
|
1b66b8d0b1 |
Memory: refuse Extended RAM while the ARM64 EE recompiler is enabled
ExtraMemory (the 128MB devkit map) is selectable from both shipping UIs with
nothing but a cosmetic compatibility warning, but the ARM64 EE recompiler is
MainRam-only: its LUT loop, recLutEntries, the recRAM advance, the alias mask and
the manual_page/manual_counter arrays are all sized to Ps2MemSize::MainRam, where
the x86 rec sizes the same things to ExposedRam. Pages 0x0200-0x1FFF keep the
unmapped default, so dispatching into one lands on UnmappedRecLUTPage -> recError
somewhere deep inside a game, with nothing tying the crash back to the setting.
Converting the LUT, the mask and the manual-page arrays together is the real fix
and has to land as one change;
|
||
|
|
2c02dc8b96 |
GS: drain the back queue in GSreopen before shredding its textures
GSreopen opens with GSParseTarget()->Flush(GSREOPEN), which flushes FRONT parse state and *queues* the resulting draw -- GSState::Flush does not drain. Both arms below it then hand the back thread's textures to the shredder: the device-loss arm (recreate_device && !recreate_renderer) calls PurgeTextureCache, ClearCurrent and PurgePool, and the other arm reads the texture cache back. Same class as the three window/vsync seams fixed in 578cb3a83a, which is where this was found and deliberately left alone pending the safety question. That question resolves in favour of draining. The worry was that on device loss the back thread could be wedged in the driver and waiting on it would hang recovery instead of recovering. It cannot: BeginPresent only reports DeviceLost off m_last_submit_failed, so the driver has already declared the loss by the time we get here, and post-loss calls return VK_ERROR_DEVICE_LOST rather than blocking. There is no backlog to chew through either -- SubmitVsync drains before ExecVsyncRecord and present never queues, so the queue is empty on entry and the Flush above is the only producer. Note this is NOT the Android suspend/resume path. Backgrounding kills the surface, not the device: BeginPresent returns FrameSkipped and resume comes back through onNativeSurfaceChanged -> MTGS::UpdateDisplayWindow, which 578cb3a83a already drains. The trigger here is genuine device loss, which the tree documents twice -- the Mali r44p1 blob that returns VK_ERROR_DEVICE_LOST on every game, and Rogue Galaxy hitting it at vkWaitForFences. DrainBackQueueBeforeDeviceMutation moves above GSreopen unchanged so it can be called from there. No test: this seam class has no runtime test surface, same as 578cb3a83a. |
||
|
|
11865fe54b |
VMManager: delete the four dead arch #else bodies
Upstream guards these blocks with `#ifdef _M_X86 // TODO(Stenzek): Remove me once EE/VU/IOP recs are added.` The arm64 JIT merge widened each guard to `#if defined(_M_X86) || defined(ARCH_ARM64)` and left the `#else` bodies in place, but Pcsx2Defs.h defines ARCH_X86 and ARCH_ARM64 exhaustively (anything else is an #error) and _M_X86 is set on every x86 build -- by BuildParameters.cmake for CMake and by common.props for MSVC. So none of the four `#else` arms can compile on any supported target, and the recs upstream's TODO was waiting on now exist. Collapsed all four. Two of them were near-duplicates of the live branch carrying stale Phase-4.3/6/7.8 commentary. The third, in ClearCPUExecutionCaches, is the one worth naming: its dead body reset recCpu and psxRec unconditionally, with a comment claiming that had to happen even when a rec is not the active provider. It does not, and dropping it is not a behaviour change on top of it already being unreachable -- ClearCPUExecutionCaches opens with Cpu->Reset()/psxCpu->Reset(), and every path that can make a recompiler active calls UpdateCPUImplementations() immediately followed by ClearCPUExecutionCaches() (VM init, and Execute()'s interpreter/rec toggle), so a rec is reset at the moment it becomes the active provider. x86 upstream never resets a non-selected rec either. No functional change on either arch. recompiler_tests 1443/1443. |
||
|
|
c2a4690474 |
VMManager: un-nest the ARM64 arm of the CPU extensions log
The `#ifdef ARCH_ARM64` sat inside the `#ifdef ARCH_X86` opened four lines above it, so it could never compile and the whole "CPU Extensions Detected" section was missing from every ARM log -- which is where we most want it. Made it an `#elif`, and reported something worth reading while there. NEON alone is architectural on AArch64 and therefore constant; what varies across our targets is LSE (absent on the ARMv8.0 handhelds) and SVE, since SPU2 selects its SVE2 path at compile time and a mismatch there is the first thing to check on a SIGILL report. cpuinfo_initialize() already runs unconditionally in CPUThreadInitialize immediately before this call, so the predicates are valid on ARM; only the early-hardware-check call site is x86-gated. Verified on an M2 Max under Asahi: "NEON LSE CRC32". |
||
|
|
71248899ba |
IOP: probe the SMC coverage array by HWADDR in the store stubs
The out-of-line RAM-store fast path computed its g_iopCodeCov index from the mirror-collapsed RAM offset (addr & (ExposedIopRam-1)), while iopCovAdjust and psxRecClearMem key that same array by HWADDR -- which strips the KSEG base but does not collapse the RAM mirrors, because recLUT_SetPage writes psxhwLUT[page] = -(pagebase << 16) and pagebase is 0 across the whole 0x00-0x7f RAM window. In the default 2MB configuration the two disagree. A block compiled at 0x00214000 registers coverage at granule 0x2140; a store to that same address probed granule 0x140, read zero, and returned without clearing. The C path would have cleared it -- psxRecClearMem's own O(1) reject and its recBlocks lookup both use HWADDR, so store and block agree there. So this was a real regression introduced with the stubs, not the pre-existing blindness the in-file comment claimed. Above the region gate every reachable address satisfies HWADDR == addr & (kIopCovSpan-1): bits 23-28 are zero, and the psxhwLUT subtraction for a KSEG mirror is exactly the removal of bits 29-31. So the fix is one extra AND, and none at all in the 8MB configuration where the RAM mask already spans the coverage window. The stub is now exactly as blind as the C path it replaces, no more: a store to a *different* mirror of a block's page still misses, because recBlocks is itself keyed by HWADDR. Rewrote the comment that asserted this was all harmless, since it would have stopped the next reader from looking. New test compiles a block at the 2MB RAM mirror and JIT-stores to it; red before this change (JIT 0x0BAD vs interpreter 0x1337). The two existing mirror tests use KSEG mirrors, where every domain agrees and the bug cannot show. recompiler_tests 1443/1443. |
||
|
|
2e40d9e799 |
GS: drain the back queue before the three window/vsync device seams
GSResizeDisplayWindow, GSUpdateDisplayWindow and GSSetVSyncMode all reach
g_gs_device from the MTGS thread -- swapchain resize, window recreate, vsync
change -- while the back thread is executing draws against that same device.
Every sibling seam of this class drains first; GSUpdateConfig does, and its
comment names this exact hazard.
|
||
|
|
848e22c3ae |
PS1DrvTrace: drop the include of a header that does not exist
PS1DrvTrace.h includes "arm64/InterpFlags.h", which is nowhere in the repo -- it was a JIT-bisect scaffold of commented-out INTERP_* toggles that went away when the JIT matured. Latent today because no .cpp includes PS1DrvTrace.h; the first consumer would have broken the build. The include was wrong even when the file existed: InterpFlags.h never defined PS1DRV_TRACE_<CAT>. Those are developer-set toggles, so say that instead. Syntax-checking the header standalone then turned up a second defect in the same class -- the documented macros PS1DRV_TRACE_LOG/RATE/CHANGE(CAT, ...) do not exist. CAT is part of the name: PS1DRV_LOG_<CAT>, PS1DRV_RATE_<CAT>, PS1DRV_CHG_<CAT>. Corrected, and verified by compiling a TU that enables all six categories and calls the macros. |
||
|
|
1e7661fe7a |
EE: clear the fastmem backpatch map on an arm64 recompiler reset
recResetRaw rewinds the code cache, which dangles every fastmem backpatch record in vtlb's map, but arm64 never called vtlb_ClearLoadStoreInfo(). x86 iR5900.cpp does, right after recBlocks.Reset(). Not a mispatch risk -- vtlb_AddLoadStoreInfo erases a colliding code_address before inserting, so a recycled address overwrites the stale record. It is a leak: nothing else prunes the map, so it accumulates for the entire VM session across every reset. Only the EE recVTLB registers backpatch info on either arch, so clearing it here is complete and cannot strand the IOP. |
||
|
|
e2474e14b9 |
GameDB: stop discarding a HWDownloadMode of Asynchronous
The GSHWFixId::HWDownloadMode apply path range-checks the raw wire value with
`value > Enabled && value <= Disabled`. Asynchronous (5) was appended after
Disabled (4) for ini/GameDB wire compatibility, so an entry asking for it was
accepted by the parser and then silently dropped here -- no diagnostic, no
fallback, the game just kept the default.
Bound the check at the last enumerator instead, and say in the comment that
this is a range check over the wire value rather than the accuracy ordering
Config.h forbids comparing on, so the next append updates it.
Twin of the VMManager.cpp warning fixed in
|
||
|
|
fe32ef2c27 |
EE: flush the source pins before QFSRV's raw adjacent-source load
recQFSRV has a fast path for Rs == Rt+1 that reads the contiguous 256-bit
{Rt:Rs} window straight out of cpuRegs.GPR with an unaligned raw Ldr. Its
comment claimed the window was "memory-coherent after the flushes above",
but those flushes are mmiFlushReg -> _deleteEEreg, which reconciles
const-prop and the scalar/NEON slots and never touches the pins.
Under lazy-dirty the pin is authoritative for UD[0] and armStoreEERegPtrRaw
elides the canonical store entirely for a pinned lane-0 write, so a pinned
source's lower half in memory is routinely stale mid-block. Nine GPRs are
pinned, which makes four adjacent pairs both-pinned -- ($at,$v0) ($v0,$v1)
($v1,$a0) ($a0,$a1) -- plus eight more with one pinned operand: exactly the
register range a funnel-shift memcpy loop uses. Failure mode is wrong data,
not a fault.
Every other raw quad-load site fixes this by merging the pin into lane 0
after the load, which cannot work here because the read straddles two guest
registers. Flush the two pins the window covers instead -- it covers exactly
r[Rt] and r[Rt+1], since sa <= 15 over their 32 bytes -- via a new
armFlushEEGPRPin. That keeps the fast path (0-2 extra Str) rather than
falling back to the ~10-instruction temp-buffer path, and the flushed pins
stay authoritative.
This was the last raw address-of-GPR read in pcsx2/arm64/; the GE-M2e sweep
in
|
||
|
|
77a4a2366a |
Merge pull request #435 from pstef/tests
Add more tests
Console-conformance suites for EE MMI / FPU control registers / loads and
stores / SA and the performance counters / the data and instruction
caches, IOP loads, stores and branches, VU0 COP2 macro mode, VU1 EFU, and
VU sticky flags. Each case is scored against a PS2 hardware capture on the
interpreter and the JIT separately rather than against the other engine,
so a defect the two share is still visible. 1439 -> 1509 cases.
Two fixes ride along, each confirmed load-bearing by reverting it:
* psxJALR read its branch target out of Rs after writing the link, so
`jalr $t0, $t0` jumped to the link address instead of the old Rs.
Reverting fails BranchDelaySlotOrderingMatchesConsole alone.
* MTSA masks to four bits. The console says `mtsa 0x10` leaves SA at 0
and `mtsa 0xFFFFFFFF` leaves 0xF, and the x86 recompiler already
masked on both of its paths, so this aligns the interpreter with what
the JIT had been doing. Reverting fails four cases.
Twenty-seven DISABLED cases record console divergences PCSX2 has not
closed yet, each a tripwire that starts passing when the gap does. None of
them disables a case that used to pass. One is ours: cop2EmitFlagUpdate
builds the MAC flag from sign and zero only and clears U/O outright, so
arm64 COP2 macro mode raises no underflow or overflow flag.
|
||
|
|
539f4f7247 |
Tests: guard the EE cache2 host mapping behind MAP_FIXED_NOREPLACE
MAP_FIXED_NOREPLACE is Linux 4.17+; Darwin's <sys/mman.h> has no such macro and Windows has no such header, so the unguarded include plus bare use broke the macOS CI job outright — macos_build.yml builds `unittests` and hard-fails when the recompiler_tests binary is missing, making this a compile error there rather than a skipped test. __has_include for the header, #if defined for the flag, and MapAt returns nullptr when neither is available. Both callers already GTEST_SKIP on a null return, so the two tests that need a page at a chosen host address skip off Linux and nothing else moves. Verified by compiling this TU with the macro #undef'd: clean build, those two skip, the other eight pass. Also records why they skip on a 16K-page kernel: all four candidate addresses are 4K-aligned but none is 16K-aligned, so Asahi, Apple Silicon and some Android reject every one. That is not the loader collision the comment assumed. |
||
|
|
3410a08c2b |
GS: skip the PS2 Z floor on Apple GPUs
Depth written from the pixel shader does not bit-match the fixed-function interpolation that a later read-only pass tests against on Apple GPUs, so a GEQUAL retest of the same geometry drops out along shared triangle edges and whatever was drawn underneath shows through as pinpoints of light. Black (SLUS-21376) speckles white over dark walls; God of War II's Athena statue speckles blue. The PS2 32-bit Z floor is the only reason a depth-writing draw takes the gl_FragDepth path at all. Its arithmetic is exact -- z*2^32, floor, *2^-32 is an integer op between two exponent shifts -- and it only ever lowers the stored value, so it masks the mismatch rather than causing it. Stray pixels on a Black wall against the software renderer, Vulkan on an M2 Max: floor + gl_FragDepth (shipping) 748 gl_FragDepth, floor removed 7062 floor - 1 Z unit 0 floor + 1 Z unit 263082 no gl_FragDepth at all 0 The disagreement is therefore under one PS2 Z unit, and a coplanar retest has no margin to absorb it. OpenGL reproduces at exactly 748 as well, which rules out the API and leaves the hardware. Whole-frame divergence from the software oracle drops 762 -> 2 on Black; the God of War II and NFS Underground dumps are unchanged. This is what no_ps2_z_quantization already does for Mali, and the floor only landed in January, so opting out returns Apple to long-standing behaviour. Wire the flag up for Metal and OpenGL too, neither of which read it before -- Metal is how Mac and iOS actually reach this, and it is the only backend the bug was reported on. Both now honour the INI override as well. Vulkan gates on driverID rather than vendorID because Apple silicon reports whoever wrote the driver: Honeykrisp is Mesa's 0x10005, not Apple's 0x106B. OpenGL matches on GL_RENDERER for the same reason -- an Intel Mac reports vendor "Apple Inc." with an AMD GPU. Mali is deliberately left out of the OpenGL gate; the Vulkan path opts it out for early-ZS, but that has not been tested on a Mali GL driver. The Metal change is uncompiled -- those translation units only build on macOS. |
||
|
|
187cb90287 |
GS: assert nothing rewrites clear state behind the scheduler
The flush wrappers guard what a deferred draw can be reordered past. Nothing guarded what a deferred draw can be made to lie about: a queued draw has not run yet, but GSTexture::m_state says whether its target still owes a clear, and rewriting that behind the scheduler's back moves the clear to the wrong side of the draw. That is invisible to a frame hash until it corrupts, and it is the shape of both bugs this design has produced so far - m_state going stale during deferral, and Recycle() parking a texture a queued draw named. So assert it directly, in SetState/SetClearColor/SetClearDepth. The scheduler itself rewrites this state by design, hiding a pending clear at enqueue and restoring it at emit, so it gets an explicit bypass rather than an exemption the assert has to guess at. Placement was measured, not assumed. The first attempt put the tripwire on the Vulkan image layout transition, on the theory that it sees every path to a texture. It caught nothing: deleting FlushDeferredDrawsFor() entirely corrupts Dirge of Cerberus, and the layout-transition assert stayed silent through all of it, because ClearRenderTarget/ClearDepth/InvalidateRenderTarget only touch CPU-side state and never reach the GPU at all. Moving the assert into GSTexture catches that same control on the first frame, and covers all six backends instead of one. The Vulkan assert stays as the GPU-side half, which no longer has to carry a job it cannot do. Devel-only; the corpus is unchanged and silent with it armed. |
||
|
|
aeb2b63e81 |
GS: route texture mapping and mipmap generation through the flush point
Update() already flushed queued draws before uploading, but the two other paths that write texture contents from outside the device did not: Map(), which the texture cache uses to stream uploads straight into a surface, and GenerateMipmap(), which reads every level and writes the smaller ones. Map() takes the same non-virtual wrapper treatment as the GSDevice entry points - the backend override becomes a protected DoMap(), so a backend cannot be reached without passing through the flush, and one that misses the rename fails to compile as abstract. GenerateMipmap() needs no such change: its only caller is the non-virtual GenerateMipmapsIfNeeded(), so guarding that one site covers all six backends and leaves the overrides untouched. Both use the narrow FlushDeferredDrawsFor(this) rather than a full flush, for the reason Update() does - the overwhelming majority of uploads are into a surface the queue has never seen. |
||
|
|
b9504ac752 |
GameDB: enable render-pass coalescing for Dirge of Cerberus
Dirge alternates 1:1 between a colour target and a mask target for most of the frame, which is the exact pattern the scheduler exists for. On a four-frame capture it takes the frame from 8035 render passes to 523, with every frame hash identical to the unscheduled path, and on an Adreno 650 handheld the difference is plainly visible. All six regional serials get it. This goes in the mobile overlay rather than the canonical GameIndex, because it is worth nothing on a desktop GPU where a pass boundary is cheap - the overlay ships on Android, iOS and ARM64 Linux handhelds, which is exactly the set of targets that pay for tile load and store. |
||
|
|
44e950e7d5 |
GS: expose render-pass coalescing in the settings UI and GameDB
Coalescing has only been reachable by hand-editing the INI. It needs to be switchable per game, because whether it is worth anything depends entirely on the title: it pays off when a game alternates between two render targets, and does nothing at all otherwise. Add the GameDB key coalesceRenderPasses, so a game that benefits can turn it on by itself, and a checkbox in Graphics > Advanced plus a Full Screen UI toggle next to the other driver-level GS options, so it can be tried on anything. It classifies as a user-hack fix, which means enabling Manual Hardware Renderer Fixes turns it back off - the usual escape hatch, and worth having while this is new. Note the consequence for A/B testing: GameDB is applied after settings are loaded, so for a game carrying the key, -set cannot switch it off. |
||
|
|
f2213660f0 |
GS: only flush queued draws for an upload the queue can see
GSTexture::Update drained the whole deferred-draw queue before every upload. Almost none of those uploads touch a texture the queue has heard of - the texture cache uploads into surfaces it just fetched from the pool - so the flush was throwing away coalescing for nothing. Use the narrow form, which flushes only when a queued draw actually reads or writes this texture. That is the same test the pool and deferred-clear paths already use. Attributing every flush in a four-frame Dirge of Cerberus capture put uploads at 46 of 305, second only to draws that carry a barrier. Removing them takes the capture from 543 render passes to 523; the rest of the corpus is unmoved and every frame hash is still identical to the unscheduled path. |
||
|
|
8c702e0d1d |
GS: let the pool see the textures the scheduler is holding back
Recycle() parks a texture that a queued draw still names instead of returning it to the pool, and FetchSurface did not know about that list. A request the parked texture would have satisfied fell through to CreateSurface, so the working set stayed one surface larger for the rest of the frame, and alternating onto that extra target cost pass boundaries the unscheduled path never paid. Diagnosed by probing the pass sequence at the counter itself: the two arms agreed exactly on every reordering metric - 103 same-attachment rebinds, 96 feedback flips, 88 feedback passes - and differed only in using 12 distinct render targets where the baseline used 11. Scan the parked list before the pool and flush only when the surface being asked for is in it. An unconditional flush here is the thing the scheduler exists to avoid. God of War II 513 -> 506 render passes over four frames (unscheduled: 506) MGS3 391 -> 388 (unscheduled: 388) Dirge 547 -> 543 (unscheduled: 8035) Frame hashes remain identical to the unscheduled path across the whole dump corpus. |
||
|
|
ef10efb3c7 |
GS: flush only for textures a queued draw can actually see
Most of what remained of the flush rate was bookkeeping, not hazards. Texture pooling and deferred-clear state run several times a frame against textures the queue has never heard of, and draining the whole queue for those threw away most of the coalescing. ClearRenderTarget, ClearDepth, InvalidateRenderTarget and Recycle now take the narrow form, which flushes only when the scheduler is holding a draw that reads or writes that texture. Recycle goes further and holds the texture back instead, returning it to the pool once the queue drains - the texture cache has already dropped its reference, so the queue is the only thing that can still name it, and keeping it out of the pool is what FetchSurface's flush was really for. That flush therefore goes away entirely. With the queue living longer, a third and fourth target come into view, so raise MAX_RUNS to four. Eight measures no better. Dirge of Cerberus, dump replay: 72.4 flushes and 139 render passes per frame, down to 21.6 and 67. Frame hashes identical across the whole dump corpus. |
||
|
|
82b862580c |
GS: let the second-pass draws defer as well
alpha_second_pass and blend_multi_pass were rejected on the theory that they re-read the target between their own passes. They do not: both are extra draws the backend issues inside the same RenderHW call, into the same attachments, with different pipeline state and the same geometry - verified in the Vulkan and OpenGL backends, neither of which ends the render pass for them. The whole config copies by value, so they ride along with the record for free. Only their own barrier requests still disqualify a draw, for the same reason the primary one does. This was the single largest source of forced flushes on the Dirge of Cerberus dump - 37.9 of 72.4 per frame. |
||
|
|
33e7def66f |
GS: keep target state visible while draws sit in the scheduler
GSTexture::State is not private to the backend. The texture cache reads it in seven places to decide whether a target has been written to yet, and the backend flips it to Dirty at the moment it picks the attachment's load op - which, for a deferred draw, is much later than the game asked for it. A queued target therefore looked Cleared or Invalidated to the texture cache for the whole deferral window, and it took the wrong branch. Apply the transition at enqueue instead, so the window is unobservable, and stash the original state on the run to hand back just before emitting it - the backend still has to see Cleared or Invalidated to choose the right load op, and it sets Dirty again itself. Caught by the dump-corpus frame hashes: FlatOut 2 diverged with a single open run, where deferral is supposed to be identity by construction, and Katamari Damacy diverged once two runs could reorder. |
||
|
|
923da1b811 |
GS: coalesce across a target alternation with two open runs
Lets the scheduler keep one run open per target instead of one in total, which
is what actually collapses a ping-pong. Draws to target A accumulate in one run
while draws to target B accumulate in another; at flush each run is emitted
contiguously, so the alternation costs two render passes instead of two per
draw pair.
Reordering between runs is only legal while the runs cannot observe each other,
so three checks force a flush instead:
- Read-after-write: the incoming draw samples a texture that is an attachment
of an open run.
- Write-after-read: the incoming draw writes a texture some queued draw
samples. This fires even when the draw is joining the run that owns that
attachment, because the queued reader may be in the other run.
- Attachment overlap: a new run may not share rt or ds with an existing one.
A partial match, such as two colour targets sharing a depth buffer, is
exactly the aliasing write that cannot be reordered.
Order within a run is never changed, so same-target results are untouched.
Measured with gsrunner over the .gs dump corpus, off vs on, every frame hash
identical in both arms:
Dirge of Cerberus 8035 -> 1117 render passes (-86%, ~1004 -> ~140/frame)
God of War II 506 -> 506
FlatOut 2 416 -> 416
Katamari Damacy 82 -> 82
MGS3 388 -> 388
Ratchet & Clank UYA 41 -> 41
The unchanged titles are the expected result, not a failure: they do not
alternate targets, so their draws flush straight through.
Dirge lands at ~140 passes/frame rather than the handful the pattern suggests,
so something is still forcing a flush around 17 times per frame. Worth chasing,
but it is a tuning question on top of a working reduction.
|
||
|
|
1620453785 |
GS: add the render-pass scheduler, one open run
Introduces GSPassScheduler, which holds hardware draws in a queue instead of handing them straight to the backend, and emits them when something needs to observe the target. This is the plumbing only: with a single open run the queue can only ever hold draws that are already consecutive and already share a render pass, so GPU order is identical to before by construction. Coalescing across a target alternation - the point of the exercise, and where the win on a tiler is - needs a second open run and lands separately. Deferral copies the draw config and, importantly, its geometry: config.verts and config.indices point into GSState's per-draw buffers, which the very next draw overwrites. Records index into two vectors rather than holding pointers, since those vectors reallocate as a run grows; they are never shrunk, so a scene reaches its high-water mark and then stops allocating. Only "plain" draws are deferred - no barrier, no feedback loop, no destination alpha, no colclip, no second pass, no drawlist. Everything else renders immediately after flushing, so a game that never ping-pongs targets keeps exactly today's behaviour. Gated on EmuCore/GS/CoalesceRenderPasses, default off, deliberately not in the restart set: toggling it just stops deferring. Verified over the .gs dump corpus (Dirge of Cerberus, God of War II, FlatOut 2, Katamari Damacy, MGS3, Ratchet & Clank UYA) with gsrunner: every frame hash and every render-pass count identical between off and on. On Dirge, 96.5% of draws take the deferred path and the longest run reaches 204 draws, so the copy, the run-key match and the flush hooks are all genuinely exercised. |
||
|
|
1cbbe8b999 |
GS: route texture upload and readback through the flush point
The GSDevice entry points are not the whole story. A CPU upload into a texture
goes through GSTexture::Update and a readback goes through
GSDownloadTexture::CopyFromTexture, neither of which is a GSDevice method, so
both would let a deferred draw be reordered past work that must not move:
- Update into a target a queued draw writes, or into a texture a queued draw
samples. In the original order the draw sees the old contents; deferred
past the upload, it would see the new ones.
- CopyFromTexture is the actual readback - CreateDownloadTexture only
allocates, and the texture cache reuses those - so guarding creation would
not have covered it.
Same treatment as the device entry points: the virtual becomes a protected
Do* form and the public name becomes a non-virtual wrapper that flushes first,
which the pure base virtual makes compiler-enforced across all six backends.
Still unguarded and left to the debug tripwire: GSTexture::Map and
GenerateMipmap. Map cannot get the same mechanical rename because
GSDownloadTexture declares an unrelated Map in the same headers, and neither
is on a path that writes a render target today.
|
||
|
|
518418b3ac |
GS: route texture-touching device work through a flush point
Preparation for render-pass coalescing, which needs to hold hardware draws back and emit them later, grouped by target. That is only safe if nothing can observe a render target between a draw being submitted and that draw actually running, so every GSDevice entry point that reads or writes texture contents has to get the chance to flush first. Rather than maintain that as a list of call sites - there are ~130 in the HW renderer and texture cache alone - make it structural. The virtuals that do the work move to a protected Do* form, and the public name becomes a non-virtual wrapper that calls FlushDeferredDraws() first, following the DoStretchRect / DoMerge / DoApplyShaderChain convention already used here. A caller holding a GSDevice* then cannot reach the backend without passing through the flush, and since the base virtuals are pure, a backend that misses the rename fails to compile as abstract rather than silently skipping the guard. Converted: CopyRect, DrawMultiStretchRects, UpdateCLUTTexture, ConvertToIndexedTexture, FilteredDownsampleTexture, RenderHW, BeginDSAsRT, BeginPresent, HintReadbackSource. The base-class non-virtual operations take the guard inline instead: the clears, Recycle and the texture pool, the whole StretchRect family via its single root DoStretchRectWithAssertions, and the present-path effects. Backends call their own Do* directly where they already called themselves - the RT clone VK/OGL/DX11/DX12 do from inside RenderHW for a feedback loop - so emitting a deferred draw does not re-enter the flush. FlushDeferredDraws() is empty here and the scheduler lands next, so there is no behaviour change. |
||
|
|
1daeee29d5 |
SDL: report argument errors on stderr instead of into the void
ParseCommandLineArgs reported bad arguments through Console.Error*, but argument parsing runs before the console and file log sinks exist, so the message reached neither the terminal nor emulog.txt. An unrecognised flag exited silently with no diagnostic anywhere, which reads as a crash. Write them to stderr, as --help in the same function already does. The unknown- argument case also names the trap it exists to catch: this frontend takes none of the Qt frontend's flags, so -fullscreen or -bigpicture land here, and both are things it already does unprompted. |
||
|
|
ec57f7f1c6 |
GS: stop forcing an RT feedback read for Ad-masked blends on fbfetch
A destination-alpha blend with alpha writes masked (blend_c == 1, !colormask.wa)
was given require_one_barrier wherever framebuffer fetch was available, on the
reasoning that fbfetch makes the feedback read cheap. The read is cheap. The
render pass is not: binding the target as an input attachment changes the pass
configuration, and OMSetRenderTargets ends the pass every time that flag flips.
NFS Underground flips it ~697 times a frame against only 40 real target
switches, so nearly every pass boundary in the frame came from this.
Drop the framebuffer_fetch term, leaving only the no-texture-barrier case where
the fallback already copies the RT and no pass boundary is at stake. That term
was ours; upstream gated this path on texture_barrier (later texture_barrier ||
multidraw_fb_copy) until it was made unconditional, and never on framebuffer
fetch. On turnip, which exposes the EXT spelling of the rasterization-order
extension, our term switched the path on for exactly the drivers where a pass
boundary is the dominant cost.
Adreno 650 NFSU 440 -> 64 passes/frame, 27.6 -> 7.6 ms
FlatOut 2 954 -> 102, 37.2 -> 16.4 ms
Mali-G52 NFSU 746 -> 69, 276.6 -> 96.8 ms
FlatOut 2 892 -> 96, 373.6 -> 145.0 ms
Correctness is unaffected: Ad blends that genuinely need software blending are
still forced into it by blend_requires_barrier, which tests blend_ad against the
RT alpha scaling separately. Scored per-pixel against the software renderer both
GPUs came out more accurate, not less - on Adreno the worst-case error in NFSU
halves, 19 to 8, with no pixel off by more than 16. Katamari, MGS3 and Ratchet &
Clank are bit-identical on both.
|
||
|
|
c4d0a8a47c |
EE: record why recRAMCopy is sized to MainRam, not ExposedRam
Upstream x86 sizes the snapshot buffer, its guard and its LUT to Ps2MemSize::ExposedRam, which follows the 128MB devkit mapping; ours uses MainRam. That looks like a portable one-line divergence and is not: this rec is MainRam-only throughout — recLutEntries and the recRAM advance in recReserveRAM, the (MainRam / _64kb) alias mask in recResetRaw that folds all guest RAM into a 32MB LUT window, and the manual_page / manual_counter arrays. Widening the buffer on its own would hand two blocks aliased to the same LUT entry two distinct snapshots, which is worse than the coverage it buys. Note the constraint at the allocation so the next reader does not try it. The real gap — that this rec does not support the 128MB mapping at all — stands, and needs the LUT, the mask and the manual-page arrays converted together. Comment only, no behaviour change. recompiler_tests 1439/1439. |
||
|
|
2286e47027 |
EE: assert no block survives a recClear of its own range
recClear's walk is supposed to leave nothing behind that overlaps the range it just cleared. Upstream x86 checks exactly that and calls a violation "Impossible block clearing failure" (iR5900.cpp); our port dropped the check when it was written. It is worth having: the straddler-from-below that |
||
|
|
47614daa67 |
IOP: scan past non-overlapping neighbours when hunting straddlers
psxRecClearMem walks down through recBlocks from the block containing the written word to pick up straddlers — blocks that start below it but whose bodies reach into the range being cleared. recBlocks is ordered by startpc, not by end address, so stopping at the first block that ends before the write is unsound: a longer block lower down can jump clean over a short one and still cover the write, and the scan quits before it is ever examined. The straddler is then neither removed from recBlocks nor LUT-reset, so it keeps executing stale code. This is the IOP half of the same blind spot |
||
|
|
4c70e6ecd3 |
tests: establish shared state instead of inheriting it
Three EE tests asserted on process-global emulator state they never set up. They passed in declaration order because their neighbours happened to leave that state alone, and failed under --gtest_shuffle. Same class as the overlap tests in |
||
|
|
cf403dbd8a |
tests: make the EE overlap-walk tests self-sufficient
OverlappingCompileClearsStaleBlock and OverlapWalkIgnoresUnmodifiedNeighbors
passed only in full-suite order; run alone, both failed with the JIT taking a
different path through the routine than the interpreter.
Not a JIT bug. Both drove the routine through a caller holding a direct JAL,
and re-pointed it between Run()s by calling LoadProgram again with a new
target. Nothing invalidates that caller: the harness wires no page-protection
SIGSEGV, so on a PreserveCache Run a rewritten program is re-compiled only if
the page has reached ProtMode_Manual and its blocks carry the inline entry-time
SMC check. Promotion needs manual_counter to build up over repeated clears
(memory_protect_recompiled_code), which only happens once some earlier test has
hammered the program page — every preceding test that runs the JIT was priming
it. Alone, the stale caller ran and called its old target.
Call indirectly instead: JALR through t1, seeded per Run, so the caller block
is compiled once and never needs invalidating, and the entry point varies by
register rather than by rewriting code. This is the pattern
MidCompileOverlapClearKeepsLutAndLinkerCoherent already uses and documents. The
per-step sentinel goes away with it — one fixed value still separates "the
routine's ADDIU ran" from "we entered past it".
OverlapWalkIgnoresUnmodifiedNeighbors also now asserts what its name claims.
It only checked v0, which its own comment conceded proves nothing: a cleared
and recompiled head block returns 9 exactly like an untouched one. It now
captures the head block's fnptr before the overlapping compile and requires it
to survive unchanged — blocks are bump-allocated and never reused before a full
reset, so an unchanged fnptr means that compile is still live. Verified
non-vacuous by forcing the walk's memcmp to always mismatch: the new assertion
fires, and it is the one that fires.
All 8 EeRecSmc tests now pass individually as well as in-suite;
recompiler_tests 1438/1438.
Unrelated and pre-existing: EeRecCarbonSelfLoop.PinnedValueLoopCarriedBaseByteFill
and two EeRecLoadStore tests fail under --gtest_shuffle. Reproduced identically
at
|
||
|
|
3ac41e05bd |
EE: scan past non-overlapping neighbours when hunting stale blocks
Both the stale-overlap walk at the tail of recRecompile and recClear
descend recBlocks and stop at the first block that ends before the range
they care about. recBlocks is ordered by startpc, not by end address, so
that stop is unsound: a long block at a low address can jump clean over a
short one lying between it and the range, and the scan quits before the
long one is ever examined.
The walk then misses a genuinely stale block, and recClear leaves it in
the cache — the straddler-from-below it is specifically written to remove.
Reachable whenever a block goes stale AFTER its neighbours compiled, which
is exactly the ordering the walk exists for: an earlier compile's own walk
would otherwise have cleared it.
Scan past a non-overlapping neighbour instead of stopping on it, bounded
by s_maxBlockBytes — the longest guest extent compiled since the last
reset, so the scan still terminates as soon as nothing below could reach.
recClear splits its pending remove range around a skipped survivor exactly
as it already does for s_pCurBlock.
recClear also drops its lowerextent floor clamp. A skipped survivor can
sit inside a removed straddler's extent, and raising the floor to that
survivor's end would leave the straddler's own start word pointing at its
removed stub — the fnptr assert StraddlerBlockRecClearResetsStartFnptr
pins. Resetting a survivor's LUT entry instead costs it one recompile.
This was inert until
|
||
|
|
450d5e5e28 |
mVU: pack MAC/status flags with one ADDV instead of two movemask chains
mVUupdateFlags was a 1:1 port of x86's double-MOVMSKPS extraction: two NEON
AND/ADDV/UMOV chains glued together with GPR AND/SHL/AND/OR, plus a per-site
weight vector emitted through vixl's literal pool. That is 16 instructions of
flag packing around the 2 instructions of real FMAC work, and it measured as
~29% of all VU1 code time in God of War II - the first VU-bound title we have
profiled. One VU1 program carried 1666 literal-pool slots holding 34 distinct
values, 12.8% of its host code, each on its own cache line.
SLI collapses it. CMLT and FCMEQ both yield all-ones-or-zero lanes, so
sli vZero.4s, vSign.4s, #4
leaves sign in bits [31:4] and zero in [3:0] of one register, and a single AND
against a combined per-lane weight selects lane i's sign into bit (i+4) and its
zero into bit i. The per-lane bit sets are disjoint, so ADDV's sum is an OR.
Seven instructions replace sixteen, in the two temporaries the caller already
had - the weight vector may reuse the sign register, which is dead by then.
Because the weight vector is picked at emit time it also absorbs AND_XYZW and
SHIFT_XYZW, which stop existing as instructions, and the vectors move out of
the literal pool into mVUglob.macWeights so they ride the pinned gprMVUglob
base as a single Ldr and share a few D-cache lines. The status merge folds the
same way: x86's `AND mReg, 0xFF` is provably a no-op off the overflow path, and
the non-sticky copy becomes an ORR shifted operand.
Same rewrite for the COP2 macro path in cop2EmitFlagUpdate, which loads its
weights as a literal - that path deliberately leaves gprMVUglob unpinned, since
x25 is the EE recompiler's RECCYCLE inside an EE block. mVUupdateFlags now
asserts it is micro-mode only. The parked-result copy in q28 goes away too: the
pack only touches q29/q31, so a result in RQSCRATCH survives untouched.
Measured with pcsx2-vurunner on M2 over 323 VU programs: host instructions
-22.8%, cycles -20.0% (median per-program -17.1%, best -34.3%, no program
regressed by more than 0.5%). Emitted host bytes -19.6% to -40.3% across
Burnout 3, R&C UYA, Shadow of the Colossus and Katamari - the I-cache side
should matter more on the SD865's 64 KB L1I than it does here. All 5818 corpus
captures replay bit-identically against the interpreter, before and after.
vu_mac_flag_pack_tests pins the weight table against the interpreter across the
three shapes that select different table rows (full mask, partial mask, and the
single-scalar rotate that is the only folded non-zero shift).
kMvuCompilerAbiVersion 15 -> 16: every flag-writing FMAC changes shape, and the
emitted [x25, #imm] weight offsets only exist in an mVUglob layout carrying
macWeights, so on-disk program caches must evict.
|
||
|
|
f47e207912 |
GS/HW: skip the PS2 Z floor where it is provably an identity
God of War II's Athena statue speckles with bright blue pinpoints on Apple GPUs. The statue is drawn as paired passes over identical geometry: an env-map layer that writes depth, then a stone layer that re-tests the same triangles with GEQUAL and no depth write. The second pass only lands because its depth matches what the first pass stored. zfloor is set on the depth-writing pass, so that pass emits gl_FragDepth = floor(z * 2^32) * 2^-32 while the read-only pass tests against fixed-function interpolated depth. But a float32 ULP at depth z is 2^(exp(z) - 23) and a PS2 Z unit is 2^-32, so from z >= 2^-9 -- integer Z >= 2^23 -- the two are the same size and the floor cannot change any value the draw produces. The statue sits at z ~ 0.00196, inside that range. So the floor does nothing arithmetically there; all it does is move the depth write onto the gl_FragDepth path. The two paths need not agree bit-for-bit, and where they don't the GEQUAL retest drops out and the env-map layer's saturated blue survives as a pinpoint. Skipping a provably-identity floor keeps the arithmetic identical, and removing gl_FragDepth restores early-ZS for that class of draw -- what the no_ps2_z_quantization escape hatch was reaching for, without giving up the floor's real job on ZTST_GREATER. Measured with gsrunner over three GoW2 dumps, software renderer as oracle. Sparkles in the statue box: 220 -> 1 (the remainder is present in the clean control too). Whole-frame divergence from SW across all four frames of each dump: 2937 -> 698, 1624 -> 897, 3046 -> 874 -- every dump moves closer to the reference. Blending is uninvolved: -accblend 0 and -accblend 5 both leave the count at 220, as do -no-fb-fetch and -no-tex-barriers. Reproduces on M2 under Asahi via Vulkan, so this is Apple GPU silicon rather than the Metal backend; Snapdragon 865 is unaffected. In-game fix confirmed. |
||
|
|
5da1d59568 |
gsrunner: capture GS dump frames with RenderDoc
Add -renderdoc <path> and -renderdoc-frame N[,C], writing one .rdc per selected dump frame. RenderDoc's own triggers cannot reach gsrunner on a Wayland session. It polls only X11/xcb for the capture key, so PlatformHasKeyInput() is false and F12 is never seen. Target control cannot drive it either: RenderDoc hooks only the core EGL entry points, while GLContextEGL prefers eglGetPlatformDisplayEXT and eglCreatePlatformWindowSurfaceEXT, so no native window is ever registered for the surface and there is nothing to attach a capture to. The in-application API sidesteps both -- StartFrameCapture(nullptr, nullptr) captures the active device whatever the windowing system, and needs no keypress, so a headless dump replay can capture unattended. Captures open and close at present boundaries in Host::BeginPresentFrame(), which runs on the GS thread with the frame's work submitted but not yet presented. RenderDoc must already be in the process, since it installs its hooks from its library constructor. A late dlopen hands back a working API whose hooks were never installed and then captures nothing, so that case is refused with the LD_PRELOAD command to use instead. Verified on aarch64/Asahi against a God of War II dump. Vulkan surfaceless produces a valid capture: 351 actions, 218 draws, 99 textures, render target carrying real image data. Windowed Vulkan cannot work under RenderDoc's layer at all, which does not advertise VK_KHR_wayland_surface. GL captures record correct events and texture contents but replay with black render targets on Honeykrisp. 3rdparty/include/renderdoc_app.h is RenderDoc's MIT-licensed in-application API header, vendored verbatim. |
||
|
|
f4debc54c1 |
EE: index recRAMCopy by guest address, not startpc/4
The stale-overlap walk at the tail of recRecompile snapshots each
compiled block's guest bytes into recRAMCopy, then memcmps older
overlapping blocks against their own snapshots to catch code that went
stale through a write no protection path caught. recRAMCopy is a byte
array covering main RAM 1:1, but both the compare and the snapshot
indexed it at `startpc / 4`, packing every block's snapshot 4:1 into the
low quarter of the buffer.
Two blocks whose guest starts differ by N bytes then land only N/4 bytes
apart in the snapshot, so overlapping blocks scribble over each other's
snapshots and the compare can never match: each compile recClears the
other and both recompile forever.
Final Fantasy X (SLUS-20312) has such a pair at 0x002B9F48 / 0x002B9F5C.
Measured over 300 frames from an in-game savestate, EE thread:
before 56,546,855,007 insns 14,591,559,685 cycles 6 cache resets
after 5,885,593,412 insns 1,368,467,226 cycles 0 cache resets
9.6x fewer instructions, 10.7x fewer cycles. The EE thread had been
spending ~95% of itself inside the recompiler (vixl, register allocation,
vtlb_AddLoadStoreInfo, icache flushes) and only ~5% executing JIT code,
burning 51 MB of the 58 MB EE code cache every ~48 frames and taking a
full cache reset behind it.
The /4 was correct in 2009, when recRAMCopy was u32*. It became wrong in
|
||
|
|
540c5ae0c5 |
eerunner: --gsdump headless GS-dump capture
Records the GIF command stream from a --liverun into a .gs dump, so a GS workload can be replayed in pcsx2-gsrunner with no emulator in front of it. This is the only honest way to A/B the GS across two builds. Measuring the GS inside a live run does not work: both the MTGS ring and the software rasterizer's job queue spin while waiting for work, so a build with a faster EE shifts the GS threads' instruction counts without changing a single pixel of GS work. Measured on Xenosaga with byte-identical GS workloads (same PRIM, DRW and Mpps), a live run reported the GS threads executing 23% more instructions while burning 21% fewer cycles -- the signature of spin, not rasterization. Replaying the same captured dump standalone put the same build 11% ahead on instructions. Recording arms after --gsdump-at frames (default 30, so the scene has settled) and stops on its own once the requested frame count has been recorded; the frame budget is raised automatically if it would truncate the dump. |
||
|
|
fd1fa89e10 |
IOP: route JIT RAM stores through out-of-line fast-path stubs
After the removal of the per-store block search, the remaining per-store cost was the C call chain itself: iopMemWrite* region dispatch + WLUT load, the indirect psxCpu->Clear call, and the recClearIOP/psxRecClearMem frames -- ~2.9% of battle EE-thread samples in pstef's profile (iopMemWrite32 1.13%, iopMemWrite16 0.28%, recClearIOP 1.44%). Emit three per-width stubs (_DynGen_StoreStub) alongside the dispatchers instead: gate, IsC check, direct store into Main[addr & (ExposedIopRam-1)], and an inline g_iopCodeCov granule probe that only calls into the clear machinery (iopStoreClearHit -> recClearIOP) when a live block overlaps the store. Store sites emit a single BL, exactly the size of the old C call. Hw/unmapped targets tail-jump from the stub to iopMemWrite*, which returns straight to the store site; compile-time-known hw addresses keep calling C directly. The condition is (addr & 0x1f800000) == 0, not a bit-28 test: iopMemWrite* masks addresses to 0x1fffffff, and within that space pages 0x00-0x7f are the only WLUT-mapped write target reachable this way (plain RAM; the parallel port at 0x1f00 is the other mapped page and the mask excludes it). Everything else -- 0x1f80/0x1f40 hw, SIF, DEV9, SPU2, ROM, and the unmapped 0x00800000-0x0fffffff range that bit 28 alone misclassifies as RAM -- must take the C path. Bits 23-28 survive the phys mask, so the test is exact for every KUSEG/KSEG0/KSEG1 mirror, and matches iopMemReset's `for (i < 0x0080)` mapping loop. The RAM mask equals the recLUT_SetPage mirror collapse, so the masked offset doubles as the coverage probe index. IsC (Status bit 16) swallows the store with no clear, matching the C RAM branch's p != NULL && !IsC guard. Stubs are re-emitted on every recResetIOP, so the baked mask and RAM base track extra-memory-mode flips (which discard all blocks). RPSXSTATE is x21, callee-saved, so it survives both the BL and the slow-path tail jump. Outlining is deliberate: pstef measured a fully inline fast path (~15 insns/site) first, and its per-site code growth cost +10% L1I misses/frame, eating the win (3D net zero, FMV only -1.08% cycles). The shared stubs keep sites baseline-sized and also displace the C functions' own icache footprint. Documented at the stub, not fixed here: the inline probe is mirror-collapsed (addr & (ExposedIopRam-1)) while iopCovAdjust and psxRecClearMem key the same array by HWADDR, which strips the KSEG base but not the RAM mirrors. Under a 2MB configuration a block executed from a mirror page registers coverage at a different granule than a store to its physical alias probes. That is exactly the pre-existing blindness of keying recBlocks by HWADDR -- not a regression -- but the two domains are a trap for anyone making either side finer-grained. Six new IopSmc cases: KSEG1-mirror, const-address, SB, and SH stores over a pre-compiled victim block (compiled first, then stored into via RunResume, so a stale-cache leak is actually observable), unmapped-low-region drop, and IsC-swallow. Verified not inert: a bit-28-only gate turns JitStoreToUnmappedLowRegionIsDropped red, and defeating the IsC check turns six cases red including JitStoreWithCacheIsolatedIsSwallowed. Full suite 1432/1432. Taken from https://github.com/yaps2/yaps2/pull/11 by pstef (https://github.com/pstef). Co-authored-by: pstef <3462925+pstef@users.noreply.github.com> |
||
|
|
b7e360b3c7 |
IOP: gate psxRecClearMem with a 256B block-coverage count array
Every qualifying IOP RAM store funnels through psxRecClearMem for SMC
detection, and in practice ~100% of them hit addresses no compiled block
covers -- yet each one paid a binary search over recBlocks to learn that.
Replace the unconditional search with s_iopCodeCov[]: u16 counts of live
blocks whose span overlaps each 256-byte granule of the HWADDR window. A zero
counter proves no block overlaps the written word, so psxRecClearMem returns a
skip to the next granule boundary in O(1); range clears (SIF1/CDVD/SPU2 DMA)
step per granule instead of per word.
Counting the full span rather than just block heads is what keeps mid-block
stores correct, and counts (not bits) stay exact when overlapping blocks are
removed independently. Counters are maintained at the three block-lifecycle
points -- size-finalize (+1), recompile of an existing entry (-1 old span),
psxRecClearMem removal (-1) -- and zeroed on recResetIOP. ROM-resident blocks
are never counted; stores cannot target ROM.
The error direction is the safe one: an over-count only costs a search that
early-outs, and only an under-count could miss a clear.
Two deltas from pstef's version, both from our tree having diverged:
- The recompile-reuse hook attaches differently. His iopRecRecompile splits
Get()/New() and hangs the retirement off the else arm; ours calls New()
unconditionally because ours re-binds an existing entry in place (the
block-link owner-tracking rework,
|
||
|
|
8e0b6e04b4 |
arm64: limit the const-MMIO shortcut's FLUSH_PC to unmapped handlers
The const-paddr MMIO shortcut flushed cpuRegs.pc on every access. Only the two unmapped-page handlers can raise a guest exception -- vtlb_Miss -> cpuTlbMiss -> cpuException derives EPC from that pc -- so for every registered hardware handler the per-access mov/movk/str was dead weight, paid once per access in an MMIO burst. x86 never flushes pc at its MMIO sites at all. Add vtlb_IsUnmappedHandlerID() and flush pc only for those two handlers, plus the forceEventTest path: EE-counter reads end the block with the g_branch=2 event-test exit, and FLUSH_EVERYTHING (0x1ff) excludes FLUSH_PC (0x200), so this seam's store is the only thing that gives DispatcherReg a resume pc. ConstCounterReadEventTestExitResumesAtNextPc pins that carve-out; EeRecTraps.LoadTlbMissInDelaySlotSetsCauseBdAndBranchEpc pins the retained unmapped-handler flush. The decision reads handler identity at compile time to settle a runtime property, but introduces no new assumption: the shortcut already resolves its callee from the same compile-time mapping and hard-codes a direct BL to it, so a mapping change that could falsify the pc decision would already be sending the call to the wrong handler. Noted at the site, since that equivalence is the whole basis for the trim being safe. Full suite 1423/1423. Taken from https://github.com/yaps2/yaps2/pull/10 by pstef (https://github.com/pstef). Co-authored-by: pstef <3462925+pstef@users.noreply.github.com> |
||
|
|
b772d73abf |
tests/ee: pin LUT/BASEBLOCKEX coherence after a mid-compile overlap clear
The GE-18 stale-overlap walk fires recClear mid-compile. The walk spares the in-progress block's BASEBLOCKEX, but the post-walk ClearRecLUT tail runs over the removed straddler's full extent, which spans the in-progress startpc when the straddler comes from below. If that wipes BLOCK(startpc)->fnptr with nothing to restore it, the next dispatch recompiles into the surviving entry and the linker's target diverges from the dispatcher's. Add recEeBlockHostInfo() so a test can read both routes -- BASEBLOCKEX fnptr plus host size, and the LUT dispatch target -- and assert they agree, then pin the scenario: compile a straddler, poke its first word so its recRAMCopy snapshot goes stale, compile a victim entered four bytes inside it, and check after both that compile and a re-dispatch that the routes match, the victim was not recompiled, and x86size did not grow. This passes on the current tree. Our recRecompile publishes the LUT entry at the start of the compile rather than at the end as x86 does (x86/ix86-32/iR5900.cpp:2704), which is what made the wipe unrecoverable on the tree this fixture came from; here recClear's `ceiling` clamp keeps the tail off the in-progress entry. The fixture is a pin on that property, not a bug repro -- so it carries an explicit inertness guard asserting the straddler really was removed. Without it, a future change that stops the overlap walk from firing would leave the whole test green and meaningless. Taken from https://github.com/yaps2/yaps2/pull/10 by pstef (https://github.com/pstef); the inertness guard is ours. Co-authored-by: pstef <3462925+pstef@users.noreply.github.com> |
||
|
|
8742617fe4 |
arm64: keep EE const marks across vtlb C-call seams
The vtlb load/store seams flushed const-tracked GPRs with delete_const=true, so the first vtlb seam in a block destroyed every const mark. An MMIO burst -- N accesses off one lui/ori const address -- then degraded accesses 2..N from the const-paddr shortcut (a direct BL to the registered handler) to the dynamic fastmem/softmem path. The DBZ3 GS-priv block at 0x0029474c (5x sd to 0x120000xx) emitted one BL plus four dynamic stores where x86 emits five BLs. Add FLUSH_CONST_KEEP as a modifier on FLUSH_CONSTANT_REGS: write dirty const values back to memory, but keep the tracking marks. FLUSH_VTLB pairs the two and replaces FLUSH_CONSTANT_REGS at every vtlb seam. The marks are valid to keep because the callee set at these seams -- the registered MMIO handlers and vtlb_memRead/Write -- cannot write guest GPRs. x86 relies on the same invariant more aggressively: its equivalent sites use FLUSH_FULLVTLB (0x000) and flush nothing at all, values included. We keep the value writeback because the softmem paths re-read guest GPRs from cpuRegs memory post-flush and vtlb exception paths expect memory to be current; that writeback is what the Tier-2G bisect (f4601964b) found load-bearing, and it is preserved here. Interpreter seams must NOT set the modifier -- the interpreter does write cpuRegs.GPR, so its marks go stale. This makes several previously-dead paths live for the first time, since recLQ/recSQ/recLQC2/recSQC2 and the post-flush GPR_IS_CONST1 folds in recLoad/recStore never saw a live const mark before. _allocGPRtoNEONreg already handles both directions (MODE_READ materializes a const as a 128-bit memory load plus an Ins of the constant into lane 0; MODE_WRITE deletes the mark), which the new ConstQuadStoreMaterializesAndAliasLoadKillsConst fixture pins along with the rs==rt alias case. Note the const-MMIO shortcut sites move from FLUSH_INTERPRETER to FLUSH_VTLB|FLUSH_PC, which drops FLUSH_FREE_XMM and so lets the GE-15 retention keep FPREG/FPACC slots mapped in q10-q15 across the handler call. That is safe here -- MMIO handlers do not RMW cpuRegs.fpr -- and makes the shortcut consistent with the softmem seams. Dropping FLUSH_ALL_X86 changes nothing: iFlushCall frees all caller-saved GPRs unconditionally. FLUSH_FREE_VU0 is inert on arm64; only x86's iFlushCall consumes it. ConstAddrMmioBurstStaysOnShortcut is red before this change (accesses 2..N land in the faulting-PC set) and green after. Full suite 1423/1423. Taken from https://github.com/yaps2/yaps2/pull/10 by pstef (https://github.com/pstef). Co-authored-by: pstef <3462925+pstef@users.noreply.github.com> |
||
|
|
aead47e805 |
tests/ee: pin resident self-loop byte-copy semantics
Two fixtures for the DBZ3 byte-copy loop at EE pc 0x002a9aa8: lbu/sb through separate walking pointers with the counter compared against a non-zero bound held in a register, so the loop exits on the sign-extended ADDIU wrap rather than on zero. A stepdiff run originally flagged this block (JIT a2=-1 vs interp a2=0x10); that report turned out to be a sampling artifact of SL-1 residency -- one block-entry sample per resident run against interp's per-op stream -- not a divergence. These pin the semantics the artifact was mistaken for: trip count, copied bytes, and final pointer/counter state. The overlapping variant (dst-src smaller than the length) pins the strict per-iteration memory order, so any load hoisting or store deferral across resident back-edge iterations changes the result and fails. Both pass on the current tree; they are regression pins, not bug repros. Taken from https://github.com/yaps2/yaps2/pull/10 by pstef (https://github.com/pstef). Co-authored-by: pstef <3462925+pstef@users.noreply.github.com> |
||
|
|
9fa68bdfce |
eerunner: --rec-fallback opcode-group interpreter bisect switch
EE JIT divergence hunts start at "this game miscomputes something under the EE recompiler" with no idea which emitter is at fault, and the existing funnel (stepdiff/contmem) localizes to a frame and a RAM region — which chaotic float amplification makes almost unreadable by end of frame. Add a harness-only switch that routes whole EE opcode groups through recCall(interpret) instead of their native emitter, everything else still JIT. The interpreters are the known-good baseline, so the group that makes the symptom disappear contains the bug, and each probe is one run instead of a rebuild per hypothesis. Groups: fpu, cop2, mmi, multdiv, shift, arith, loadstore, move, cop0, branch, plus all/none. cop2 narrows into cop2move / cop2vu / cop2ls and then into qmfc2 / cfc2 / qmtc2 / ctc2, and those four accept a `:<reg>` destination filter (`ctc2:0`) since their fs field names 32 registers with wildly different semantics. Gated on PCSX2_RECOMPILER_TESTS so it never reaches a shipping build; the non-hooks path compiles to `constexpr bool forcedInterp = false`. A forced fallback also feeds delaySlotNeedsBranchBracket, matching the conservatism already applied to genuine !opcode.recompile fallbacks, so the switch cannot itself perturb delay-slot exception semantics. Pairs with `--mkstate --renderer sw`, whose savestate carries a rendered screenshot, to give a headless visual oracle for graphical bugs. Together these took the Xenosaga zoom bug from "somewhere in the EE JIT" to the exact instruction (CTC2 to vi00) in about ten runs. |
||
|
|
74ef511d31 |
arm64: emit the VU0 interlock for CTC2 to vi00
vi00 is hardwired to zero, so `CTC2 rt, $vi00` writes nothing — which makes it a free VU0 barrier, and games use it as exactly that. recCOP2_CTC2 returned on `fs == 0` before reaching cop2EmitConditionalSync, so the barrier was compiled to nothing at all: the EE then ran ahead of an in-flight micro program and consumed half-computed VU0 results. Xenosaga Episode I (SLUS-20469) brackets its transform setup with interlocked CTC2-to-vi00 and rendered massively zoomed in under the EE JIT while EE-interp and the pre-transplant JIT were both correct. Both references emit the sync ahead of any destination-based early return: x86 recCTC2 calls COP2_Interlock(1) before its `if (!_Rd_) return`, and the interpreter runs vu0Sync() + _vu0WaitMicro() before its `_Fs_ == 0` check. Emit the interlock for vi00 (write side stays a no-op), and move the MAC/TPC/VPU_STAT read-only returns after the sync for the same reason. Regression test drives a pending micro program across the barrier and reads the result with a following CFC2 — the COP2 analysis pass marks only the first COP2 op after a kick as the sync point, so that CFC2 emits no sync of its own and reads whatever the barrier left behind. Red before, green after. |
||
|
|
821257346c |
FullscreenUI: controller-navigable first-time setup in Big Picture
The Qt SetupWizardDialog is mouse/keyboard-only and ran before Big Picture
started, so launching -bigpicture (or StartBigPictureMode) on a fresh datapath
hit an un-navigable modal. There is no gamepad->Qt bridge; controller input
only drives ImGui/FullscreenUI.
Skip the Qt wizard when starting into Big Picture and surface a
controller-navigable setup flow in FullscreenUI instead. A new
MainWindowType::Setup walks Welcome -> BIOS -> Game Directories ->
RetroAchievements -> Finished, reusing the existing BIOS and Achievements
settings pages and an extracted DrawSearchDirectoriesList() (factored out of
DrawGameListSettingsWindow). Welcome offers Set Up vs Skip; completion clears
UI/SetupWizardIncomplete and lands on the game list when directories exist.
Desktop first-run keeps the Qt wizard.
Two FullscreenUI correctness details: give each step its own window ID
(setup_content_{step}) so ImGui does not carry a stale NavId across steps and
leave a button unselectable by controller; and scope the settings lock to the
steps that draw settings pages, since the Welcome/Finished buttons call
CompleteSetupWizard() which re-acquires the non-recursive settings lock.
|
||
|
|
97eac53fa8 |
arm64: use the live x86 SetMaxValue constant in DOUBLE-mode FPU
x86 iFPUd.cpp SetMaxValue() branches on FPU_RESULT, which is #defined to 1, so the only arm ever emitted is `xOR.PS(regd, s_const.pos[0])` with pos[0] == 0x7fffffff. The arm64 DOUBLE-mode port transcribed the dead else-arm instead and used 0x7f7fffff (+FLT_MAX, the single-precision constant) at both SetMaxValue sites: SetMaxValueS, feeding RSQRT, and the divide-by-zero result inside recDIVhelper1, feeding DIV. ToPS2FPU_Full's overflow clamp in the same file already used 0x7fffffff, so the file was internally inconsistent. The two constants differ only in the exponent field, 0xff vs 0xfe. The EE has no NaN/Inf encodings, so both are ordinary large finite floats there, but guest softfloat routines do classify exp==0xff separately, which makes the one-band difference game-visible. NFS Carbon (SLUS-21493, eeClampMode:3) deadlocks on it. A disabled sine-wobble axis leaves both table parameters +0.0, so the game divides 0.0/0.0 every time it builds the table and relies on the exp==0xff result converting to <= 0 so the following blez skips the build. With 0x7f7fffff the game's float->double->int helper saturates to INT_MAX instead, and the game allocates and fills a 2^31-entry table, wiping guest RAM until a NULL vtable dispatch lands the EE at PC 0 and the kernel halts; the SIF0 deadlock that surfaces is the aftermath, not the cause. Two existing tests asserted the emitted (wrong) value; they are corrected to the x86 result, and the 0.0/0.0 shape is added as a regression test. |
||
|
|
b8e7ad46ad |
eerunner: fix stepdiff self-loop skip and add entry-state probe
The self-loop phase-skip only consulted BlockBackwardBranchTarget on ar.pc (the fall-through), so a self-loop whose divergence is observed at its exit was never skipped and stopped the walk as a false positive (this is what pinned Carbon's 0x174c30 byte-fill loop). Also check ar.prev_pc, the offending self-loop itself. The zoom report now dumps both streams' GPRs and FPRs at the offending block head, so identical-entry false positives are visible directly in the report instead of needing a hand-built harness. |
||
|
|
365f69c641 |
tests: EE self-loop byte-fill store fixtures (Carbon 0x174c30 shape)
Six fixtures replicating the SB/ADDIU/BNE self-loop at 0x174c30 in NFS Carbon (SLUS-21493), including the exact game entry state. All pass — pinning that the block compiles correctly and that the stepdiff report naming it was a sampling artifact of the tool, not a JIT bug. |
||
|
|
9eb93d4dcd |
arm64: divert stale block-link sites to the dispatcher, not JITCompile
Remove() stamped a removed block's entry with B JITCompile, and Link() routed not-yet-compiled targets there too. JITCompile unconditionally recompiles from cpuRegs.pc — so when a block was removed and later recompiled at a new address, any orphaned link site still branching to the old entry re-entered recRecompile on an already-compiled block: fnptr assert on Devel, and on Release a spurious in-place recompile that superseded live code and stranded other orphaned callers in the stale pre-SMC compile (the NFS Carbon SLUS-21493 post-FMV deadlock, localized by twindiff against the pre-transplant build). Every link tail in both recs already stores the target guest pc before its branch, so the correct stale-site policy is re-DISPATCH, never re-COMPILE: divert to DispatcherReg, which routes through the recLUT (the single SMC-invalidation rewrite point) to the current code, or to compilation via the LUT's own JITCompile slot when genuinely uncompiled. Both AetherSX2's Remove() and the pre-transplant linker enforce this same fallback-to-dispatcher policy; the recRecompile fnptr assert becomes a true invariant again. Zero hot-path cost: the dispatcher hop is paid only by a cold first execution of an unresolved link (repatched direct by New()) or by a stale site after SMC churn. Remove() keeps its signal-safety contract (single atomic 4-byte B patch, no link-map access). recompiler_tests: 1415/1415. Carbon twincompare free-runs 200 frames where it previously aborted on the recRecompile assert. |
||
|
|
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. |
||
|
|
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. |
||
|
|
0b4d0bd03d |
sdl: fall back to a Wayland Vulkan surface when a compositor is present
The SDL frontend previously only ever reported WindowInfo::Type::VulkanDirect and relied on VK_KHR_display to acquire the display. That fails when the frontend is launched inside a Wayland session, where a compositor owns the display (desktop, phone, or an embedded gamescope-style compositor). BuildWindowInfo() now checks WAYLAND_DISPLAY: when set, it brings up an SDL3 SDL_WINDOW_VULKAN window on the wayland video driver and hands its wl_display / wl_surface to the Vulkan backend as a Type::Wayland surface (the backend's vkCreateWaylandSurfaceKHR path already existed). Any failure logs a warning and falls through to the existing VK_KHR_display path, so bare kmsdrm devices are unaffected. The whole block is gated on WAYLAND_API. Enable WAYLAND_API in the clang-handheld preset so this path compiles. Pure kmsdrm targets without the ECM / Wayland-Egl headers can still build VulkanDirect-only via -DWAYLAND_API=OFF. |
||
|
|
7bfe6a44a1 |
gamedb-overlay: let SLPM-74405 inherit InstantDMAHack from bin
The mobile GameDB base predated upstream PCSX2's addition of InstantDMAHack
("Stops hang when entering Sword Select screen") to Samurai: Complete Edition
and never resynced, so the Android/iOS copies carried EETimingHack alone. The
generated overlay faithfully reproduced that stale state, which the override
loader's clear-then-replace on gameFixes would re-inflict on mobile/ARM64-Linux.
The drop was never intentional (no commit in the Android line ever touched this
entry's fixes; the fix is an arch-neutral DMA-timing hang fix). Remove the
overlay entry so the mobile tiers inherit bin's full [EETimingHack,
InstantDMAHack].
|
||
|
|
ea351894e8 |
ios: bundle resources from bin and converge on the runtime overlay
Point the iOS Xcode/CMake resource bundling at the canonical bin/resources tree instead of a committed iOS copy, mirroring the desktop and Android builds. The GLOBs now embed bin/resources fonts/shaders/sounds and the top-level GameDB / Redump / controller data into the app bundle's Resources/, excluding the dx11 shaders iOS never compiles (it renders with Metal). iOS also drops its bespoke build-time GameDB merge (tools/gamedb-convert.py plus its GameIndex[original]/[override].yaml inputs) and instead bundles the canonical mobile overlay bin/resources-overlay/armsx2_overrides.yaml, which the shared GameDatabase override loader applies at runtime from EmuFolders::Resources — the same path every other platform now uses. This converges iOS onto Android's mobile GS/JIT tuning (its committed GameIndex was stale — older Tekken clamp, missing the Ziemas and Crazy Frog fixes) and onto one override code path. Delete the 41 committed iOS resource duplicates; patches.zip stays as the only iOS-only extra. Needs a macOS/Xcode build to confirm the bundled Resources/. |