Files
ARMSX2/pcsx2/R3000AInterpreter.cpp
T
pstef 2aae26a187 Fix psxJALR, add tests: IOP loads, stores and branches
Fixes psxJALR: it read its branch target out of Rs AFTER writing the
link into Rd, so `jalr $t0, $t0` jumped to the link address instead of
the old Rs.  Hardware keeps the old Rs. rpsxJALR already stashed Rs to
pcWriteback before linking, so the two IOP engines were split and the
harness diff would have had to be suppressed to leave it. Reverting the
one-line reorder fails exactly that case and nothing else.

lsudelay is recorded as known-bad instead of asserted: PCSX2 retires IOP
loads immediately in both engines, so three of its nine cases diverge
from silicon on both, with DISABLED_AllLsuDelayMatchesConsole as the
tripwire.

Also pinned, none of it reachable by a differential test: BGEZAL/BLTZAL
link unconditionally, the delay slot always runs, the delay slot's write
to the link register beats the link itself, `sd`/`ld` are GAS macros
over lw/sw pairs rather than IOP instructions, and mfhi/mflo/mult/div
expose no HI/LO delay.

Six sibling functions share psxJALR's link-before-read shape but only
differ when Rs is 31, which the capture never exercised. Both IOP
engines agree there, so no claim is made; the shared behavior is pinned
by LinkBranchesWithRaAsSourceArePinned.
2026-07-26 10:41:26 +02:00

338 lines
7.8 KiB
C++

// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "R3000A.h"
#include "Common.h"
#include "Config.h"
#include "VMManager.h"
#include "R5900OpcodeTables.h"
#include "DebugTools/Breakpoints.h"
#include "IopBios.h"
#include "IopHw.h"
using namespace R3000A;
// Used to flag delay slot instructions when throwig exceptions.
bool iopIsDelaySlot = false;
static bool branch2 = 0;
static u32 branchPC;
static void doBranch(s32 tar); // forward declared prototype
/*********************************************************
* Register branch logic *
* Format: OP rs, offset *
*********************************************************/
void psxBGEZ() // Branch if Rs >= 0
{
if (_i32(_rRs_) >= 0) doBranch(_BranchTarget_);
}
void psxBGEZAL() // Branch if Rs >= 0 and link
{
_SetLink(31);
if (_i32(_rRs_) >= 0)
{
doBranch(_BranchTarget_);
}
}
void psxBGTZ() // Branch if Rs > 0
{
if (_i32(_rRs_) > 0) doBranch(_BranchTarget_);
}
void psxBLEZ() // Branch if Rs <= 0
{
if (_i32(_rRs_) <= 0) doBranch(_BranchTarget_);
}
void psxBLTZ() // Branch if Rs < 0
{
if (_i32(_rRs_) < 0) doBranch(_BranchTarget_);
}
void psxBLTZAL() // Branch if Rs < 0 and link
{
_SetLink(31);
if (_i32(_rRs_) < 0)
{
doBranch(_BranchTarget_);
}
}
/*********************************************************
* Register branch logic *
* Format: OP rs, rt, offset *
*********************************************************/
void psxBEQ() // Branch if Rs == Rt
{
if (_i32(_rRs_) == _i32(_rRt_)) doBranch(_BranchTarget_);
}
void psxBNE() // Branch if Rs != Rt
{
if (_i32(_rRs_) != _i32(_rRt_)) doBranch(_BranchTarget_);
}
/*********************************************************
* Jump to target *
* Format: OP target *
*********************************************************/
void psxJ()
{
// check for iop module import table magic
u32 delayslot = iopMemRead32(psxRegs.pc);
if (delayslot >> 16 == 0x2400 && irxImportExec(irxImportTableAddr(psxRegs.pc), delayslot & 0xffff))
return;
doBranch(_JumpTarget_);
}
void psxJAL()
{
_SetLink(31);
doBranch(_JumpTarget_);
}
/*********************************************************
* Register jump *
* Format: OP rs, rd *
*********************************************************/
void psxJR()
{
doBranch(_u32(_rRs_));
}
void psxJALR()
{
const u32 target = _u32(_rRs_); // latch the target before linking
if (_Rd_)
{
_SetLink(_Rd_);
}
doBranch(target);
}
void psxBreakpoint(bool memcheck)
{
u32 pc = psxRegs.pc;
if (CBreakPoints::CheckSkipFirst(BREAKPOINT_IOP, pc) != 0)
{
CBreakPoints::ClearSkipFirst(BREAKPOINT_IOP);
return;
}
if (!memcheck)
{
auto cond = CBreakPoints::GetBreakPointCondition(BREAKPOINT_IOP, pc);
if (cond && !cond->Evaluate())
return;
}
CBreakPoints::SetBreakpointTriggered(true, BREAKPOINT_IOP);
VMManager::SetPaused(true);
Cpu->ExitExecution();
}
void psxMemcheck(u32 op, u32 bits, bool store)
{
// compute accessed address
u32 start = psxRegs.GPR.r[(op >> 21) & 0x1F];
if ((s16)op != 0)
start += (s16)op;
u32 end = start + bits / 8;
auto checks = CBreakPoints::GetMemChecks(BREAKPOINT_IOP);
for (size_t i = 0; i < checks.size(); i++)
{
auto& check = checks[i];
if (check.result == 0)
continue;
if ((check.memCond & MEMCHECK_WRITE) == 0 && store)
continue;
if ((check.memCond & MEMCHECK_READ) == 0 && !store)
continue;
if (check.hasCond)
{
if (!check.cond.Evaluate())
continue;
}
if (start < check.end && check.start < end)
psxBreakpoint(true);
}
}
void psxCheckMemcheck()
{
u32 pc = psxRegs.pc;
int needed = psxIsMemcheckNeeded(pc);
if (needed == 0)
return;
u32 op = iopMemRead32(needed == 2 ? pc + 4 : pc);
// Yeah, we use the R5900 opcode table for the R3000
const R5900::OPCODE& opcode = R5900::GetInstruction(op);
bool store = (opcode.flags & IS_STORE) != 0;
switch (opcode.flags & MEMTYPE_MASK)
{
case MEMTYPE_BYTE:
psxMemcheck(op, 8, store);
break;
case MEMTYPE_HALF:
psxMemcheck(op, 16, store);
break;
case MEMTYPE_WORD:
psxMemcheck(op, 32, store);
break;
case MEMTYPE_DWORD:
psxMemcheck(op, 64, store);
break;
}
}
///////////////////////////////////////////
// These macros are used to assemble the repassembler functions
static __fi void execI()
{
// This function is called for every instruction.
// Enabling the define below will probably, no, will cause the interpretor to be slower.
//#define EXTRA_DEBUG
#if defined(EXTRA_DEBUG) || defined(PCSX2_DEVBUILD)
if (psxIsBreakpointNeeded(psxRegs.pc))
psxBreakpoint(false);
psxCheckMemcheck();
CBreakPoints::CommitClearSkipFirst(BREAKPOINT_IOP);
#endif
// Inject IRX hack
if (psxRegs.pc == 0x1630 && EmuConfig.CurrentIRX.length() > 3) {
if (iopMemRead32(0x20018) == 0x1F) {
// FIXME do I need to increase the module count (0x1F -> 0x20)
iopMemWrite32(0x20094, 0xbffc0000);
}
}
psxRegs.code = iopMemRead32(psxRegs.pc);
PSXCPU_LOG("%s", disR3000AF(psxRegs.code, psxRegs.pc));
psxRegs.pc+= 4;
psxRegs.cycle++;
psxBSC[psxRegs.code >> 26]();
}
static void doBranch(s32 tar) {
if (tar == 0x0)
DevCon.Warning("[R3000 Interpreter] Warning: Branch to 0x0!");
// When upgrading the IOP, there are two resets, the second of which is a 'fake' reset
// This second 'reset' involves UDNL calling SYSMEM and LOADCORE directly, resetting LOADCORE's modules
// This detects when SYSMEM is called and clears the modules then
if(tar == 0x890)
{
DevCon.WriteLn(Color_Gray, "R3000 Debugger: Branch to 0x890 (SYSMEM). Clearing modules.");
R3000SymbolGuardian.ClearIrxModules();
}
// Override the memory size argument to IOPBOOT
if(tar == 0xbfc4a000) {
psxRegs.GPR.n.a0 = Ps2MemSize::ExposedIopRam >> 20;
}
branch2 = iopIsDelaySlot = true;
branchPC = tar;
execI();
PSXCPU_LOG( "\n" );
iopIsDelaySlot = false;
psxRegs.pc = branchPC;
iopEventTest();
}
// Interpret exactly one IOP instruction at psxRegs.pc, then return. This is the
// ARM64 IOP recompiler's per-instruction fallback for opcodes it cannot yet compile.
// It mirrors the interpreter's inner step: execI() reads the op, advances pc, charges
// one cycle and dispatches; for a branch opcode the interpreter's doBranch runs the
// delay slot, redirects pc and runs the IOP event test, exactly as in intExecuteBlock.
// It must NOT end the IOP timeslice (that is driven by the rec's recExecuteBlock loop
// via iopCycleEE).
void iopExecuteOneInst()
{
execI();
}
static void intReserve() {
}
static void intAlloc() {
}
static void intReset() {
intAlloc();
}
static s32 intExecuteBlock( s32 eeCycles )
{
psxRegs.iopBreak = 0;
psxRegs.iopCycleEE = eeCycles;
u64 lastIOPCycle = 0;
while (psxRegs.iopCycleEE > 0)
{
lastIOPCycle = psxRegs.cycle;
if ((psxHu32(HW_ICFG) & 8) && ((psxRegs.pc & 0x1fffffffU) == 0xa0 || (psxRegs.pc & 0x1fffffffU) == 0xb0 || (psxRegs.pc & 0x1fffffffU) == 0xc0))
psxBiosCall();
branch2 = 0;
while (!branch2)
execI();
if ((psxHu32(HW_ICFG) & (1 << 3)))
{
// F = gcd(PS2CLK, PSXCLK) = 230400
const u32 cnum = 1280; // PS2CLK / F
const u32 cdenom = 147; // PSXCLK / F
//One of the Iop to EE delta clocks to be set in PS1 mode.
const u32 t = ((cnum * (psxRegs.cycle - lastIOPCycle)) + psxRegs.iopCycleEECarry);
psxRegs.iopCycleEE -= t / cdenom;
psxRegs.iopCycleEECarry = t % cdenom;
}
else
{
//default ps2 mode value
psxRegs.iopCycleEE -= (psxRegs.cycle - lastIOPCycle) * 8;
}
}
return psxRegs.iopBreak + psxRegs.iopCycleEE;
}
static void intClear(u32 Addr, u32 Size) {
}
static void intShutdown() {
}
R3000Acpu psxInt = {
intReserve,
intReset,
intExecuteBlock,
intClear,
intShutdown
};