tests: cover every write-lane subset of the masked VIF unpack store

A masked unpack does not store its quadword with one instruction. doMaskWrite
picks, from a sixteen-way switch, a hand-written sequence touching only the
lanes that cycle actually writes, and those sequences differ in kind rather
than just in offset: a 64-bit store for X+Y, a 64-bit lane store for Z+W,
per-lane stores at hand-computed byte offsets for the scattered subsets, and a
post-indexed pair for Y+Z. Each is its own chance to name the wrong lane.

Only the three-lane subsets were reached. Measured, not assumed: of the sixteen
cases, 7/11/13/14 executed and the other twelve had zero counts, because the
existing mixed-mask cases happen to protect exactly one lane apiece.

The subset is selected by which lanes carry the write-protect code, so ten new
cases -- one per unreached subset -- name three protected lanes to reach a
single-lane store and two to reach a pair. Protected lanes must come back
holding the fill pattern while written lanes hold unpacked data, so a sequence
that stores to a neighbouring lane fails on both halves at once. Two more cross
the selector with a mode, where the mode merge runs on a partial lane set
rather than the whole register.

Validated by mutation, each bounded to exactly the predicted set: swapping the
Z lane for W in the Y+Z sequence fails write_yz and write_yz_mode1 and nothing
else; moving the single-lane Z store from offset 8 to 4 fails write_z alone.

The remaining two switch arms stay unreached and are unreachable, which the new
absolute test pins from the other side. A fully write-protected cycle is
dropped by ProcessMasks before any store is emitted, so the "no lanes" arm is
guarded, not exercised; the differential case for it would pass whatever the
generator did, since it only has to agree with an oracle that also writes
nothing. FullyProtectedBlockWritesNothing asserts the fact itself -- VU memory
byte-identical to the fill pattern. The all-lanes arm is likewise dead: the
caller emits a plain full-width store when no lane is protected.

1705 tests, 1703 pass, 2 pre-existing skips.
This commit is contained in:
Brian Degenhardt
2026-07-28 15:24:36 -07:00
parent 7a5ed084c4
commit a88b4aa89c
@@ -432,6 +432,41 @@ const UnpackCase kCases[] = {
{"vif0_masked", 0, V4_32, true, 0, 4, 1, 1, 0, 0xE4B11B4Eu, 0x000, 0},
{"vif0_mode2", 0, V4_32, false, 0, 4, 1, 1, 2, 0, 0x000, 0},
{"vif0_skip", 0, V4_32, false, 0, 8, 2, 1, 0, 0, 0x000, 0},
// 8. Every write-lane subset, one case each.
//
// The dynarec does not store a masked quadword with one instruction. It
// selects, from a sixteen-way switch, a hand-written sequence that touches
// only the lanes this cycle actually writes — and those sequences differ
// in kind, not just in offset: a full-width store for X+Y, a 64-bit lane
// store for Z+W, per-lane stores with hand-computed byte offsets for the
// scattered subsets, and a post-indexed pair for Y+Z. Each one is an
// independent opportunity to name the wrong lane or the wrong offset.
//
// The subset is chosen by which lanes carry the write-protect code, so a
// mask naming three protected lanes reaches the single-lane store. The
// cases already above happen to land only on the three-lane subsets;
// these reach the other ten. Protected lanes must come back holding the
// fill pattern and written lanes the unpacked data, so a sequence that
// stores to a neighbouring lane fails on both halves at once.
{"write_x", 1, V4_32, true, 0, 4, 1, 1, 0, 0xFCFCFCFCu, 0x000, 0},
{"write_y", 1, V4_32, true, 0, 4, 1, 1, 0, 0xF3F3F3F3u, 0x000, 0},
{"write_z", 1, V4_32, true, 0, 4, 1, 1, 0, 0xCFCFCFCFu, 0x000, 0},
{"write_w", 1, V4_32, true, 0, 4, 1, 1, 0, 0x3F3F3F3Fu, 0x000, 0},
{"write_xy", 1, V4_32, true, 0, 4, 1, 1, 0, 0xF0F0F0F0u, 0x000, 0},
{"write_xz", 1, V4_32, true, 0, 4, 1, 1, 0, 0xCCCCCCCCu, 0x000, 0},
{"write_xw", 1, V4_32, true, 0, 4, 1, 1, 0, 0x3C3C3C3Cu, 0x000, 0},
{"write_yz", 1, V4_32, true, 0, 4, 1, 1, 0, 0xC3C3C3C3u, 0x000, 0},
{"write_yw", 1, V4_32, true, 0, 4, 1, 1, 0, 0x33333333u, 0x000, 0},
{"write_zw", 1, V4_32, true, 0, 4, 1, 1, 0, 0x0F0F0F0Fu, 0x000, 0},
// The same selector again with a mode active, because doMaskWrite folds the
// mode merge and the mask merge into one lane computation before choosing
// the store: with lanes protected, the mode merge runs on a partial lane set
// rather than the whole register, which is a different code path from the
// unmasked mode cases in group 3.
{"write_yz_mode1", 1, V4_32, true, 0, 4, 1, 1, 1, 0xC3C3C3C3u, 0x000, 0},
{"write_x_mode2", 1, V4_32, true, 0, 4, 1, 1, 2, 0xFCFCFCFCu, 0x000, 0},
};
INSTANTIATE_TEST_SUITE_P(
@@ -486,4 +521,34 @@ TEST(VifUnpackWrap, OversizedTransferFallsBackToInterpreter)
ExpectMemEqual(c, expected, dyn, "dynarec wrap fallback");
}
// A cycle whose four lanes are all write-protected has nothing to store, and the
// compiler drops the whole body for it rather than emitting a store of no lanes.
// The differential case above (mask_all_protect) would pass either way, since a
// generator that wrote something wrong and an oracle that wrote nothing only have
// to agree with each other — and here they cannot, because the oracle writes
// nothing at all. So this asserts the absolute fact instead: VU memory comes back
// byte-identical to the fill pattern.
//
// It also pins the guard behind it. The store selector has a case for "no lanes"
// that only logs an error, and it is reachable only if this skip stops working.
TEST(VifUnpackMask, FullyProtectedBlockWritesNothing)
{
ASSERT_TRUE(recompiler_tests::RecompilerTestEnvironment::IsReady());
EnsureVifUnpackReady();
UnpackCase c{"all_protect", 1, V4_32, true, 0, 8, 1, 1, 0, 0xFFFFFFFFu, 0x000, 0};
const std::vector<u8> data = MakeSourceData(static_cast<size_t>(c.num) * nVifT[V4_32]);
// The pattern RunPath seeds before it dispatches, to compare against.
FillVuMemPattern(c.idx);
std::vector<u8> untouched(VuMemSize(c.idx));
std::memcpy(untouched.data(), VuMem(c.idx), untouched.size());
const RunResult dyn = RunPath(c, data, Path::Dynarec);
ASSERT_EQ(untouched.size(), dyn.mem.size());
EXPECT_EQ(0, std::memcmp(untouched.data(), dyn.mem.data(), untouched.size()))
<< "a fully write-protected unpack must leave VU memory alone";
}
} // namespace