Skip to content

Commit a962484

Browse files
committed
iOS: Compile EI/ERET as block terminators and live-cycle COP0 Count/PERF
Port the COP0 block-terminator and live-cycle handling from the mac arm64 backend (upstream 673135b) so EI/ERET and the MFC0/MTC0 Count/PERF polls no longer force an interpreter single-step block. EI (enable interrupts) and ERET (exception return) are now compiled as in-block terminators matching x86 recEI/recERET: run the interpreter handler inline (Interp::EI sets Status.EIE; Interp::ERET writes cpuRegs.pc), then end the block so the tail event-tests and dispatches from cpuRegs.pc. This keeps the preceding straight-line ops in the same block and lets the block's cycles ride forward to the post-loop commit instead of fragmenting the block around every EI/ERET. MFC0/MTC0 of Count(rd9)/PERF(rd25) commit the block's accumulated cycles before an inline interpreter call (so the read sees a live cpuRegs.cycle) rather than single-stepping. Games busy-poll Count for timing and these were ~80% of Jackie Chan Adventures' EE fallbacks; the per-op commit also fixes the stale-value hazard where two Count reads in one block returned the same value. Correctness is inherited from the interpreter handlers; no native COP0 emitter is needed. Both paths are inserted in recRecompile before the recTranslateOpOptimized straight-line fallback.
1 parent 8a70616 commit a962484

1 file changed

Lines changed: 94 additions & 0 deletions

File tree

app/src/main/cpp/pcsx2/arm64/aR5900.cpp

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3058,6 +3058,24 @@ static bool recIsCop0DI(u32 op)
30583058
return (op >> 26) == 0x10 && ((op >> 21) & 0x1f) == 0x10 && (op & 0x3f) == 0x39;
30593059
}
30603060

3061+
// COP0 EI (enable interrupts, funct 0x38) / ERET (exception return, funct 0x18) —
3062+
// CO ops (rs==0x10). x86 compiles both via recBranchCall: end the block, run the
3063+
// interpreter handler, then event-test + dispatch (EI so a now-unmasked pending IRQ
3064+
// is serviced; ERET because it writes cpuRegs.pc). recRecompile handles them that way
3065+
// instead of single-stepping (the top EE interpreter fallback after the BC0/MADD work).
3066+
static bool recIsCop0EI(u32 op)
3067+
{
3068+
return (op >> 26) == 0x10 && ((op >> 21) & 0x1f) == 0x10 && (op & 0x3f) == 0x38;
3069+
}
3070+
static bool recIsCop0ERET(u32 op)
3071+
{
3072+
return (op >> 26) == 0x10 && ((op >> 21) & 0x1f) == 0x10 && (op & 0x3f) == 0x18;
3073+
}
3074+
static bool recIsCop0EIorERET(u32 op)
3075+
{
3076+
return recIsCop0EI(op) || recIsCop0ERET(op);
3077+
}
3078+
30613079
// MIPS trap ops: SPECIAL T{GE,GEU,LT,LTU,EQ,NE} (funct 0x30-0x34,0x36) and REGIMM
30623080
// T{GE,GEU,LT,LTU,EQ,NE}I (rt 0x08-0x0C,0x0E). Emitted natively (block-conditional)
30633081
// in recRecompile — see recEmitTrapCompareIfTrap.
@@ -3532,6 +3550,26 @@ static bool recOpNeedsCycleFlush(u32 op)
35323550
return (op >> 26) == OP_LQC2 || (op >> 26) == OP_SQC2;
35333551
}
35343552

3553+
// True for MFC0/MTC0 of the Count (rd 9) or PERF (rd 25) registers — the COP0 ops the EE
3554+
// rec previously single-stepped (recTranslateOpOptimized returns false for them) because
3555+
// they read a live cpuRegs.cycle this rec only flushes at the block tail. Games busy-poll
3556+
// Count for timing, so the single-step path can dominate EE (Jackie Chan Adventures: ~80%
3557+
// of EE fallbacks). recRecompile handles these by committing the block's accumulated cycles
3558+
// before an INLINE interp call (so the read is live), instead of the expensive single-step.
3559+
// Per-op commit also fixes the historic "two MFC0 Count in one block read the same stale
3560+
// value -> games lock up" hazard: each read now advances cpuRegs.cycle. Excludes BC0 / ERET
3561+
// / EI / DI / WAIT, which still single-step (they write PC or gate interrupts).
3562+
static bool recCop0NeedsLiveCycle(u32 op)
3563+
{
3564+
if ((op >> 26) != 0x10)
3565+
return false; // COP0
3566+
const u32 rs = (op >> 21) & 0x1f;
3567+
if (rs != 0x00 && rs != 0x04)
3568+
return false; // MFC0 / MTC0 only (BC0 rs==0x08 + C0 rs==0x10 keep their paths)
3569+
const u32 rd = (op >> 11) & 0x1f;
3570+
return (rd == 9 || rd == 25); // Count / PERF
3571+
}
3572+
35353573
// CALLMS (COP2 SPECIAL1 funct 0x38) / CALLMSR (0x39) — x86's only INTERPRETATE_COP2_FUNC ops
35363574
// (microVU_Macro.inl:295-296). M5.5 keeps them on the inline interpreter (faithful: the interp
35373575
// path self-finishes VU0 and launches the microprogram via vu0ExecMicro), but unlike the native
@@ -4732,6 +4770,62 @@ static void recRecompile(u32 startpc)
47324770
raw_cycles = 0;
47334771
}
47344772

