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.
This commit is contained in:
Brian Degenhardt
2026-07-29 20:28:41 -07:00
parent 9b1f9992b8
commit efeab3d35b
@@ -640,6 +640,20 @@ void EeRecTestHarness::SeedVu0Microprogram(u32 byte_offset, std::initializer_lis
std::memcpy(vu.Micro + ((base + 4) & mask), &p.upper, 4);
base += 8;
}
// Architectural E-bit cleanup executes one more pair after the E-bit
// pair. Micro mem is shared, never-reset global state, so leaving that
// delay slot unseeded executes whatever pair a previous test left there
// — order-dependent poison under --gtest_shuffle, and invisible to the
// JIT-vs-interp diff because both engines faithfully run the same stale
// word and agree. Mirror VuTestHarness::LoadProgram: when the caller's
// final pair ends the program, seed the delay slot with a NOP pair.
if (pairs.size() != 0 && ((pairs.end() - 1)->upper & vu::bits::E))
{
const vu::VuOp nop = vu::NopPair();
std::memcpy(vu.Micro + ((base + 0) & mask), &nop.lower, 4);
std::memcpy(vu.Micro + ((base + 4) & mask), &nop.upper, 4);
}
}
namespace {