mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
SL-03: arm64 EE rec: superblocks — forward conditionals become continuation sites
The block scanner no longer ends a block at a forward conditional branch (BEQ/BNE/BLEZ/BGTZ/BLTZ/BGEZ; non-likely, non-link, non-BCx): it records a continuation site and keeps scanning at the fallthrough, so the not-taken path compiles as one straight line — no pc store, no event check, no linked-B, no next-head reload; register and constant residency ride through the former boundary. The taken arm becomes a cold side exit outlined after the block tail: it snapshots the compile state at the branch (BranchCompileState, per pending exit), and its emission restores the snapshot, compiles the taken-path delay slot (unless TrySwapDelaySlot already hoisted it), and ends with the normal SetBranchImm flush + event + linked-B tail. Compare shapes mirror recSetBranchEQ/recSetBranchL with the sense inverted (branch out when TAKEN), const fast paths included. Analysis stays exactly as conservative as today's block ends at each former boundary: the liveness backward pass merges all-live at the branch and its delay slot (the taken path leaves the block there), and the COP2 deferred-commit passes run per segment delimited at sites. A backward-split landing on a site's delay slot clamps to the branch instead (a split pair would leave the delay slot outside the analyzed range). Event-check coarsening equals today's straight-line blocks: one check per exit, range still capped by the 4K page. Caps: 8 sites, 128 insns per block. Composition with SL-01: loops whose body contains a forward conditional were previously split at it and could never form a self-loop — now they fuse into one block and the loop-residency preheader/back-edge applies (pinned by LoopWithInternalForwardBranchBecomesResident). Deliberate non-sites: backward branches (loops keep the SL-01 shape), likely variants (taken-only delay slot — different continuation shape), BEQ rs==rt (unconditional idiom), branch-class delay slots, compile-time const-resolved-taken. BNE rs==rt and const-resolved-not-taken continue with no side exit. Tests: ee_rec_superblock_tests (formation via recEeBlockGuestSize, both runtime paths vs interp, delay-slot-both-paths, dirty-state flush at taken exits incl. NEON quads, const propagation, multi-site, cap, SMC in the fused range, memory traffic across a site); full recompiler_tests 1336/1336. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
8c8dc89e2b
commit
151b93fde9
@@ -1632,6 +1632,101 @@ void LoadBranchState()
|
||||
s_savedBranchState.restore();
|
||||
}
|
||||
|
||||
// =====================================================================================================
|
||||
// SL-03: superblocks — conditional-fallthrough continuation
|
||||
// =====================================================================================================
|
||||
// The scanner records forward conditional branches (BEQ/BNE/BLEZ/BGTZ/BLTZ/
|
||||
// BGEZ; non-likely, non-link) as continuation sites and keeps scanning at the
|
||||
// fallthrough, so the not-taken path compiles as one straight line: no pc
|
||||
// store, no event check, no linked-B, no next-head reload — register/const
|
||||
// residency rides through the former boundary. The taken arm becomes a cold
|
||||
// side exit outlined after the block tail; it snapshots the compile state at
|
||||
// the branch, and its emission restores that snapshot, compiles the taken-path
|
||||
// delay slot (unless TrySwapDelaySlot already hoisted it), and ends with the
|
||||
// normal SetBranchImm tail. Analysis stays exactly as conservative as today's
|
||||
// block ends: the liveness backward pass merges all-live at each site (the
|
||||
// taken path leaves the block there), and the COP2 deferred-commit passes run
|
||||
// per segment delimited at sites. Event-check coarsening equals today's
|
||||
// straight-line blocks (one check per exit, range capped by the 4K page).
|
||||
|
||||
static constexpr int kMaxContSites = 8;
|
||||
static constexpr u32 kMaxSuperblockInsns = 128;
|
||||
|
||||
static u32 s_contSitePcs[kMaxContSites]; // ascending (scan order)
|
||||
static int s_numContSites = 0;
|
||||
|
||||
struct SuperblockSideExit
|
||||
{
|
||||
std::unique_ptr<a64::Label> label;
|
||||
u32 branchTo;
|
||||
u32 dsPc;
|
||||
bool needDs;
|
||||
BranchCompileState state;
|
||||
};
|
||||
static SuperblockSideExit s_sideExits[kMaxContSites];
|
||||
static int s_numSideExits = 0;
|
||||
|
||||
bool recSuperblockIsContSite(u32 branch_pc)
|
||||
{
|
||||
for (int i = 0; i < s_numContSites; i++)
|
||||
if (s_contSitePcs[i] == branch_pc)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Snapshot the compile state for the taken path and hand back the label the
|
||||
// handler's inverted condition branches to. Call after _eeFlushAllDirty and
|
||||
// (for a swapped slot) after the delay-slot emission; the compare itself is a
|
||||
// pure post-flush read and may be emitted after the snapshot. pc points at the
|
||||
// delay slot here.
|
||||
a64::Label* recSuperblockAddSideExit(u32 branch_target, bool need_delay_slot)
|
||||
{
|
||||
pxAssert(s_numSideExits < kMaxContSites);
|
||||
SuperblockSideExit& x = s_sideExits[s_numSideExits++];
|
||||
x.label = std::make_unique<a64::Label>();
|
||||
x.branchTo = branch_target;
|
||||
x.dsPc = pc;
|
||||
x.needDs = need_delay_slot;
|
||||
x.state.capture();
|
||||
return x.label.get();
|
||||
}
|
||||
|
||||
// Emit every pending cold side exit after the block tail. Each restores its
|
||||
// branch-point snapshot, so the exit charges exactly the branch-point cycles
|
||||
// and SetBranchImm's flush writes back exactly what was live-dirty there.
|
||||
// The mainline pc/size/recRAMCopy bookkeeping ran before this — pc is dead.
|
||||
static void recEmitPendingSideExits()
|
||||
{
|
||||
const int n = s_numSideExits;
|
||||
s_numSideExits = 0;
|
||||
for (int k = 0; k < n; k++)
|
||||
{
|
||||
SuperblockSideExit& x = s_sideExits[k];
|
||||
armAsm->Bind(x.label.get());
|
||||
x.state.restore();
|
||||
g_branch = 0;
|
||||
if (x.needDs)
|
||||
{
|
||||
pc = x.dsPc;
|
||||
recompileNextInstruction(true, false);
|
||||
}
|
||||
SetBranchImm(x.branchTo);
|
||||
x.label.reset();
|
||||
}
|
||||
g_branch = 1;
|
||||
}
|
||||
|
||||
// Liveness barrier for the backward pass: `addr` is a continuation branch or
|
||||
// its delay slot. The taken path leaves the block after the delay slot, so no
|
||||
// dead-value assumption may cross either out-state.
|
||||
static bool recSuperblockLivenessBarrier(u32 addr)
|
||||
{
|
||||
for (int i = 0; i < s_numContSites; i++)
|
||||
if (addr == s_contSitePcs[i] || addr == s_contSitePcs[i] + 4)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// =====================================================================================================
|
||||
// Instruction recompilation
|
||||
// =====================================================================================================
|
||||
@@ -2710,6 +2805,15 @@ bool recEeLoopBackedgeInfo(u32 pc_query, uptr* site, uptr* stub)
|
||||
return true;
|
||||
}
|
||||
|
||||
// SL-03 introspection: guest-insn size of the compiled block starting at
|
||||
// pc_query (0 = no block), so tests can assert superblock formation (the
|
||||
// range spans past a continuation branch) vs termination (it doesn't).
|
||||
u32 recEeBlockGuestSize(u32 pc_query)
|
||||
{
|
||||
BASEBLOCKEX* b = recBlocks.Get(HWADDR(pc_query));
|
||||
return b ? b->size : 0;
|
||||
}
|
||||
|
||||
// Test-harness recLUT coverage introspection: does this guest PC dispatch to
|
||||
// a real (compile-on-first-hit) LUT page rather than UnmappedRecLUTPage?
|
||||
// The harness can't execute code from ROM regions (it's hardwired to EE
|
||||
@@ -2850,6 +2954,63 @@ static bool recSkipTimeoutLoop(s32 reg, bool is_timeout_loop)
|
||||
// Main Recompilation Loop
|
||||
// =====================================================================================================
|
||||
|
||||
// Scanner-side decode: is `code` any branch/jump/eret/syscall-class op (a
|
||||
// block-ender or continuation candidate)? Used to refuse continuation through
|
||||
// a branch whose delay slot is itself a branch (architecturally-UB shape) —
|
||||
// those fall back to ending the block, which is today's behavior.
|
||||
static bool eeScanInsnIsBranchClass(u32 code)
|
||||
{
|
||||
switch (code >> 26)
|
||||
{
|
||||
case 0: // SPECIAL
|
||||
{
|
||||
const u32 funct = code & 0x3f;
|
||||
return funct == 8 || funct == 9 || funct == 12 || funct == 13; // JR/JALR/SYSCALL/BREAK
|
||||
}
|
||||
case 1: // REGIMM
|
||||
{
|
||||
const u32 rt = (code >> 16) & 0x1f;
|
||||
return rt < 4 || (rt >= 16 && rt < 20);
|
||||
}
|
||||
case 2: case 3: // J, JAL
|
||||
case 4: case 5: case 6: case 7: // BEQ, BNE, BLEZ, BGTZ
|
||||
case 20: case 21: case 22: case 23: // likely forms
|
||||
return true;
|
||||
case 16: // COP0: BC0x or ERET
|
||||
return ((code >> 21) & 0x1f) == 8 ||
|
||||
(((code >> 21) & 0x1f) == 16 && (code & 0x3f) == 24);
|
||||
case 17: case 18: // COP1/COP2: BCx
|
||||
return ((code >> 21) & 0x1f) == 8;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Scanner-side continuation gate for a conditional branch at `i` targeting
|
||||
// `target`. Forward-only (backward keeps the split/end logic), bounded, and
|
||||
// refuses a branch-class delay slot.
|
||||
static bool eeScanContinuable(u32 startpc, u32 i, u32 target)
|
||||
{
|
||||
return target > i + 4 &&
|
||||
s_numContSites < kMaxContSites &&
|
||||
((i + 8 - startpc) / 4) < kMaxSuperblockInsns &&
|
||||
!eeScanInsnIsBranchClass(memRead32(i + 4));
|
||||
}
|
||||
|
||||
// A backward-split target that lands exactly on a continuation site's delay
|
||||
// slot would leave that branch as the last insn of the range with its delay
|
||||
// slot outside the analyzed block (instinfo overrun, split-pair emission).
|
||||
// End the block before the branch instead; the split's purpose — making the
|
||||
// target a block start — is preserved (a block starting at the delay-slot
|
||||
// address compiles it as a plain instruction, which is the architectural
|
||||
// meaning of branching into a delay slot).
|
||||
static u32 eeSuperblockClampSplit(u32 target)
|
||||
{
|
||||
for (int k = 0; k < s_numContSites; k++)
|
||||
if (target == s_contSitePcs[k] + 4)
|
||||
return s_contSitePcs[k];
|
||||
return target;
|
||||
}
|
||||
|
||||
static void recRecompile(const u32 startpc)
|
||||
{
|
||||
u32 i;
|
||||
@@ -3036,6 +3197,7 @@ static void recRecompile(const u32 startpc)
|
||||
s_nEndBlock = 0xffffffff;
|
||||
s_branchTo = -1;
|
||||
s_branchLoopable = false;
|
||||
s_numContSites = 0;
|
||||
|
||||
// Timeout loop detection (matches x86 recSkipTimeoutLoop pattern):
|
||||
// addiu reg,reg,-N / nop*N / bne reg,zero,loop / nop
|
||||
@@ -3106,13 +3268,22 @@ static void recRecompile(const u32 startpc)
|
||||
{
|
||||
// rt 16-19 are the AL link variants — call-shaped tails
|
||||
// (SetBranchImmCall), not back-edge candidates.
|
||||
// SL-03: forward BLTZ/BGEZ (rt 0/1) become continuation
|
||||
// sites — scan on at the fallthrough. Likely + AL forms
|
||||
// keep ending the block.
|
||||
if (_Rt_ < 2 && eeScanContinuable(startpc, i, _Imm_ * 4 + i + 4))
|
||||
{
|
||||
s_contSitePcs[s_numContSites++] = i;
|
||||
i += 8; // skip the delay slot word in the scan
|
||||
continue;
|
||||
}
|
||||
s_branchLoopable = _Rt_ < 4;
|
||||
s_branchTo = _Imm_ * 4 + i + 4;
|
||||
// Backward branch into the current block: end the block at the
|
||||
// target so the loop head becomes its own linkable block.
|
||||
// Mirrors x86 iR5900.cpp:2362 and the COP1/COP2 case below.
|
||||
if (s_branchTo > startpc && s_branchTo < i)
|
||||
s_nEndBlock = s_branchTo;
|
||||
s_nEndBlock = eeSuperblockClampSplit(s_branchTo);
|
||||
else
|
||||
{
|
||||
s_nEndBlock = i + 8;
|
||||
@@ -3128,6 +3299,18 @@ static void recRecompile(const u32 startpc)
|
||||
goto StartRecomp;
|
||||
|
||||
case 4: case 5: case 6: case 7: // BEQ, BNE, BLEZ, BGTZ
|
||||
// SL-03: forward conditionals become continuation sites — scan
|
||||
// on at the fallthrough. BEQ rs==rt is the unconditional-`b`
|
||||
// idiom (always taken: everything after is unreachable on the
|
||||
// fallthrough) and keeps ending the block.
|
||||
if (!((cpuRegs.code >> 26) == 4 && _Rs_ == _Rt_) &&
|
||||
eeScanContinuable(startpc, i, _Imm_ * 4 + i + 4))
|
||||
{
|
||||
s_contSitePcs[s_numContSites++] = i;
|
||||
i += 8; // skip the delay slot word in the scan
|
||||
continue;
|
||||
}
|
||||
[[fallthrough]];
|
||||
case 20: case 21: // BEQL, BNEL
|
||||
case 22: case 23: // BLEZL, BGTZL
|
||||
s_branchLoopable = true;
|
||||
@@ -3136,7 +3319,7 @@ static void recRecompile(const u32 startpc)
|
||||
// is its own linkable block. Mirrors x86 iR5900.cpp:2387 and
|
||||
// the COP1/COP2 case below.
|
||||
if (s_branchTo > startpc && s_branchTo < i)
|
||||
s_nEndBlock = s_branchTo;
|
||||
s_nEndBlock = eeSuperblockClampSplit(s_branchTo);
|
||||
else
|
||||
{
|
||||
s_nEndBlock = i + 8;
|
||||
@@ -3159,7 +3342,7 @@ static void recRecompile(const u32 startpc)
|
||||
s_branchLoopable = true;
|
||||
s_branchTo = _Imm_ * 4 + i + 4;
|
||||
if (s_branchTo > startpc && s_branchTo < i)
|
||||
s_nEndBlock = s_branchTo;
|
||||
s_nEndBlock = eeSuperblockClampSplit(s_branchTo);
|
||||
else
|
||||
{
|
||||
s_nEndBlock = i + 8;
|
||||
@@ -3174,6 +3357,14 @@ static void recRecompile(const u32 startpc)
|
||||
|
||||
StartRecomp:
|
||||
|
||||
// SL-03: a backward-split can truncate s_nEndBlock below already-recorded
|
||||
// continuation sites — drop any site whose branch+delay-slot pair no longer
|
||||
// fits inside [startpc, s_nEndBlock). (Sites are ascending; the compile
|
||||
// loop never reaches a dropped one, but the analysis passes below must not
|
||||
// treat it as an exit boundary either.)
|
||||
while (s_numContSites > 0 && s_contSitePcs[s_numContSites - 1] + 8 > s_nEndBlock)
|
||||
s_numContSites--;
|
||||
|
||||
// Self-modifying code detection: generate inline memory checks for manual blocks.
|
||||
const bool is_manual_block = memory_protect_recompiled_code(startpc, (s_nEndBlock - startpc) >> 2);
|
||||
|
||||
@@ -3306,6 +3497,18 @@ StartRecomp:
|
||||
for (i = s_nEndBlock; i > startpc; i -= 4)
|
||||
{
|
||||
cpuRegs.code = memRead32(i - 4);
|
||||
// SL-03: at a continuation site the taken path leaves the block
|
||||
// after the delay slot — merge "everything live" (the block-end
|
||||
// init state) into the out-state of both the branch and its delay
|
||||
// slot so no dead-value assumption crosses the side exit. This is
|
||||
// exactly today's block-end conservatism at the former boundary.
|
||||
if (recSuperblockLivenessBarrier(i - 4))
|
||||
{
|
||||
memset(pcur->regs, EEINST_LIVE, sizeof(pcur->regs));
|
||||
memset(pcur->fpuregs, EEINST_LIVE, sizeof(pcur->fpuregs));
|
||||
memset(pcur->vfregs, EEINST_LIVE, sizeof(pcur->vfregs));
|
||||
memset(pcur->viregs, EEINST_LIVE, sizeof(pcur->viregs));
|
||||
}
|
||||
pcur[-1] = pcur[0];
|
||||
recBackpropBSC(cpuRegs.code, pcur - 1, pcur);
|
||||
pcur--;
|
||||
@@ -3315,12 +3518,27 @@ StartRecomp:
|
||||
|
||||
// Run COP2 analysis passes — sets EEINST_COP2_SYNC_VU0/FINISH_VU0 flags
|
||||
// for conditional VU0 synchronization in transfer ops.
|
||||
// SL-03: run per segment, delimited at continuation sites. Both passes
|
||||
// defer commits forward (flag-hack elision, micro-finish placement); a
|
||||
// deferral must not cross a side exit that can escape before the
|
||||
// superseding instruction executes. Per-segment == today's per-block
|
||||
// semantics at each former boundary.
|
||||
if (has_cop2_instructions)
|
||||
{
|
||||
R5900::COP2MicroFinishPass().Run(startpc, s_nEndBlock, s_pInstCache + 1);
|
||||
u32 seg_start = startpc;
|
||||
for (int k = 0; k <= s_numContSites; k++)
|
||||
{
|
||||
const u32 seg_end = (k < s_numContSites) ? (s_contSitePcs[k] + 8) : s_nEndBlock;
|
||||
if (seg_end <= seg_start)
|
||||
continue;
|
||||
EEINST* const seg_inst = s_pInstCache + 1 + (seg_start - startpc) / 4;
|
||||
R5900::COP2MicroFinishPass().Run(seg_start, seg_end, seg_inst);
|
||||
|
||||
if (EmuConfig.Speedhacks.vuFlagHack)
|
||||
R5900::COP2FlagHackPass().Run(startpc, s_nEndBlock, s_pInstCache + 1);
|
||||
if (EmuConfig.Speedhacks.vuFlagHack)
|
||||
R5900::COP2FlagHackPass().Run(seg_start, seg_end, seg_inst);
|
||||
|
||||
seg_start = seg_end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3340,6 +3558,9 @@ StartRecomp:
|
||||
// Sweep any LDL/LDR fusion residue from an aborted prior compile (the
|
||||
// per-pair gate otherwise guarantees same-block consume).
|
||||
g_eeUnalignedFused = false;
|
||||
// SL-03: sweep side-exit residue the same way (recEmitPendingSideExits
|
||||
// drains the list on every completed compile).
|
||||
s_numSideExits = 0;
|
||||
|
||||
// SL-1 candidacy: a resident self-loop is a block whose terminal branch
|
||||
// targets its own startpc. Wait-loop-FF blocks keep the fast-forward
|
||||
@@ -3505,6 +3726,10 @@ StartRecomp:
|
||||
}
|
||||
}
|
||||
|
||||
// SL-03: outline the cold taken side exits after the tail (mainline pc is
|
||||
// dead — size and recRAMCopy bookkeeping ran on it above).
|
||||
recEmitPendingSideExits();
|
||||
|
||||
pxAssert(armGetCurrentCodePointer() < SysMemory::GetEERecEnd());
|
||||
|
||||
// Size is from the aligned block_fnptr, not the pre-alignment recPtr —
|
||||
|
||||
@@ -657,6 +657,18 @@ bool TrySwapDelaySlot(u32 rs, u32 rt, u32 rd, bool allow_loadstore);
|
||||
void SaveBranchState();
|
||||
void LoadBranchState();
|
||||
|
||||
// SL-03 superblocks: conditional-fallthrough continuation. The block scanner
|
||||
// records forward conditional branches as continuation sites instead of ending
|
||||
// the block there. A site's handler queries recSuperblockIsContSite(pc-4) and
|
||||
// emits the inverted shape — branch OUT to a cold taken side exit, compile the
|
||||
// delay slot inline, and fall through into the rest of the block — instead of
|
||||
// the two-arm terminal fork. Side exits snapshot the compile state at the
|
||||
// branch and are outlined after the block tail (recEmitPendingSideExits),
|
||||
// where each restores its snapshot, compiles the taken-path delay slot, and
|
||||
// emits the normal flush + event-check + linked-B tail.
|
||||
bool recSuperblockIsContSite(u32 branch_pc);
|
||||
vixl::aarch64::Label* recSuperblockAddSideExit(u32 branch_target, bool need_delay_slot);
|
||||
|
||||
void recompileNextInstruction(bool delayslot, bool swapped_delay_slot);
|
||||
|
||||
// Block-tail transfer for register-indirect targets (recJR/recJALR).
|
||||
|
||||
@@ -136,6 +136,122 @@ static void recBindBranchLabel()
|
||||
s_pBranchLabel = nullptr;
|
||||
}
|
||||
|
||||
// =====================================================================================================
|
||||
// SL-03 superblock continuation — inverted branch emission
|
||||
// =====================================================================================================
|
||||
// At a continuation site the block does NOT end: the handler emits the
|
||||
// TAKEN-condition branch out to a cold side exit (recorded for outlined
|
||||
// emission after the block tail), compiles the not-taken delay slot inline,
|
||||
// and returns with g_branch clear so the main loop keeps compiling at the
|
||||
// fallthrough. Compare shapes mirror recSetBranchEQ/recSetBranchL with the
|
||||
// condition sense inverted (branch out when TAKEN instead of skip when
|
||||
// not-taken), const fast paths included.
|
||||
|
||||
// BEQ/BNE continuation. Returns true when fully handled (caller returns).
|
||||
static bool recTrySuperblockContinueEQ(bool is_bne)
|
||||
{
|
||||
if (!recSuperblockIsContSite(pc - 4))
|
||||
return false;
|
||||
const u32 branchTo = ((s32)_Imm_ * 4) + pc;
|
||||
|
||||
if (GPR_IS_CONST2(_Rs_, _Rt_))
|
||||
{
|
||||
const bool taken = is_bne ? (g_cpuConstRegs[_Rs_].SD[0] != g_cpuConstRegs[_Rt_].SD[0]) :
|
||||
(g_cpuConstRegs[_Rs_].SD[0] == g_cpuConstRegs[_Rt_].SD[0]);
|
||||
if (taken)
|
||||
return false; // resolved-taken: terminal — the const path emits it
|
||||
recompileNextInstruction(true, false); // resolved fallthrough: no exit
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_Rs_ == _Rt_)
|
||||
{
|
||||
if (!is_bne)
|
||||
return false; // BEQ rs==rt: always taken (scanner excludes; defensive)
|
||||
recompileNextInstruction(true, false); // BNE rs==rt: never taken
|
||||
return true;
|
||||
}
|
||||
|
||||
const int process = GPR_IS_CONST1(_Rs_) ? PROCESS_CONSTS :
|
||||
(GPR_IS_CONST1(_Rt_) ? PROCESS_CONSTT : 0);
|
||||
const bool swap = TrySwapDelaySlot(_Rs_, _Rt_, 0, true);
|
||||
_eeFlushAllDirty();
|
||||
a64::Label* const exit = recSuperblockAddSideExit(branchTo, !swap);
|
||||
|
||||
if (process)
|
||||
{
|
||||
const int constReg = (process & PROCESS_CONSTS) ? _Rs_ : _Rt_;
|
||||
const int liveReg = (process & PROCESS_CONSTS) ? _Rt_ : _Rs_;
|
||||
const s64 cval = g_cpuConstRegs[constReg].SD[0];
|
||||
const a64::Register live = loadGPRtoX(RXARG1, liveReg);
|
||||
if (cval == 0)
|
||||
{
|
||||
// BEQ taken ⇔ live == 0 → Cbz; BNE taken ⇔ live != 0 → Cbnz.
|
||||
if (is_bne)
|
||||
armAsm->Cbnz(live, exit);
|
||||
else
|
||||
armAsm->Cbz(live, exit);
|
||||
}
|
||||
else
|
||||
{
|
||||
armAsm->Cmp(live, cval);
|
||||
armAsm->B(exit, is_bne ? a64::ne : a64::eq);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const a64::Register rs = loadGPRtoX(RXARG1, _Rs_);
|
||||
const a64::Register rt = loadGPRtoX(RXSCRATCH, _Rt_);
|
||||
armAsm->Cmp(rs, rt);
|
||||
armAsm->B(exit, is_bne ? a64::ne : a64::eq);
|
||||
}
|
||||
|
||||
if (!swap)
|
||||
recompileNextInstruction(true, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
// BLEZ/BGTZ/BLTZ/BGEZ continuation; taken_cond is the TAKEN sense (le/gt/lt/ge).
|
||||
static bool recTrySuperblockContinueSingle(a64::Condition taken_cond)
|
||||
{
|
||||
if (!recSuperblockIsContSite(pc - 4))
|
||||
return false;
|
||||
const u32 branchTo = ((s32)_Imm_ * 4) + pc;
|
||||
|
||||
if (GPR_IS_CONST1(_Rs_))
|
||||
{
|
||||
const s64 val = g_cpuConstRegs[_Rs_].SD[0];
|
||||
bool taken = false;
|
||||
if (taken_cond == a64::le) taken = (val <= 0);
|
||||
else if (taken_cond == a64::gt) taken = (val > 0);
|
||||
else if (taken_cond == a64::lt) taken = (val < 0);
|
||||
else if (taken_cond == a64::ge) taken = (val >= 0);
|
||||
if (taken)
|
||||
return false; // resolved-taken: terminal — the const path emits it
|
||||
recompileNextInstruction(true, false); // resolved fallthrough: no exit
|
||||
return true;
|
||||
}
|
||||
|
||||
const bool swap = TrySwapDelaySlot(_Rs_, 0, 0, true);
|
||||
_eeFlushAllDirty();
|
||||
a64::Label* const exit = recSuperblockAddSideExit(branchTo, !swap);
|
||||
const a64::Register rs = loadGPRtoX(RXSCRATCH, _Rs_);
|
||||
|
||||
if (taken_cond == a64::lt)
|
||||
armAsm->Tbnz(rs, 63, exit); // BLTZ taken ⇔ sign bit set
|
||||
else if (taken_cond == a64::ge)
|
||||
armAsm->Tbz(rs, 63, exit); // BGEZ taken ⇔ sign bit clear
|
||||
else
|
||||
{
|
||||
armAsm->Cmp(rs, 0);
|
||||
armAsm->B(exit, taken_cond);
|
||||
}
|
||||
|
||||
if (!swap)
|
||||
recompileNextInstruction(true, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
//// BEQ — branch if rs == rt
|
||||
static void recBEQ_const()
|
||||
{
|
||||
@@ -182,6 +298,8 @@ static void recBEQ_process(int process)
|
||||
|
||||
void recBEQ()
|
||||
{
|
||||
if (recTrySuperblockContinueEQ(false))
|
||||
return;
|
||||
if (GPR_IS_CONST2(_Rs_, _Rt_))
|
||||
recBEQ_const();
|
||||
else if (GPR_IS_CONST1(_Rs_))
|
||||
@@ -238,6 +356,8 @@ static void recBNE_process(int process)
|
||||
|
||||
void recBNE()
|
||||
{
|
||||
if (recTrySuperblockContinueEQ(true))
|
||||
return;
|
||||
if (GPR_IS_CONST2(_Rs_, _Rt_))
|
||||
recBNE_const();
|
||||
else if (GPR_IS_CONST1(_Rs_))
|
||||
@@ -433,10 +553,30 @@ static void recBranchSingleLikely(a64::Condition skip_cond)
|
||||
SetBranchImm(pc);
|
||||
}
|
||||
|
||||
void recBLEZ() { recBranchSingle(a64::gt); }
|
||||
void recBGTZ() { recBranchSingle(a64::le); }
|
||||
void recBLTZ() { recBranchSingle(a64::ge); }
|
||||
void recBGEZ() { recBranchSingle(a64::lt); }
|
||||
void recBLEZ()
|
||||
{
|
||||
if (recTrySuperblockContinueSingle(a64::le))
|
||||
return;
|
||||
recBranchSingle(a64::gt);
|
||||
}
|
||||
void recBGTZ()
|
||||
{
|
||||
if (recTrySuperblockContinueSingle(a64::gt))
|
||||
return;
|
||||
recBranchSingle(a64::le);
|
||||
}
|
||||
void recBLTZ()
|
||||
{
|
||||
if (recTrySuperblockContinueSingle(a64::lt))
|
||||
return;
|
||||
recBranchSingle(a64::ge);
|
||||
}
|
||||
void recBGEZ()
|
||||
{
|
||||
if (recTrySuperblockContinueSingle(a64::ge))
|
||||
return;
|
||||
recBranchSingle(a64::lt);
|
||||
}
|
||||
|
||||
void recBLEZL() { recBranchSingleLikely(a64::gt); }
|
||||
void recBGTZL() { recBranchSingleLikely(a64::le); }
|
||||
|
||||
@@ -41,6 +41,7 @@ add_pcsx2_test(recompiler_tests
|
||||
ee_rec_alu_imm_tests.cpp
|
||||
ee_rec_branch_tests.cpp
|
||||
ee_rec_loop_residency_tests.cpp
|
||||
ee_rec_superblock_tests.cpp
|
||||
ee_rec_branch_in_delay_tests.cpp
|
||||
ee_rec_callret_tests.cpp
|
||||
ee_rec_cop0_tests.cpp
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
// SL-03 superblocks: forward conditional branches become continuation sites —
|
||||
// the not-taken path compiles straight through (no tail, no head reload), the
|
||||
// taken arm becomes a cold side exit outlined after the block tail. These
|
||||
// tests pin block formation (via recEeBlockGuestSize), both runtime paths
|
||||
// (Run() diffs the full architectural state against the interpreter), the
|
||||
// delay-slot-on-both-paths contract, snapshot-flush coherence at taken exits,
|
||||
// the S1×S2 composition (a loop with an internal forward branch becomes one
|
||||
// resident self-loop), and the deliberate non-sites (backward, likely,
|
||||
// unconditional-idiom, cap overflow).
|
||||
|
||||
#include "harness/EeRecTestHarness.h"
|
||||
|
||||
#include "R5900.h"
|
||||
#include "vtlb.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace recompiler_tests;
|
||||
using namespace mips;
|
||||
|
||||
extern u32 recEeBlockGuestSize(u32 pc_query);
|
||||
extern bool recEeBlockIsLoopResident(u32 pc_query);
|
||||
|
||||
namespace {
|
||||
constexpr u32 kPark = RecompilerTestEnvironment::kParkingPc;
|
||||
constexpr u32 kProgPc = RecompilerTestEnvironment::kProgramPc;
|
||||
constexpr u32 kDataAddr = 0x00020000; // EE RAM scratch, away from the program
|
||||
|
||||
// Formation-asserting tests need a deterministic page state (the harness
|
||||
// program page can flip to ProtMode_Manual across accumulated write→clear
|
||||
// cycles in this process; manual blocks still superblock, but a fresh state
|
||||
// keeps size assertions honest) — same recipe as the loop-residency tests.
|
||||
void ResetRecAndPageProtection()
|
||||
{
|
||||
recCpu.Reset();
|
||||
mmap_ResetBlockTracking();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// A forward BNE no longer ends the block: the compiled range spans the branch,
|
||||
// its delay slot, both the skipped and merge instructions, and the terminal J.
|
||||
TEST(EeRecSuperblock, ForwardBranchFusesBlock)
|
||||
{
|
||||
ResetRecAndPageProtection();
|
||||
EeRecTestHarness h;
|
||||
h.SetGpr64(reg::t0, 0); // not taken
|
||||
h.LoadProgramNoTerm({
|
||||
BNE(reg::t0, reg::zero, 2), // idx0 → idx4 when taken
|
||||
NOP, // idx1 ds
|
||||
ADDIU(reg::t1, reg::zero, 5), // idx2 fallthrough-only
|
||||
ADDIU(reg::t2, reg::zero, 7), // idx3 merge
|
||||
J(kPark), NOP, // idx4/5
|
||||
});
|
||||
h.Run();
|
||||
h.ExpectGpr64(reg::t1, 5ull);
|
||||
h.ExpectGpr64(reg::t2, 7ull);
|
||||
// Pre-SL-03 this block was 2 insns (branch + delay slot).
|
||||
EXPECT_EQ(recEeBlockGuestSize(kProgPc), 6u);
|
||||
}
|
||||
|
||||
// Same program, condition true: the cold taken side exit runs — it must skip
|
||||
// the fallthrough-only instruction and land on the merge point.
|
||||
TEST(EeRecSuperblock, TakenSideExitCorrect)
|
||||
{
|
||||
EeRecTestHarness h;
|
||||
h.SetGpr64(reg::t0, 1); // taken
|
||||
h.LoadProgramNoTerm({
|
||||
BNE(reg::t0, reg::zero, 2),
|
||||
NOP,
|
||||
ADDIU(reg::t1, reg::zero, 5),
|
||||
ADDIU(reg::t2, reg::zero, 7),
|
||||
J(kPark), NOP,
|
||||
});
|
||||
h.Run();
|
||||
h.ExpectGpr64(reg::t1, 0ull);
|
||||
h.ExpectGpr64(reg::t2, 7ull);
|
||||
}
|
||||
|
||||
// The delay slot executes on BOTH paths: inline on the fallthrough, recompiled
|
||||
// from the snapshot inside the side exit on the taken path.
|
||||
TEST(EeRecSuperblock, DelaySlotExecutesOnBothPaths)
|
||||
{
|
||||
for (const u64 cond : {0ull, 1ull})
|
||||
{
|
||||
EeRecTestHarness h;
|
||||
h.SetGpr64(reg::t0, cond);
|
||||
h.SetGpr64(reg::t3, 0);
|
||||
h.LoadProgramNoTerm({
|
||||
BNE(reg::t0, reg::zero, 2), // idx0
|
||||
ADDIU(reg::t3, reg::t3, 11), // idx1 ds: both paths
|
||||
ADDIU(reg::t1, reg::zero, 5), // idx2 fallthrough-only
|
||||
ADDIU(reg::t2, reg::zero, 7), // idx3 merge
|
||||
J(kPark), NOP,
|
||||
});
|
||||
h.Run();
|
||||
h.ExpectGpr64(reg::t3, 11ull);
|
||||
h.ExpectGpr64(reg::t1, cond ? 0ull : 5ull);
|
||||
h.ExpectGpr64(reg::t2, 7ull);
|
||||
}
|
||||
}
|
||||
|
||||
// Dirty scalar + dirty 128-bit NEON state at the branch: the side exit's
|
||||
// flush is emitted from the branch-point snapshot and must publish exactly
|
||||
// that state before linking out (Run() diffs everything vs the interpreter).
|
||||
TEST(EeRecSuperblock, TakenExitFlushesDirtyState)
|
||||
{
|
||||
EeRecTestHarness h;
|
||||
h.SetGpr64(reg::t0, 3);
|
||||
h.SetGpr64(reg::t1, 4);
|
||||
h.SetGpr128(reg::t4, 0x0000000100000002ull, 0x0000000300000004ull);
|
||||
h.SetGpr128(reg::t5, 0x0000001000000010ull, 0x0000001000000010ull);
|
||||
h.LoadProgramNoTerm({
|
||||
ADDU(reg::t2, reg::t0, reg::t1), // t2 = 7, dirty resident
|
||||
ee::PADDW(reg::t4, reg::t4, reg::t5), // NEON quad dirty
|
||||
BNE(reg::t2, reg::zero, 2), // taken → side exit must flush t2/t4
|
||||
NOP,
|
||||
ADDIU(reg::t2, reg::zero, 0), // fallthrough-only (skipped)
|
||||
ADDIU(reg::v0, reg::zero, 9),
|
||||
J(kPark), NOP,
|
||||
});
|
||||
h.Run();
|
||||
h.ExpectGpr64(reg::t2, 7ull);
|
||||
h.ExpectGpr128(reg::t4, 0x0000001100000012ull, 0x0000001300000014ull);
|
||||
h.ExpectGpr64(reg::v0, 9ull);
|
||||
}
|
||||
|
||||
// A compile-time constant created before the branch stays const through the
|
||||
// continuation — the value must still be architecturally correct on both
|
||||
// paths (the taken exit materializes it in its flush).
|
||||
TEST(EeRecSuperblock, ConstRidesThroughContinuation)
|
||||
{
|
||||
for (const u64 cond : {0ull, 1ull})
|
||||
{
|
||||
EeRecTestHarness h;
|
||||
h.SetGpr64(reg::t0, cond);
|
||||
h.LoadProgramNoTerm({
|
||||
ORI(reg::t2, reg::zero, 0x123), // const before the branch
|
||||
BNE(reg::t0, reg::zero, 2),
|
||||
NOP,
|
||||
ADDIU(reg::t1, reg::zero, 5), // fallthrough-only
|
||||
ADDU(reg::t3, reg::t2, reg::t2), // merge: uses the const
|
||||
J(kPark), NOP,
|
||||
});
|
||||
h.Run();
|
||||
h.ExpectGpr64(reg::t2, 0x123ull);
|
||||
h.ExpectGpr64(reg::t3, 0x246ull);
|
||||
h.ExpectGpr64(reg::t1, cond ? 0ull : 5ull);
|
||||
}
|
||||
}
|
||||
|
||||
// Two continuation sites in one block, all four taken/not-taken combinations.
|
||||
TEST(EeRecSuperblock, MultipleContinuationSites)
|
||||
{
|
||||
for (const u64 c0 : {0ull, 1ull})
|
||||
{
|
||||
for (const u64 c1 : {0ull, 1ull})
|
||||
{
|
||||
EeRecTestHarness h;
|
||||
h.SetGpr64(reg::t0, c0);
|
||||
h.SetGpr64(reg::t1, c1);
|
||||
h.LoadProgramNoTerm({
|
||||
BNE(reg::t0, reg::zero, 2), // idx0 → idx3 (skip the t2 add, land ON the 2nd branch)
|
||||
NOP,
|
||||
ADDIU(reg::t2, reg::t2, 1), // idx2: only when !c0
|
||||
BNE(reg::t1, reg::zero, 2), // idx3 → idx6 (skip the t3 add) — both paths reach this
|
||||
NOP,
|
||||
ADDIU(reg::t3, reg::t3, 1), // idx5: only when !c1
|
||||
NOP, // idx6
|
||||
ADDIU(reg::v0, reg::zero, 9), // idx7
|
||||
J(kPark), NOP,
|
||||
});
|
||||
h.Run();
|
||||
h.ExpectGpr64(reg::t2, c0 ? 0ull : 1ull);
|
||||
h.ExpectGpr64(reg::t3, c1 ? 0ull : 1ull);
|
||||
h.ExpectGpr64(reg::v0, 9ull);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Backward conditionals still end the block (loops stay S1's shape).
|
||||
TEST(EeRecSuperblock, BackwardBranchStillEndsBlock)
|
||||
{
|
||||
ResetRecAndPageProtection();
|
||||
EeRecTestHarness h;
|
||||
h.SetGpr64(reg::t0, 0);
|
||||
h.SetGpr64(reg::t1, 3);
|
||||
h.LoadProgramNoTerm({
|
||||
// 0x00 loop:
|
||||
ADDIU(reg::t0, reg::t0, 1),
|
||||
ADDIU(reg::t1, reg::t1, -1),
|
||||
BNE(reg::t1, reg::zero, -3), // → 0x00
|
||||
NOP,
|
||||
J(kPark), NOP,
|
||||
});
|
||||
h.Run();
|
||||
h.ExpectGpr64(reg::t0, 3ull);
|
||||
EXPECT_EQ(recEeBlockGuestSize(kProgPc), 4u); // loop only — not fused onward
|
||||
}
|
||||
|
||||
// Likely branches keep ending the block (wave-1 scope: their delay slot is
|
||||
// taken-path-only, a different continuation shape).
|
||||
TEST(EeRecSuperblock, LikelyBranchStillEndsBlock)
|
||||
{
|
||||
ResetRecAndPageProtection();
|
||||
EeRecTestHarness h;
|
||||
h.SetGpr64(reg::t0, 0);
|
||||
h.LoadProgramNoTerm({
|
||||
ee::BNEL(reg::t0, reg::zero, 2), // forward likely — terminal
|
||||
NOP,
|
||||
ADDIU(reg::t1, reg::zero, 5),
|
||||
ADDIU(reg::t2, reg::zero, 7),
|
||||
J(kPark), NOP,
|
||||
});
|
||||
h.Run();
|
||||
h.ExpectGpr64(reg::t1, 5ull);
|
||||
h.ExpectGpr64(reg::t2, 7ull);
|
||||
EXPECT_EQ(recEeBlockGuestSize(kProgPc), 2u);
|
||||
}
|
||||
|
||||
// BEQ rs==rt is the unconditional-`b` idiom: always taken, so the fallthrough
|
||||
// is unreachable and the block must end there.
|
||||
TEST(EeRecSuperblock, UnconditionalBeqIdiomEndsBlock)
|
||||
{
|
||||
ResetRecAndPageProtection();
|
||||
EeRecTestHarness h;
|
||||
h.LoadProgramNoTerm({
|
||||
BEQ(reg::zero, reg::zero, 2), // b → idx4
|
||||
NOP,
|
||||
ADDIU(reg::t1, reg::zero, 5), // unreachable
|
||||
NOP,
|
||||
ADDIU(reg::v0, reg::zero, 9), // idx4
|
||||
J(kPark), NOP,
|
||||
});
|
||||
h.Run();
|
||||
h.ExpectGpr64(reg::t1, 0ull);
|
||||
h.ExpectGpr64(reg::v0, 9ull);
|
||||
EXPECT_EQ(recEeBlockGuestSize(kProgPc), 2u);
|
||||
}
|
||||
|
||||
// BNE rs==rt never takes: pure fallthrough, fused with no side exit.
|
||||
TEST(EeRecSuperblock, NeverTakenBneSameRegContinues)
|
||||
{
|
||||
ResetRecAndPageProtection();
|
||||
EeRecTestHarness h;
|
||||
h.SetGpr64(reg::t0, 42);
|
||||
h.LoadProgramNoTerm({
|
||||
BNE(reg::t0, reg::t0, 2),
|
||||
NOP,
|
||||
ADDIU(reg::t1, reg::zero, 5),
|
||||
ADDIU(reg::t2, reg::zero, 7),
|
||||
J(kPark), NOP,
|
||||
});
|
||||
h.Run();
|
||||
h.ExpectGpr64(reg::t1, 5ull);
|
||||
h.ExpectGpr64(reg::t2, 7ull);
|
||||
EXPECT_EQ(recEeBlockGuestSize(kProgPc), 6u);
|
||||
}
|
||||
|
||||
// Compile-time const-resolved-taken branch is terminal (everything after is
|
||||
// unreachable on this compile); const-resolved-not-taken continues inline.
|
||||
TEST(EeRecSuperblock, ConstResolvedTakenEndsBlock)
|
||||
{
|
||||
ResetRecAndPageProtection();
|
||||
EeRecTestHarness h;
|
||||
h.LoadProgramNoTerm({
|
||||
ORI(reg::t0, reg::zero, 1), // t0 = const 1
|
||||
BNE(reg::t0, reg::zero, 2), // resolved taken → idx4
|
||||
NOP,
|
||||
ADDIU(reg::t2, reg::zero, 7), // unreachable
|
||||
ADDIU(reg::v0, reg::zero, 9), // idx4
|
||||
J(kPark), NOP,
|
||||
});
|
||||
h.Run();
|
||||
h.ExpectGpr64(reg::t2, 0ull);
|
||||
h.ExpectGpr64(reg::v0, 9ull);
|
||||
EXPECT_EQ(recEeBlockGuestSize(kProgPc), 3u); // ORI + BNE + ds
|
||||
}
|
||||
|
||||
TEST(EeRecSuperblock, ConstResolvedNotTakenContinues)
|
||||
{
|
||||
ResetRecAndPageProtection();
|
||||
EeRecTestHarness h;
|
||||
h.LoadProgramNoTerm({
|
||||
ORI(reg::t0, reg::zero, 0), // t0 = const 0
|
||||
BNE(reg::t0, reg::zero, 2), // resolved not-taken → falls through
|
||||
NOP,
|
||||
ADDIU(reg::t2, reg::zero, 7),
|
||||
ADDIU(reg::v0, reg::zero, 9),
|
||||
J(kPark), NOP,
|
||||
});
|
||||
h.Run();
|
||||
h.ExpectGpr64(reg::t2, 7ull);
|
||||
h.ExpectGpr64(reg::v0, 9ull);
|
||||
EXPECT_EQ(recEeBlockGuestSize(kProgPc), 7u);
|
||||
}
|
||||
|
||||
// The per-block site cap (8): the ninth forward conditional ends the block.
|
||||
TEST(EeRecSuperblock, SiteCapEndsBlockAtNinthBranch)
|
||||
{
|
||||
ResetRecAndPageProtection();
|
||||
EeRecTestHarness h;
|
||||
h.SetGpr64(reg::t0, 0);
|
||||
std::vector<u32> prog;
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
prog.push_back(BNE(reg::t0, reg::zero, 1)); // → its own fallthrough
|
||||
prog.push_back(NOP);
|
||||
}
|
||||
prog.push_back(ADDIU(reg::v0, reg::zero, 9));
|
||||
prog.push_back(J(kPark));
|
||||
prog.push_back(NOP);
|
||||
h.LoadProgramNoTerm(prog);
|
||||
h.Run();
|
||||
h.ExpectGpr64(reg::v0, 9ull);
|
||||
EXPECT_EQ(recEeBlockGuestSize(kProgPc), 18u); // 8 fused pairs + terminal pair
|
||||
}
|
||||
|
||||
// The S1×S2 composition prize: a loop whose body contains a forward
|
||||
// conditional used to be split into two blocks — now it is ONE self-loop
|
||||
// superblock and SL-1 makes it loop-resident. Both body paths iterate.
|
||||
TEST(EeRecSuperblock, LoopWithInternalForwardBranchBecomesResident)
|
||||
{
|
||||
for (const u64 skip : {0ull, 1ull})
|
||||
{
|
||||
ResetRecAndPageProtection();
|
||||
EeRecTestHarness h;
|
||||
h.SetGpr64(reg::t0, 0);
|
||||
h.SetGpr64(reg::t1, 5);
|
||||
h.SetGpr64(reg::t2, 0);
|
||||
h.SetGpr64(reg::t3, skip);
|
||||
h.LoadProgramNoTerm({
|
||||
// 0x00 loop:
|
||||
ADDIU(reg::t0, reg::t0, 1), // idx0: iteration count
|
||||
BNE(reg::t3, reg::zero, 2), // idx1: forward → idx4 (skip the add)
|
||||
NOP, // idx2 ds
|
||||
ADDIU(reg::t2, reg::t2, 1), // idx3: only when !skip
|
||||
ADDIU(reg::t1, reg::t1, -1), // idx4: merge
|
||||
BNE(reg::t1, reg::zero, -6), // idx5: → 0x00
|
||||
NOP, // idx6 ds
|
||||
J(kPark), NOP,
|
||||
});
|
||||
h.Run();
|
||||
h.ExpectGpr64(reg::t0, 5ull);
|
||||
h.ExpectGpr64(reg::t1, 0ull);
|
||||
h.ExpectGpr64(reg::t2, skip ? 0ull : 5ull);
|
||||
EXPECT_EQ(recEeBlockGuestSize(kProgPc), 7u) << "loop body not fused";
|
||||
EXPECT_TRUE(recEeBlockIsLoopResident(kProgPc)) << "fused loop not resident";
|
||||
}
|
||||
}
|
||||
|
||||
// SMC anywhere in the fused range must invalidate the whole superblock: a
|
||||
// rewrite AFTER the continuation branch changes the recompiled semantics.
|
||||
TEST(EeRecSuperblock, SmcInFusedRegionRecompiles)
|
||||
{
|
||||
EeRecTestHarness h;
|
||||
h.SetGpr64(reg::t0, 0);
|
||||
h.LoadProgramNoTerm({
|
||||
BNE(reg::t0, reg::zero, 2), // idx0
|
||||
NOP,
|
||||
ADDIU(reg::t1, reg::zero, 5), // idx2
|
||||
ADDIU(reg::t2, reg::zero, 7), // idx3 ← rewritten below
|
||||
J(kPark), NOP,
|
||||
});
|
||||
h.Run();
|
||||
h.ExpectGpr64(reg::t2, 7ull);
|
||||
|
||||
h.TriggerSmc(kProgPc + 3 * 4, ADDIU(reg::t2, reg::zero, 0x55));
|
||||
|
||||
h.SetGpr64(reg::t0, 0);
|
||||
h.Run(EeRecTestHarness::RunMode::PreserveCache);
|
||||
h.ExpectGpr64(reg::t2, 0x55ull);
|
||||
}
|
||||
|
||||
// Store/load traffic straddling a continuation site: inline fastmem stores
|
||||
// publish before the branch, loads after it read them, both paths.
|
||||
TEST(EeRecSuperblock, MemoryTrafficAcrossContinuation)
|
||||
{
|
||||
for (const u64 cond : {0ull, 1ull})
|
||||
{
|
||||
EeRecTestHarness h;
|
||||
h.SetGpr64(reg::t0, cond);
|
||||
h.SetGpr64(reg::t4, kDataAddr);
|
||||
h.SetGpr64(reg::t1, 0x1234);
|
||||
h.TrackMemWindow(kDataAddr, 8);
|
||||
h.LoadProgramNoTerm({
|
||||
SW(reg::t1, 0, reg::t4), // idx0
|
||||
BNE(reg::t0, reg::zero, 2), // idx1 → idx5
|
||||
NOP,
|
||||
ADDIU(reg::t1, reg::t1, 1), // idx3: only when !cond
|
||||
NOP, // idx4
|
||||
LW(reg::t2, 0, reg::t4), // idx5: reads the pre-branch store
|
||||
SW(reg::t1, 4, reg::t4),
|
||||
J(kPark), NOP,
|
||||
});
|
||||
h.Run();
|
||||
h.ExpectGpr64(reg::t2, 0x1234ull);
|
||||
EXPECT_EQ(h.ReadU32(kDataAddr + 4), cond ? 0x1234u : 0x1235u);
|
||||
}
|
||||
}
|
||||
@@ -256,6 +256,13 @@ void EeRecTestHarness::LoadProgramNoTerm(std::initializer_list<u32> instructions
|
||||
LoadProgramImpl(instructions, /*append_term=*/false);
|
||||
}
|
||||
|
||||
void EeRecTestHarness::LoadProgramNoTerm(const std::vector<u32>& instructions)
|
||||
{
|
||||
program_words_ = instructions;
|
||||
for (size_t i = 0; i < program_words_.size(); ++i)
|
||||
memWrite32(kProgramPc + static_cast<u32>(i * 4), program_words_[i]);
|
||||
}
|
||||
|
||||
void EeRecTestHarness::SeedEntryState()
|
||||
{
|
||||
cpuRegs.GPR.n.ra.UD[0] = static_cast<s64>(static_cast<s32>(kParkingPc));
|
||||
|
||||
@@ -108,6 +108,7 @@ public:
|
||||
void LoadProgram(std::initializer_list<u32> instructions);
|
||||
void LoadProgram(const std::vector<u32>& instructions);
|
||||
void LoadProgramNoTerm(std::initializer_list<u32> instructions);
|
||||
void LoadProgramNoTerm(const std::vector<u32>& instructions);
|
||||
|
||||
// ---- Execute ----
|
||||
|
||||
|
||||
Reference in New Issue
Block a user