4773+
// MFC0/MTC0 of Count(rd9)/PERF(rd25): commit the block's cycles (incl. this op) so
4774+
// the read is live, clear the accumulator, then INLINE-interp in-block — instead of
4775+
// the expensive single-step path (these were ~80% of Jackie Chan's EE fallbacks, a
4776+
// Count busy-poll). Same commit-then-inline shape as the CALLMS launch above; the
4777+
// per-op commit makes consecutive Count reads see an advancing cpuRegs.cycle (no
4778+
// stale-value lock-up). See recCop0NeedsLiveCycle.
4779+
if (recCop0NeedsLiveCycle(op))
4780+
{
4781+
raw_cycles += eeOpCycles(op);
4782+
recEmitCommitBlockCycles(raw_cycles);
4783+
raw_cycles = 0;
4784+
// Flush + kill the GPR register cache / const tracking before the inline interp,
4785+
// exactly like the trap path: MTC0 reads cpuRegs.GPR.r[rt] from memory (must be
4786+
// authoritative) and MFC0 WRITES it — a stale cached copy in a callee-saved host
4787+
// reg would survive the C call and shadow the Count value, silently breaking the
4788+
// very poll loop this targets.
4789+
recCacheFlushAll(cache_state);
4790+
recCacheKillAll(cache_state);
4791+
recConstKillAll(const_state);
4792+
recEmitInterpInline(op);
4793+
waitloop_possible = false; // inline live-cycle op — not a wait-loop body
4794+
pc += 4;
4795+
endpc = pc;
4796+
if (++compiled >= MAX_BLOCK_INSTS)
4797+
{
4798+
recEmitWritePc(pc);
4799+
known_dispatch_pc = true;
4800+
dispatch_pc = pc;
4801+
break;
4802+
}
4803+
continue;
4804+
}
4805+
4806+
// COP0 EI / ERET (CO ops, rs==0x10; funct 0x38 / 0x18). Faithful to x86
4807+
// recEI/recERET (recBranchCall): run the interpreter handler in-block, then END
4808+
// the block so the tail event-tests + dispatches from cpuRegs.pc. EI: a now-
4809+
// unmasked pending IRQ gets serviced by that event test (x86 "must branch after
4810+
// enabling interrupts"); ERET: it writes cpuRegs.pc, so the dispatch must read it.
4811+
// Like the handled-branch path, the block's cycles ride in raw_cycles and the
4812+
// post-loop commits them — no early commit, so no spurious tail +1. Replaces the
4813+
// per-op single-step (the top EE interpreter fallback after the BC0/MADD work).
4814+
if (recIsCop0EIorERET(op))
4815+
{
4816+
raw_cycles += eeOpCycles(op);
4817+
recCacheFlushAll(cache_state);
4818+
recCacheKillAll(cache_state);
4819+
recConstKillAll(const_state);
4820+
if (!recIsCop0ERET(op))
4821+
recEmitWritePc(pc + 4); // EI doesn't write PC; resume after it unless the event test diverts
4822+
recEmitInterpInline(op); // Interp::EI sets Status.EIE / Interp::ERET sets cpuRegs.pc
4823+
waitloop_possible = false;
4824+
known_dispatch_pc = false; // dispatch from cpuRegs.pc: an IRQ raised by the tail event test, or ERET's new pc
4825+
endpc = pc + 4;
4826+
break;
4827+
}
4828+
47354829
// Straight-line op we can codegen? (Generators decode from `op` directly;
47364830
// they never read cpuRegs.code, so nothing to set here at compile time.)
47374831
if (recTranslateOpOptimized(op, const_state, cache_state))

0 commit comments

Comments
 (0)