mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
EE/arm64: make recSafeExitExecution safe to call cross-thread
The Android pause/stop JNI calls Cpu->ExitExecution() from the UI thread
against a running EE. recSafeExitExecution carried two accelerants inherited
from the x86 recompiler alongside its exit flag: it zeroed
cpuRegs.nextEventCycle when the EE was outside the event test, and folded
psxRegs.iopCycleEE into iopBreak when inside it.
Both are data races from a foreign thread, and the first one is how God of
War II savestates got their poisoned timers. The arm64 JIT pins the cycle
counter as a delta (RECCYCLE = cycle - nextEventCycle) for the whole life of
a block chain, reconstructing the absolute clock as delta + nextEventCycle
at C-call seams. A cross-thread zero landing mid-chain makes the next flush
reconstruct cycle = delta + 0, warping the EE clock back to near VM birth —
observed as a 142-billion-cycle rollback in a live repro. Counter baselines
are then "ahead" of the clock, which the old u32 rcntSyncCounter arithmetic
turned into the +2^32 startCycle scar and blown count that rode along in
every savestate taken afterwards (see the Counters fix in 4e34e65b84).
Desktop hosts only call ExitExecution on the CPU thread, which is why the
poisoning was Android-only. Reproduced on Linux with eerunner's new
EERUNNER_EXITSTORM knob: cross-thread ExitExecution every ~2-3 ms poisons
timer 0 within 1500 frames of God of War II on the unfixed code, and a
denser storm over 3000 frames stays clean on this fix.
The accelerants never bought arm64 anything even on the CPU thread: x86
block tails compare the clock against nextEventCycle in memory, so zeroing
it forced the very next tail into the event test — arm64 tails test the
pinned delta's sign and never reread memory mid-chain. The flag alone is the
mechanism, consumed at most one scheduler horizon (~an hblank) later.
This commit is contained in:
@@ -3148,20 +3148,27 @@ static void recExitExecution()
|
||||
|
||||
static void recSafeExitExecution()
|
||||
{
|
||||
// Callable from ANY thread: the Android pause/stop JNI fires this cross-thread
|
||||
// against a running EE (desktop hosts only call it from the CPU thread). The
|
||||
// flag is the whole mechanism — recEventTest consumes it at the next event
|
||||
// test, at most one scheduler horizon (~an hblank of guest time) away.
|
||||
//
|
||||
// The x86-inherited accelerants this used to carry were cross-thread poison:
|
||||
//
|
||||
// - `cpuRegs.nextEventCycle = 0` raced the pinned cycle delta (RECCYCLE =
|
||||
// cycle - nextEventCycle). A hit landing while the EE was mid-chain made
|
||||
// the next armFlushCycleDelta reconstruct cycle = delta + 0, warping the
|
||||
// EE clock back to near VM birth (observed: -142e9 cycles). Counter
|
||||
// baselines were then "ahead" of cycle, and the old u32 rcntSyncCounter
|
||||
// blowup minted the GoW2 poisoned-savestate scar from exactly this seam.
|
||||
// On x86 the store was safe AND useful — block tails compare cycle
|
||||
// against nextEventCycle in MEMORY, so zeroing it forced the very next
|
||||
// tail into the event test. arm64 tails test the pinned delta's sign, so
|
||||
// here it never even accelerated the exit.
|
||||
//
|
||||
// - the iopCycleEE/iopBreak fold raced the IOP timeslice bookkeeping on
|
||||
// the CPU thread. It only ever shaved one IOP slice of exit latency.
|
||||
eeRecExitRequested.store(true, std::memory_order_release);
|
||||
|
||||
if (!eeEventTestIsActive)
|
||||
{
|
||||
cpuRegs.nextEventCycle = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (psxRegs.iopCycleEE > 0)
|
||||
{
|
||||
psxRegs.iopBreak += psxRegs.iopCycleEE;
|
||||
psxRegs.iopCycleEE = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void recCancelInstruction()
|
||||
|
||||
Reference in New Issue
Block a user