Skip to content

fix(vu0): discard macro-mode writes that name vf0 as the destination - #201

Draft
smmathews wants to merge 1 commit into
ran-j:mainfrom
smmathews:feature/31-vf0-codegen-write-discard
Draft

fix(vu0): discard macro-mode writes that name vf0 as the destination#201
smmathews wants to merge 1 commit into
ran-j:mainfrom
smmathews:feature/31-vf0-codegen-write-discard

Conversation

@smmathews

Copy link
Copy Markdown
Contributor

fix(vu0): discard macro-mode writes that name vf0 as the destination

vf0 is the VU's hardwired constant register. The code generator never checked the destination
index on a VU0 macro-mode write, so any instruction naming vf0 emitted a real store into
ctx->vu0_vf[0], at three shapes: masked-blend writers, QMTC2, and LQC2 (OPCODE_LDC2). It
resolves no observed failure.

grep -rn 'vu0_vf\[[^]]*\] *=' ps2xRecomp/src/
grep -rn 'rd == 0\|vfd == 0\|dest == 0' \
  ps2xRecomp/src/lib/vu_translator.cpp \
  ps2xRecomp/src/lib/vu_translation_helpers.cpp \
  ps2xRecomp/src/lib/instruction_translator.cpp

(the second returns nothing on the pre-fix tree)

Hardware grounding

The manual never says "writes to VF00 are discarded"; it follows from what it does.

  • VU User's Manual §3.1.1, "Floating-Point Registers": "VF00 is the constant register. Its fields
    are set to the following values: VF00x: 0.0 … VF00y: 0.0 … VF00z: 0.0 … VF00w: 1.0". §3.1.1 sits
    under "Micro Mode Register Set", and §5.1.1 under "Macro Mode Register Set" binds it: "The VU0
    floating-point registers are allocated to the COP2 data register. The register fields etc. are the
    same as those in the micro mode. For details, see '3.1.1. Floating-Point Registers'."
  • §4.2, SUB Remarks: "When VF00 is specified as the destination, the instruction is used to
    compare VF[fs] with VF[ft]." Chapter 6's VSUB page binds this to the COP2 instruction patched
    here: "Same as the micro instruction SUB. Refer to '4.2. Upper Instruction Reference'."
  • §5.4.2, "Hazards in Macro Mode", under "Data hazard": "VF00 and VI00 are not subject to hazard
    checks."

The fix

The destination register is a field of the decoded instruction, so the guard costs nothing in
generated code. Write sites differ in what else the statement carries, so the discard takes
different shapes; the rule is the same in each — remove the register write and nothing else.

  • // NOP and "" for r0, where nothing else is at stake.
  • The full computation with only the register store dropped, for SPECIAL_MULT and
    SPECIAL_MULTU, whose lo/hi updates must survive a zero destination — the existing r0
    precedent for the case below.
  • A zeroed mask for the masked-blend family, whose statement also carries the VLQI/VLQD pointer
    update and the accumulator assignment. The manual puts LQI's VI[is] = VI[is] + 1 outside every
    field guard, and Chapter 6's VLQI page binds the macro form: "Same as the micro instruction LQI.
    Refer to '4.3. Lower Instruction Reference'." Suppressing the statement would delete that update.
  • An early return for QMTC2, which carries nothing else.
  • A (void) discard for LQC2, whose READ128 routes to runtime->Load128() for special
    addresses: the access is observable, only the loaded value is discarded.

codegen::vuVfDestMask sits beside codegen::vuMaskExpr in codegen_helpers.h; code_generator.h
is untouched.

Testing

Tests assert on emitted text; there is no compile-and-run harness here. The helper is pinned over
its whole reachable domain. Every VU0_S1_*/VU0_S2_* opcode parsed out of instructions.h is
then driven at every field mask, against a shape declared in the test rather than read back off an
emission: the site emits a statement, it carries the write masks its opcode carries, and it writes
the VF register its opcode names and no other — the destination read exhaustively across all
thirty-two VF registers, not probed at a few. The opcode list is itself held against a count that
matches an enumerator however it is initialised, so it fails rather than silently shrinking. On top
of that anchor, two sweeps pin that the mask a site emits is the instruction's own field mask, that
a vf0 destination empties that mask and changes nothing else, and that neither depends on any
operand but the destination. QMTC2 and LQC2 each get a suppresses / still-writes pair run over
the operand space; the LQC2 pair also runs over the memory-path table — the MMIO flag, the
caller's resolved-address hint and the address, including rows where the two disagree — and both
halves assert the whole emitted statement rather than a substring, so the load's form, its address
and the discard's spelling are pinned on every path. EVIDENCE.md carries the mutation table, a
claim-versus-drive table for every assertion, and what stays open.

cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS=-msse4.1 -DCMAKE_CXX_FLAGS=-msse4.1
cmake --build build -j 12
./build/ps2xTest/ps2x_tests

Blast radius

Recompiler output changes: regenerate your recompiled corpus; a runtime-only rebuild changes
nothing. Output for a non-vf0 destination is byte-identical, so the diff is exactly the vf0
sites. Complementary to #187, which fixes the initial value of vu0_vf[0] at R5900Context
construction — disjoint files, either order, neither alone sufficient.

What this does not fix

  • No VU0 macro-mode MAC/status/Z/S flag model exists here. A vf0-destination arithmetic op is a
    comparison idiom on hardware; this runtime produces no flags for it either way.
  • vi0, hardwired to 0 by §3.1.2 of the same manual, is the same defect class, left for its own
    change: disjoint write sites, bare ctx->vi[...] assignments with no shared mask helper, several
    discarding the register write while keeping a memory effect.
  • VU1's microprogram interpreter has separate vf[0] handling and is untouched.
  • translateVU_VRNEXT never writes VF[ft] though the manual's RNEXT does; that gap predates
    this change.
Evidence — mutation table, claim-vs-drive audit, and reproduction commands

Evidence

Durable claims, each paired with the command that reproduces it. Run from the repository root on a
clean checkout of this branch. The corpus block is a recipe, not a reproduction: it needs a config
and an executable you supply.

Build

cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS=-msse4.1 -DCMAKE_CXX_FLAGS=-msse4.1
cmake --build build -j 12
./build/ps2xTest/ps2x_tests

The run ends with Failed: 0. Three tests here are timing-dependent and fail intermittently,
independently of this change; re-run before treating one as a result: wake handoff lets a contending guest thread acquire before returning
(ps2xTest/src/ps2_runtime_expansion_tests.cpp:407), sceMpegGetPicture releases an old waiter when the CD stream restarts (ps2xTest/src/ps2_runtime_expansion_tests.cpp:1140), sceGsSyncV waits on VBlank and reports interlaced field parity (ps2xTest/src/ps2_gs_tests.cpp:3346).

The defect existed, unguarded, before this change

Claim: on the pre-fix tree, every VU0 macro-mode instruction that writes a vector register does so
with no check on the destination index. Same write-site count on main as here; the guard search
returns nothing on main.

git show main:ps2xRecomp/src/lib/vu_translation_helpers.cpp \
     main:ps2xRecomp/src/lib/vu_translator.cpp \
     main:ps2xRecomp/src/lib/instruction_translator.cpp > /dev/null # sanity: files exist on main
git grep -n 'vu0_vf\[[^]]*\] *=' main -- ps2xRecomp/src | grep -v '^\S*:\s*//' | wc -l
git grep -n 'rd == 0\|vfd == 0\|dest == 0' main -- \
  ps2xRecomp/src/lib/vu_translator.cpp \
  ps2xRecomp/src/lib/vu_translation_helpers.cpp \
  ps2xRecomp/src/lib/instruction_translator.cpp

Every write site is guarded on this branch

The first block returns the pre-existing OPCODE_ADDI and OPCODE_ADDIU guards plus the new
OPCODE_LDC2 one. The second pair returns the identical set of lines. The third returns the
dest_mask assignments that skip the helper: the accumulator "A" family and the VSQI/VSQD
stores, none of which names a VF register as its destination. Read the split off these commands
rather than from any number written here.

grep -c 'codegen::vuVfDestMask(' ps2xRecomp/src/lib/vu_translation_helpers.cpp
grep -n 'rd == 0' ps2xRecomp/src/lib/vu_translator.cpp
grep -n 'inst.rt == 0' ps2xRecomp/src/lib/instruction_translator.cpp
grep -rn 'codegen::vuVfDestMask(' ps2xRecomp/src/lib/
grep -rn 'uint8_t dest_mask = codegen::vuVfDestMask' ps2xRecomp/src/lib/
grep -rn 'uint8_t dest_mask = inst.vectorInfo.vectorField;' ps2xRecomp/src/lib/

The three inline S2 cases guard inst.rt, not inst.rd

Claim: in VU0_S2_VABS, VU0_S2_VMOVE and VU0_S2_VMR32 the destination is inst.rt, the source
inst.rd. Carried by SH, whose declared shape table names vfDestSlot = 2 — the rt slot — for
all three, and which runs every opcode parsed out of instructions.h rather than a sampled one. The
pre-existing "VU0 S2 vector ops use rd as source and rt as destination" is not cited here: it
drives VABS alone, so it cannot carry a claim about three opcodes.

grep -n 'VU0_S2_VABS\|VU0_S2_VMOVE\|VU0_S2_VMR32' -A4 ps2xRecomp/src/lib/vu_translator.cpp | grep 'vuVfDestMask'
grep -n 'VU0_S2_VABS' -A8 ps2xTest/src/code_generator_tests.cpp | grep 'return {'

Non-vf0 generated output is unchanged

Claim: for a destination other than vf0, output is byte-identical to before this change.
codegen::vuVfDestMask(vf_dest_reg, dest_mask) returns dest_mask unchanged whenever
vf_dest_reg != 0, and the QMTC2 and LDC2 guards branch only at destination 0.

The in-suite carriers are SW's absolute assertion — that a non-vf0 destination still emits the
instruction's own field mask, run at every parsed opcode and every field mask — and
LQC2 to a non-zero vf still writes, which holds the whole emitted statement on every memory path.
Neither is a single-opcode test.

./build/ps2xTest/ps2x_tests 2>/dev/null | grep 'a vf0 destination changes only that mask'
./build/ps2xTest/ps2x_tests 2>/dev/null | grep 'LQC2 to a non-zero vf still writes'

OPCODE_LDC2 and translateVU_VRGET were restructured, so byte-identity was re-established for
them with a throwaway driver (not committed) diffing translateInstruction output against a main
build, rt and rs swept 0..31: every differing row had rt == 0, and every rt == 0 row
differed.

grep -n 'case OPCODE_LDC2' -A6 ps2xRecomp/src/lib/instruction_translator.cpp

The in-suite counterpart is stronger than an address substring: both LQC2 cases hold the whole
emitted statement
against an expected string, on every one of the thirteen memory-path rows. The
expected load for each row is carried in the table itself rather than recomputed from the row's own
members, because a mirror of the branch logic would agree with a generator that took the wrong
branch. On the one row where neither the MMIO flag nor the caller's hint is set, the expected load is
built from the fixture's own base register and immediate rather than written as a literal.

grep -n 'expectedLoad' ps2xTest/src/code_generator_tests.cpp
grep -n 'lqc2ExpectedLoad(path, inst)' ps2xTest/src/code_generator_tests.cpp

Tests added

In ps2xTest/src/code_generator_tests.cpp, registered inside register_code_generator_tests().
"VU0 macro mappings cover all S1/S2 enums" was rewritten onto the shared vu0MacroOps /
makeVu0MacroOp helpers with its marker assertion unchanged and a non-empty assertion added beside
it, because the marker check is a negative that an empty emission also satisfies.

  • vuVfDestMask yields an empty mask for a vf0 destination
  • vuVfDestMask passes the field mask through for every non-vf0 destination
  • every VU0 macro site emits the write masks and VF destination its opcode declares (its
    expectation is declared in the test — a per-opcode write-mask count and destination slot read off
    the dispatch — rather than derived from an emission the mutation under test can also empty)
  • every VU0 macro write site emits its own field mask, and a vf0 destination changes only that mask
    (count-free sweep over every VU0_S1_* / VU0_S2_* enum parsed out of instructions.h, run at
    every field mask the decoder can produce)
  • VU0 non-zero destination keeps its write mask — a one-point regression spot-check, not the
    general property its name suggests: one opcode, one destination index, one field mask. The general
    form is carried by SW and MD, which walk every parsed opcode and every field mask
  • QMTC2 to vf0 emits no vf0 write
  • QMTC2 to a non-zero vf still writes
  • VU0 macro emission does not depend on the post-decode MMIO fields
  • QMTC2 emission does not depend on the post-decode MMIO fields
  • LQC2 to vf0 emits no vf0 write
  • LQC2 to vf0 still performs the load
  • LQC2 to a non-zero vf still writes
  • VU0 macro write masks depend only on the destination index and the field mask

The shared vu0MacroOps helper now asserts that its opcode list is complete, not merely
non-empty. parseEnumValues only matches a hexadecimal initialiser, so an enumerator added with a
decimal or symbolic one would drop silently out of every sweep that walks the list. It is held
against a second count that matches an enumerator however it is initialised. No number is written
down on either side, so adding an opcode touches neither.

grep -n 'parseEnumValues(text, "VU0_S1_")\|parseEnumValues(text, "VU0_S2_")' ps2xTest/src/code_generator_tests.cpp
grep -n 'countEnumDeclarations' ps2xTest/src/code_generator_tests.cpp

The mask helper is pinned over its whole reachable domain

Claim: codegen::vuVfDestMask is not merely sampled at one index and one mask. The VF register
index reaching it comes from the five-bit SA/RD/RT extractions in instructions.h, and the
field mask from (raw >> 21) & 0xF in r5900_decoder.cpp, so its reachable domain is 0..31 by
0..15 and the two added tests walk all of it: a vf0 destination yields an empty mask for every
field mask, and every other destination returns its field mask unchanged.

The unfiltered form of the field-mask grep also returns the decode-time default that precedes it,
which sets the same field to all-xyzw; both values are inside the same four-bit domain, so the
domain claim is the same either way.

grep -n 'define SA(\|define RD(\|define RT(' ps2xRecomp/include/ps2recomp/instructions.h
grep -n "vectorInfo.vectorField = static_cast<uint8_t>((inst.raw >> 21) & 0xF)" ps2xRecomp/src/lib/r5900_decoder.cpp
grep -n 'vuVfDestMask' ps2xTest/src/code_generator_tests.cpp

How the fixtures are built

Each instruction is built the way a recompile builds one: operands encoded into a word, the word
decoded. One slot is the exception. A Special2 word has no shift-amount operand — those bits carry
its function code — so the sweep sets that slot after the decode and drives it as a third probe,
which catches a helper applied to an operand that is not the site's VF destination. Construction
does not close that slot; the sweep walks it over every value a five-bit field can hold instead, at
every field mask, both where the site has a VF destination and where it has none. The value the
decoder gives the slot is inside that domain. The first command below returns the single post-decode
fixup, the second the assertions that close it.

grep -n 'inst.sa = sa;' ps2xTest/src/code_generator_tests.cpp
grep -n 'no vf destination must emit' ps2xTest/src/code_generator_tests.cpp

The MMIO flag and MMIO address are not decoder-determined: the recompiler assigns them after
decoding from an address-keyed configuration map with no opcode filter. Both are driven at each
guarded shape — held invariant where the emission does not read them, and at LQC2, where it does,
swept over the cross-product of the flag, the caller's resolved-address hint and the address,
including rows where the two disagree. The sweep below returns those two assignments and four fields
of a synthetic delay-slot NOP, which cannot reach a guarded site.

for m in $(sed -n '/struct Instruction$/,/^    };$/p' ps2xRecomp/include/ps2recomp/types.h \
           | grep -oE '(uint(8|16|32|64)_t|bool|std::string) +[A-Za-z][A-Za-z0-9]*' \
           | awk '{print $2}' | sort -u); do
  git grep -nE "(^|[^A-Za-z0-9_])inst(\.|->)([A-Za-z][A-Za-z0-9]*\.)?${m}[[:space:]]*=[^=]" \
    -- ps2xRecomp/src ps2xAnalyzer/src ps2xRuntime/src \
    | grep -v r5900_decoder.cpp | sed "s/^/[$m] /"
done

Checking a corpus for these write sites

A recipe, not a reproduction. Claim, conditional: if an executable contains VU0 macro-mode
instructions naming vf0 as their destination, recompiling it emits them and these find them. Use a
main build, where no guard exists and all three shapes assign ctx->vu0_vf[0]. An empty first
result means that executable has no such instructions, which the claim leaves open.

./build/ps2xRecomp/ps2_recomp <your-config.toml>
# every VU0 macro-mode write into vf0: masked blends, QMTC2 and LQC2 alike
grep -rn 'ctx->vu0_vf\[0\] = ' <your generated output directory>
# the same lines minus the masked blends whose write mask is already empty
grep -rn 'ctx->vu0_vf\[0\] = ' <your generated output directory> | grep -v '_mm_set_epi32(0, 0, 0, 0)'

On this branch a QMTC2 naming vf0 becomes a comment and an LQC2 naming vf0 becomes a
(void) discard, so neither emits ctx->vu0_vf[ text. Each shape needs its own pattern:

# masked blends, now every one of them with an empty mask
grep -rn 'ctx->vu0_vf\[0\] = ' <your generated output directory>
# QMTC2
grep -rnF '// QMTC2 to $vf0 discarded' <your generated output directory>
# LQC2, on each of the three forms its load can take
grep -rnE '\(void\)(READ128|FAST_READ128|runtime->Load128)\(' <your generated output directory>

These three patterns are recipe text, and a recipe is only as good as the emission it expects. The
shape of each is pinned in the suite, so a generator that stopped producing it fails a test rather
than leaving a grep silently empty:

  • The LQC2 pattern — all three load forms and the (void) spelling — is pinned by
    LQC2 to vf0 still performs the load, which drives all thirteen memory-path rows and holds the
    whole emitted statement against an expected string on each. That covers the runtime loader, the
    fast read and the bare read, so the alternation above is matched form for form. Note that
    READ128( is a substring of FAST_READ128(, so no single substring assertion can tell the three
    apart; only whole-statement equality does.
  • The QMTC2 comment text is pinned by QMTC2 to vf0 emits no vf0 write, which now asserts the
    emitted comment itself and not only the absence of a ctx->vu0_vf[ write. An empty emission also
    satisfies an absence check, so the absence check alone could not carry this pattern.
  • The masked-blend pattern is pinned by SW, which asserts that a vf0 destination empties the
    mask and changes nothing else, at every parsed opcode and every field mask.

What each assertion claims and what it drives

A test's name states a domain; its body drives one. Where the two differ, the difference is a gap
whether or not anyone has noticed it, and every gap found so far in this change was found by reading
test bodies against test names by hand. This table is that reading, written down: for each assertion
that bears on this change, the domain its claim covers, the domain its body drives, and whether
any difference is closed or disclosed. Check it against the bodies rather than re-deriving it.

Each row names the assertion in full, so this table can be checked without the abbreviation key
used by the mutation table below.

assertion domain claimed domain driven status
LQC2 to vf0 still performs the load the load survives the discard, at every form the load can take all thirteen memory-path rows × both disassembly settings × rs 0..31 × five immediates; whole emitted statement held against an expected string closed. Previously drove the bare-READ128 form only — one of three forms, one of thirteen rows — and asserted two substrings, one of which (READ128() is a substring of FAST_READ128( and so could not discriminate the form even where that form was driven
LQC2 to a non-zero vf still writes the write happens and addresses rs plus the instruction's own immediate all thirteen rows × rt 1..31 × rs 0..31 × five immediates; whole emitted statement closed. The address used to be asserted only where neither the flag nor the hint was set — one row of thirteen; on the other twelve the write was asserted and nothing about what it loaded or from where
the (void) discard spelling the corpus recipe greps for it, so the recipe depends on the emission carrying it the exact-equality assertion in the load test includes the (void) and the trailing semicolon closed. Nothing previously asserted (void) anywhere in the suite
QMTC2 to vf0 emits no vf0 write a vf0 QMTC2 emits no write, and (for the recipe) emits the discard comment every source register × both disassembly settings × four encoding low-bit values; absence of ctx->vu0_vf[, plus exact equality against the comment text closed. The comment text was pinned nowhere; an empty emission satisfies an absence check just as well
QMTC2 to a non-zero vf still writes the write happens and carries the source register the instruction names every destination 1..31 × every source register 0..31 × both disassembly settings × four encoding low-bit values; the asserted string is the entire emission, since this translator emits one statement no gap on the operand grid; the encoding low bits are sampled, as noted below
VU0 macro mappings cover all S1/S2 enums (COV), via the shared opcode list "every VU0_S1_* / VU0_S2_* enum" every enumerator the parse yields, held against a count that matches an enumerator however it is initialised closed. The parse matches only a hexadecimal initialiser, and only non-emptiness was asserted, so an enumerator added with a decimal or symbolic initialiser would have dropped out of five sweeps silently
every VU0 macro site emits the write masks and VF destination its opcode declares (SH), destination half — "writes the VF destination its opcode names, and no other" all thirty-two VF registers every ctx->vu0_vf[N] = assignment in the emission, collected by index and compared as a whole against the declared destination closed. Previously probed three indices, leaving the claim overstated by the other twenty-nine
every VU0 macro site emits the write masks and VF destination its opcode declares (SH), mask-count half every opcode, every field mask every parsed opcode × field 0..0xF, at one operand triple disclosed — one operand triple
every VU0 macro write site emits its own field mask, and a vf0 destination changes only that mask (SW) every write site; a vf0 destination changes only the mask every parsed opcode × field 0..0xF × three zeroing passes, anchored against SH so its loops cannot be vacuous disclosed — destination index at two values
VU0 macro write masks depend only on the destination index and the field mask (MD) masks depend only on destination and field every opcode × field × slot × 0..31, one slot at a time disclosed — slots never crossed
vuVfDestMask yields an empty mask for a vf0 destination (H1) and vuVfDestMask passes the field mask through for every non-vf0 destination (H2) vf0 at every mask; every non-vf0 destination at every mask 0 × {0..15}; {1..31} × {0..15} — the whole reachable five-bit by four-bit domain no gap
VU0 macro emission does not depend on the post-decode MMIO fields (MMIO) and QMTC2 emission does not depend on the post-decode MMIO fields emission is independent of the post-decode MMIO fields the declared reachable states, with an explicit non-emptiness anchor disclosed — MMIO address sampled
LQC2 to vf0 emits no vf0 write a vf0 LQC2 emits no write all thirteen rows × both disassembly settings × rs 0..31 × five immediates no gap
VU0 non-zero destination keeps its write mask reads as a general property one opcode, one destination index, one field mask disclosed — a regression spot-check; SW and MD carry the property
VU0 S1 uses fd/fs/ft fields (sa/rd/rt), VU0 S1 q/i forms keep mask and use sa as destination, VU0 S2 vector ops use rd as source and rt as destination, VU0 S2 VI memory ops use rd as VI base register each name quantifies over a family one opcode each, one operand set, one field mask disclosed — pre-existing and untouched here. The destination-slot half of each is carried across every opcode by SH; the source-operand half is not

Mutation discipline

For each row: the one-line production mutation was applied by hand, rebuilt with cmake --build build -j 12, and ./build/ps2xTest/ps2x_tests run to completion. The tests named in the row failed
and the rest of the suite stayed green. The file was then restored to its committed state, the
restoration verified against that state, rebuilt, and the suite reconfirmed green before the next
row.

H1 = vuVfDestMask yields an empty mask for a vf0 destination. H2 =
vuVfDestMask passes the field mask through for every non-vf0 destination. SW =
every VU0 macro write site emits its own field mask, and a vf0 destination changes only that mask.
MD = VU0 macro write masks depend only on the destination index and the field mask. SH =
every VU0 macro site emits the write masks and VF destination its opcode declares. COV =
VU0 macro mappings cover all S1/S2 enums. MMIO =
VU0 macro emission does not depend on the post-decode MMIO fields.

mutation file test(s) that fail note
codegen::vuVfDestMask body → return (vf_dest_reg <= 1) ? 0 : dest_mask; codegen_helpers.h H2 no test below the previous boundary used a destination index of exactly 1, so only the exhaustive property test discriminates this
codegen::vuVfDestMask body → return (vf_dest_reg <= 6) ? 0 : dest_mask; codegen_helpers.h H2 destinations 3, 4 and 6 do appear elsewhere in the file, but every test that uses them asserts only substring presence, not the mask value
codegen::vuVfDestMask body → return (vf_dest_reg == 0) ? 0 : (uint8_t)(dest_mask | 0xF); codegen_helpers.h H2, SW, MD H2 catches the widening directly; SW's partial-field (0x5) pass also fails because the widened mask diverges from the instruction's own field
codegen::vuVfDestMask body → return (vf_dest_reg == 0) ? 0 : (uint8_t)0xF; codegen_helpers.h H2, SW, MD same observable behaviour as the row above at every point either test samples
in translateVU_VADD_Field, change to codegen::vuVfDestMask(vfd, 0xF), dropping the instruction's own field vu_translation_helpers.cpp SW, MD invisible to H1/H2 (the helper itself is untouched) and to a mask-only sweep; only the partial-field pass exposes a site that stopped forwarding its own field mask
in translateVU_VMINI_Field, revert to uint8_t dest_mask = inst.vectorInfo.vectorField; vu_translation_helpers.cpp SW, MD
in translateVU_VMADD, insert if (vfd == 0) return ""; before the return vu_translation_helpers.cpp SW, MD, MMIO
in translateVU_VMSUB, insert if (vfd == 0) return ""; before the return vu_translation_helpers.cpp SW, MD, MMIO same shape as the VMADD row
in translateVU_VMADD_Field, insert if (vfd == 0) return ""; before the return vu_translation_helpers.cpp SW, MD, MMIO same shape as the VMADD row
in translateVU_VOPMSUB, insert if (vfd == 0) return ""; before the return vu_translation_helpers.cpp SW, MD, MMIO same shape as the VMADD row
in translateVU_VLQI, insert if (inst.rt == 0) return ""; before the return vu_translation_helpers.cpp SW, MD, MMIO same gate property, the S2 side
in translateVU_VLQD, insert if (inst.rt == 0) return ""; before the return vu_translation_helpers.cpp SW, MD, MMIO same shape as the VLQI row
in translateVU_VADDA, change to uint8_t dest_mask = codegen::vuVfDestMask(inst.sa, inst.vectorInfo.vectorField); vu_translation_helpers.cpp SW, MD VADDA has no VF destination register; the sweep catches a helper misapplied to an operand that is not this op's VF destination
in translateVU_VSQI, change to uint8_t dest_mask = codegen::vuVfDestMask(inst.rt, inst.vectorInfo.vectorField); vu_translation_helpers.cpp SW, MD VSQI's rt is a blend source, not a destination; the same misapplication class on the S2 side
in translateVU_VMAX, change the call to codegen::vuVfDestMask(vfd, inst.vectorInfo.vectorField == 0xA ? 0 : inst.vectorInfo.vectorField) vu_translation_helpers.cpp SW, MD a site that special-cases one field-mask value. Invisible to H1/H2, which pin the helper and not what a site forwards it; caught only because the sweep now runs the whole field-mask domain instead of two sampled values
in translateVU_VMINI_Field, change the call to codegen::vuVfDestMask(0, inst.vectorInfo.vectorField) vu_translation_helpers.cpp SW, MD the guard becomes unconditional: every VMINIx/y/z/w write is discarded for all 32 destinations. Invisible to both of SW's relative comparisons, because it corrupts the reference and the mutated emission identically; caught by the absolute assertion that a non-vf0 destination must still carry the instruction's own field mask
in translateVU_VRGET, change the call to codegen::vuVfDestMask(0, inst.vectorInfo.vectorField) vu_translation_helpers.cpp SW, MD same shape on an S2 op whose destination is rt
in case VU0_S2_VABS, change the call to codegen::vuVfDestMask(0, inst.vectorInfo.vectorField) vu_translator.cpp SW, MD same shape at an inline S2 case
in translateVU_VADDA, change to uint8_t dest_mask = codegen::vuVfDestMask(0, inst.vectorInfo.vectorField); vu_translation_helpers.cpp SW, MD the same unconditional-guard shape at a site with no VF destination at all: the accumulator blend mask goes dead everywhere
in codegen::vuVfDestMask, change the body to return 0; codegen_helpers.h H2, SW, MD, VU0 non-zero destination keeps its write mask every guarded site then emits a dead mask even for a non-vf0 destination, which SW's absolute reference-emission assertion rejects at the first opcode it walks. H2 fires at the first non-zero destination, and the non-vf0 regression test fires on its own live-mask literal.
in case COP2_QMTC2, guard → rd == 0 && (inst.function & 1) == 0 vu_translator.cpp QMTC2 to vf0 emits no vf0 write the guard conditioned on the interlock bit. Bit 0 of the encoding selects the interlocked form, so this restores the vf0 write for every interlocked QMTC2 a real recompile produces
in case COP2_QMTC2, guard → rd == 0 && inst.vectorInfo.vectorField == 0 vu_translator.cpp QMTC2 to vf0 emits no vf0 write conditioned on a field that is constant in real output at a value the tests used to leave at zero. The decoder default is 0xF for every conforming QMTC2, so this restores the write for every QMTC2 a recompile produces
in case COP2_QMTC2, guard → rd == 0 && inst.address == 0 vu_translator.cpp QMTC2 to vf0 emits no vf0 write conditioned on the instruction's own address, which is never zero in a real recompile
in translateVU_VADD_Field, dest_mask(vfd == 0 && inst.address == 0) ? 0 : inst.vectorInfo.vectorField vu_translation_helpers.cpp SW, MD the same address-conditioned shape at a masked-blend site
in translateVU_VMINI_Field, dest_mask(vfd == 0 && !inst.modificationInfo.modifiesVFR) ? 0 : inst.vectorInfo.vectorField vu_translation_helpers.cpp SW, MD conditioned on a decoder flag that is invariably true at this site, not one set on every VU0 macro instruction: the decoder defaults it true for macro mode and then clears it for the ops that write no VF register, and no VMINI field form is among them
in case VU0_S2_VABS, dest_mask(inst.rt == 0 && !inst.isVU) ? 0 : inst.vectorInfo.vectorField vu_translator.cpp SW, MD the same shape at an inline S2 case, on a flag the decoder sets for every VU0 macro-mode instruction
in translateVU_VLQI, dest_mask(inst.rt == 0 && inst.address == 0) ? 0 : inst.vectorInfo.vectorField vu_translation_helpers.cpp SW, MD the same address-conditioned shape at the load-with-increment site
in translateVU_VMUL_Field, dest_mask(vfd == 0 && inst.rs == COP2_CO) ? 0 : inst.vectorInfo.vectorField vu_translation_helpers.cpp SW, MD conditioned on the format field, which the encoding ties to the destination mask, so it equals COP2_CO in real output only when the mask is already empty. Fails at every non-zero field mask
in case OPCODE_LDC2, guard → inst.rt == 0 && !inst.isVU instruction_translator.cpp LQC2 to vf0 emits no vf0 write, LQC2 to vf0 still performs the load the same shape on the load, on a flag the decoder sets for every LDC2
in case COP2_QMTC2, delete the if (rd == 0) guard vu_translator.cpp QMTC2 to vf0 emits no vf0 write
in case COP2_QMTC2, change the guard condition from rd == 0 to rd < 32 vu_translator.cpp QMTC2 to a non-zero vf still writes the over-guard case: rd < 32 suppresses every valid VF register too
in case COP2_QMTC2, change the guard condition from rd == 0 to rd == 0 || rd == 20 vu_translator.cpp QMTC2 to a non-zero vf still writes a guard that catches one extra destination index. Caught only because the non-zero half now runs over every destination index rather than one sampled register
in case COP2_QMTC2, replace the source operand with the literal GPR_VEC(ctx, 9) vu_translator.cpp QMTC2 to a non-zero vf still writes the source register frozen at the value the test used to drive. It survived while that test held rt fixed; it is caught now the test crosses every source register with every destination index
in case OPCODE_LDC2, delete the if (inst.rt == 0) guard instruction_translator.cpp LQC2 to vf0 emits no vf0 write, LQC2 to vf0 still performs the load the unguarded form still performs the load at the same address, so only the no-write assertion moves
in case OPCODE_LDC2's guard, change the return to return ""; instruction_translator.cpp LQC2 to vf0 still performs the load an empty string carries no vu0_vf reference either, so the branch test cannot see it; the load test is what pins that the access survives
in case OPCODE_LDC2, change the guard condition from inst.rt == 0 to inst.rt < 32 instruction_translator.cpp LQC2 to a non-zero vf still writes same over-guard shape as the QMTC2 row
in case OPCODE_LDC2, change inst.rs to inst.rt in the hoisted address expression instruction_translator.cpp LQC2 to vf0 still performs the load, LQC2 to a non-zero vf still writes the hoist means one address expression is shared by both branches, so pinning it on either side catches this
in case OPCODE_LDC2, change inst.simmediate to 0 in the hoisted address expression instruction_translator.cpp LQC2 to vf0 still performs the load, LQC2 to a non-zero vf still writes same note as the row above
in case OPCODE_LDC2, change the guard condition from inst.rt == 0 to inst.rt == 0 || inst.rt == 20 instruction_translator.cpp LQC2 to a non-zero vf still writes same shape as the QMTC2 row, closed the same way
in case OPCODE_LDC2, replace inst.rs, inst.simmediate in the hoisted address with the literals 4, 16 instruction_translator.cpp LQC2 to vf0 still performs the load, LQC2 to a non-zero vf still writes the address frozen at the operand pair every LQC2 test used to drive. It is caught now that both of those cases walk the base register and the immediate
in case COP2_QMTC2, change the guard from rd == 0 to rd == 0 && rt == 9 vu_translator.cpp QMTC2 to vf0 emits no vf0 write the suppression conditioned on the one source register the vf0 case used to drive. Caught now that the vf0 half runs over every source register, closing the last thirty-one cells of the (rd, rt) grid
in case OPCODE_LDC2, change the guard from inst.rt == 0 to inst.rt == 0 && inst.rs == 4 instruction_translator.cpp LQC2 to vf0 emits no vf0 write, LQC2 to vf0 still performs the load the same shape on the base register. The companion load test walks rs but asserts only the read and the address, both of which the unguarded path also emits, so it never saw this
in case OPCODE_LDC2, change the guard to inst.rt == 0 && !inst.isMmio instruction_translator.cpp LQC2 to vf0 emits no vf0 write, LQC2 to vf0 still performs the load the guard conditioned on an axis no test drove. Reachable in production: MMIO is assigned per instruction address from config
in case OPCODE_LDC2, change the guard to inst.rt == 0 && !effectiveMemoryHint.hasAddress instruction_translator.cpp LQC2 to vf0 emits no vf0 write, LQC2 to vf0 still performs the load same shape on the resolved-address axis. LDC2 is a direct memory access, so a constant-folded base gives it a resolved address in a real recompile — this one would restore the vf0 write in generated output while the suite stayed green
in case COP2_QMTC2, change the guard to rd == 0 && !inst.isMmio vu_translator.cpp QMTC2 emission does not depend on the post-decode MMIO fields the same shape as the LDC2 MMIO row, at the shape where the flag was not being driven. The decoder never sets this member; the recompiler assigns it after the decode from a map keyed on instruction address with no opcode filter, so this restores the vf0 write for any QMTC2 listed there
in translateVU_VADD_Field, dest_maskinst.isMmio ? inst.vectorInfo.vectorField : codegen::vuVfDestMask(vfd, inst.vectorInfo.vectorField) vu_translation_helpers.cpp VU0 macro emission does not depend on the post-decode MMIO fields the same shape at a masked-blend site. Invisible to the field-mask sweeps, which never set the flag, and invisible to the helper property tests, which do not go through a site
in case COP2_QMTC2, change the guard to rd == 0 && inst.mmioAddress == 0 vu_translator.cpp QMTC2 emission does not depend on the post-decode MMIO fields recorded because it discriminates the address from the flag: it reproduces correct behaviour at the cleared state and at a flagged zero address, and diverges only where the address is non-zero. This is what the non-zero address state is for
in case COP2_QMTC2, change the guard to rd == 0 && !(inst.isMmio && inst.mmioAddress == 0) vu_translator.cpp QMTC2 emission does not depend on the post-decode MMIO fields the mirror, and what the flagged zero-address state is for: it diverges only there. The configuration map can carry a zero value, so that state is reachable and is not redundant with the other two
in case OPCODE_LDC2, change the guard to inst.rt == 0 && !(inst.isMmio && inst.mmioAddress == 0) instruction_translator.cpp LQC2 to vf0 emits no vf0 write, LQC2 to vf0 still performs the load the mirror of the QMTC2 row above, at the shape where the flag was being driven only alongside a non-zero address. The configuration map initialises the address to zero and stores it unconditionally, so a zero or non-numeric entry produces exactly this state
in case OPCODE_LDC2, change the guard to inst.rt == 0 && !(inst.isMmio && !memoryHint.hasAddress && inst.mmioAddress == 0) instruction_translator.cpp LQC2 to vf0 emits no vf0 write, LQC2 to vf0 still performs the load the same shape narrowed to the flagged zero address with no caller hint, which is the one product cell the row above does not isolate
in case OPCODE_LDC2, change the guard to inst.rt == 0 && !(effectiveMemoryHint.hasAddress && effectiveMemoryHint.address == 0) instruction_translator.cpp LQC2 to vf0 emits no vf0 write, LQC2 to vf0 still performs the load the same shape one member over, on the resolved address after the MMIO override is folded in. A constant-folded base with a zero offset gives a resolved address of zero in a real recompile
in case OPCODE_LDC2, change the guard to inst.rt == 0 && !(!inst.isMmio && memoryHint.hasAddress && memoryHint.address == 0) instruction_translator.cpp LQC2 to vf0 emits no vf0 write, LQC2 to vf0 still performs the load the caller's own hint at a zero address with the flag clear, read before the override rather than after
in case OPCODE_LDC2, change the guard to inst.rt == 0 && !(inst.isMmio && memoryHint.hasAddress && memoryHint.address == 0) instruction_translator.cpp LQC2 to vf0 emits no vf0 write, LQC2 to vf0 still performs the load the flag and the hint set together at a zero address. Neither of the two single-member rows above reaches this cell, because each is satisfied elsewhere in the product
in case OPCODE_LDC2, change the guard to inst.rt == 0 && !(!inst.isMmio && effectiveMemoryHint.hasAddress && Ps2IsSpecialAddress(effectiveMemoryHint.address)) instruction_translator.cpp LQC2 to vf0 emits no vf0 write, LQC2 to vf0 still performs the load conditioned on the load shape rather than on a member: a resolved address inside a special region reaches the runtime loader with the flag clear, which no ordinary main-RAM address produces
in case OPCODE_LDC2, change the guard to inst.rt == 0 && !(inst.isMmio && !memoryHint.hasAddress && Ps2IsSpecialAddress(inst.mmioAddress)) instruction_translator.cpp LQC2 to vf0 emits no vf0 write, LQC2 to vf0 still performs the load the same address class carried on the MMIO member with no caller hint
in case OPCODE_LDC2, change the guard to inst.rt == 0 && !(inst.isMmio && memoryHint.hasAddress && Ps2IsSpecialAddress(inst.mmioAddress)) instruction_translator.cpp LQC2 to vf0 emits no vf0 write, LQC2 to vf0 still performs the load the same address class with both set, the last cell of the product
in translateVU_VADD_Field, change dest_mask to (vfd == 0 && vfs == 11) ? 0 : inst.vectorInfo.vectorField vu_translation_helpers.cpp MD a mask conditioned on a source operand rather than a constant written in its place. It reproduces correct behaviour at all four operand triples the sweep drives
in translateVU_VADD_Field, change dest_mask to (vfd != 0 && vfs == 5) ? 0 : inst.vectorInfo.vectorField vu_translation_helpers.cpp SW, MD the mirror of the row above, on the non-vf0 half, taken literally as a direct replacement of the dest_mask expression. Because the replacement no longer routes the vfd == 0 case through the helper, it also corrupts the one vfd == 0 cell the pre-existing sweep already drives (sa zeroed to 0, rd at 11), so SW fails there in addition to MD — this mutation does not isolate the same way row above does; it was verified empirically rather than assumed
in case OPCODE_LDC2, change the guard to inst.rt == 0 && memoryHint.address == inst.mmioAddress instruction_translator.cpp LQC2 to vf0 emits no vf0 write, LQC2 to vf0 still performs the load a guard reading the relation between the two post-decode members rather than either one on its own. Every memory-path row that drives one address value into both members reproduces correct behaviour under it; it restores the vf0 write wherever a recompile resolves an address the configuration map does not name
in case OPCODE_LDC2, change the guard to inst.rt == 0 && !(inst.isMmio && !memoryHint.hasAddress && memoryHint.address != inst.mmioAddress) instruction_translator.cpp LQC2 to vf0 emits no vf0 write, LQC2 to vf0 still performs the load the same relation narrowed to the flag set with no caller hint, where a recompile leaves the hint's address at zero against a non-zero configured one. It fires at one memory-path row only; drop that row and it survives the suite
in case OPCODE_LDC2, change the guard to inst.rt == 0 && !(!inst.isMmio && memoryHint.hasAddress && memoryHint.address != inst.mmioAddress) instruction_translator.cpp LQC2 to vf0 emits no vf0 write, LQC2 to vf0 still performs the load the same relation with the flag clear and a caller hint, which is what an ordinary constant-folded LQC2 produces: the decoder leaves the MMIO address at zero while the resolved address is not. It fires at one memory-path row only
in case OPCODE_LDC2, change the guard to inst.rt == 0 && !(inst.isMmio && memoryHint.hasAddress && memoryHint.address != inst.mmioAddress) instruction_translator.cpp LQC2 to vf0 emits no vf0 write, LQC2 to vf0 still performs the load the same relation with both set, where the configuration map and the constant folder name different addresses. It fires at one memory-path row only
in codegen::vuVfDestMask, narrow the index parameter back to uint8_t codegen_helpers.h none recorded because it does not discriminate: every index reaching the helper comes from a five-bit decode, so no input the decoder can produce is affected
in translateVU_VADDA, dest_mask(inst.sa == 10) ? 0 : inst.vectorInfo.vectorField vu_translation_helpers.cpp MD the shift-amount slot is what the decoder puts there for every VADDA a recompile emits, and it is the one slot a fixture sets after the decode
in translateVU_VADDA, dest_mask(inst.rd == 17) ? 0 : inst.vectorInfo.vectorField vu_translation_helpers.cpp MD a source operand at a site with no VF destination, where the destination sweep has no destination to hold
in translateVU_VADDA, insert if (inst.rd == 5) return ""; before the return vu_translation_helpers.cpp MD the statement-shaped discard at a site with no VF destination, keyed on a source-operand value the zeroing sweep never drives. The mask-shaped discard at this site and this operand is recorded above; the two shapes are closed by the same walk
in translateVU_VADDA, insert if (inst.rd == 17) return ""; before the return vu_translation_helpers.cpp MD the same shape at a second value of the same operand, matching the value the mask-shaped row uses
in translateVU_VADDA, insert if (inst.rd == 0) return ""; before the return vu_translation_helpers.cpp SW, MD, MMIO the one value of that operand the zeroing sweep does drive, which is what makes the two rows above non-redundant with it
in translateVU_VSQI, insert if (inst.rt == 5) return ""; before the return vu_translation_helpers.cpp MD the same shape on the S2 side, at the store whose rt is a blend source rather than a destination
in translateVU_VADD_Field, insert if (inst.rd == 5) return ""; before the return vu_translation_helpers.cpp MD the same shape at a site that does have a VF destination, keyed on a source operand: the destination is untouched, so the emission is simply absent at one value of a slot the sweep walks
in translateVU_VADD_Field, insert if (inst.vectorInfo.vectorField == 0xF) return ""; after the field extraction vu_translation_helpers.cpp SH, SW, COV, MMIO the statement-shaped discard keyed on the field mask, which the reference emission shares. Before the declared shape existed this survived the whole suite: every sweep derived its expectation from a reference built at the same field mask, so emptying both sides left each comparison true over an empty collection. MD stays green even now — at this field value its destination-free branch is not taken, and its destination branch compares two empty mask lists
in translateVU_VADD_Field, return ""; as the first statement vu_translation_helpers.cpp SH, SW, COV, MMIO the same argument at every field value rather than one. Also survived the whole suite before the declared shape, and MD stays green for the same reason as the row above
translateVU_VRGET body → return "{ __m128 res = ctx->vu0_r; }"; vu_translation_helpers.cpp SH, SW a statement that keeps a statement but drops both the write mask and the destination store. SH fires on the declared mask count, SW on the declared destination slot — recorded because it pins that the anchor is not merely a non-emptiness check
in translateVU_VMULq, insert if (vfd == 9) return "// discarded"; before the return vu_translation_helpers.cpp SH, SW, MD a comment-only return, which is the shape this codebase already uses for its real discards. It is non-empty, so a bare non-emptiness assertion would pass it; it carries no mask literal and no destination store, so the declared shape does not
in case OPCODE_LDC2, guard → return inst.isMmio ? std::string("") : fmt::format("(void){};", load); instruction_translator.cpp LQC2 to vf0 still performs the load a 128-bit MMIO read deleted from generated output. Survived the whole suite before the load test was rewritten: that test drove only the bare-READ128 path, so it never reached this branch, and the no-write test cannot see a deletion because an empty emission carries no ctx->vu0_vf[ either
in case OPCODE_LDC2, guard → return effectiveMemoryHint.hasAddress ? std::string("") : fmt::format("(void){};", load); instruction_translator.cpp LQC2 to vf0 still performs the load the same deletion on the resolved-address axis, and the broader of the two: LDC2 is a direct memory access, so a constant-folded base gives it a resolved address in a real recompile. Also survived the whole suite before the rewrite
in case OPCODE_LDC2, guard → return fmt::format("(void)READ128(ADD32(GPR_U32(ctx, {}), {}));", inst.rs, inst.simmediate); instruction_translator.cpp LQC2 to vf0 still performs the load the discard hard-wired to the bare form, so a resolved or MMIO address silently reverts to an unresolved read. The sharpest statement of what a form-blind assertion misses
in case OPCODE_LDC2, guard → return fmt::format("{};", load); (drop the (void)) instruction_translator.cpp LQC2 to vf0 still performs the load pins the (void) spelling the corpus recipe greps for. Nothing in the suite asserted it before
in translateMemoryRead, drop the special-address test: if (inst.isMmio) in place of if (inst.isMmio || Ps2IsSpecialAddress(resolvedAddress)) instruction_translator.cpp LQC2 to vf0 still performs the load, LQC2 to a non-zero vf still writes measured, not predicted. translateMemoryRead is shared by every load width, yet only the two LQC2 cases fire — no other memory-op test distinguishes runtime->Load* from FAST_READ* at a resolved special address. See "What stays open"
in case COP2_QMTC2, change the discard to return "// vf0 write discarded"; vu_translator.cpp QMTC2 to vf0 emits no vf0 write pins the comment text itself. The absence-only assertion passed this, because a differently-worded comment carries no ctx->vu0_vf[ either
in translateVU_VRGET, append a second write to the format string — … ctx->vu0_vf[{}] = _mm_blendv_ps(…); ctx->vu0_vf[3] = res; }} vu_translation_helpers.cpp SH index 3 is not one of the three operand values the destination check used to probe, so the probe passed this. SW's string-replace comparison and MD's mask lists are unaffected, so SH fires alone — which is what makes it a check on the exhaustive scan rather than on the sweep around it
in instructions.h, rewrite one VU0_S1_ enumerator's initialiser from hexadecimal to the equivalent decimal literal (value unchanged) instructions.h (header — dependent objects recompile and the test binary relinks) COV, SH, SW, MD, MMIO semantically inert, so it is exactly the silent-under-coverage shape: the opcode drops out of the parse and every sweep that walks the list quietly covers one opcode fewer. Caught by the completeness assertion; before it, this changed nothing observable

Rows whose emission or assertion this change touched were re-measured rather than carried forward.
Every LDC2 guard row gained LQC2 to vf0 still performs the load alongside the test it already
named, because that test now holds the whole emitted statement and so also catches a guard that
stops firing. The address-expression rows — inst.rs to inst.rt, inst.simmediate to 0, and
the operand pair frozen to literals — did not move: they perturb only the operand-built address,
which just one memory-path row uses, and they already named both LQC2 cases. The QMTC2 rows did
not move either, since the added positive fires inside a test they already fail.

What stays open

What has been looked for and found, not a proof that nothing else survives.

  • A guard keyed on a destination index the sweep never drives, in either shape. A site's destination
    is held at its reference index and at zero while the other operands are walked in full, so
    codegen::vuVfDestMask(vfd & 0xF, …) at one call site - an ordinary truncation slip that also
    discards that opcode's writes to vf16 - survives, and so would an early return keyed the same
    way.
  • A guard keyed on one particular value of a wide field. Four such fields are sampled rather than
    walked, because none of their domains can be walked in full: the instruction address (thirty-two
    values, driven non-zero and varying), the LQC2 immediate (five of its sixty-five thousand odd),
    the QMTC2 encoding low bits (four of two thousand odd), and the post-decode MMIO address (two
    values with the flag set, plus the cleared state). A guard keyed on a value outside those samples
    survives.
  • A guard conditioned on two non-destination operands at once. They are swept one at a time and never
    crossed with each other.
  • A write site whose destination came from a field other than sa, rd or rt. No such site
    exists today, and the sweep would not reach one added later.
  • Four pre-existing tests are named for instruction families but driven at one opcode each:
    VU0 S1 uses fd/fs/ft fields (sa/rd/rt) (VADD), VU0 S1 q/i forms keep mask… (VADDq),
    VU0 S2 vector ops use rd as source and rt as destination (VABS) and
    VU0 S2 VI memory ops use rd as VI base register (VLQI) — one operand set and one field mask
    apiece. They predate this change and are untouched by it, and nothing here cites them as family
    coverage. The destination-slot half of each claim is carried across every parsed opcode by SH;
    the source-operand half is not carried anywhere, and remains a single-opcode spot-check.
  • Which load form translateMemoryRead picks, for every load width except the LQC2 one. That
    helper is shared by every read the generator emits, but dropping its special-address test fires
    only the two LQC2 cases — so no other memory-op assertion distinguishes runtime->Load* from
    FAST_READ* at a resolved special address. Pre-existing, wider than this change, and not closed
    here; recorded because the mutation that shows it is in the table above.
  • Runtime behavior of the generated code. Every assertion here is on emitted text.
  • Whether leaving the accumulator "A" family and VSQI/VSQD unguarded is right. That is a reading
    of the manual; the suite pins the emission, not the reading.
  • A site that emits nothing at all. Closed at every opcode and every field mask by the declared
    shape, at one operand triple. A discard keyed on an operand value that triple does not hold is
    reached only by the per-slot walk, and only at the slots that walk reaches.
  • What a site computes, past its destination and its write masks. The declared shape pins how many
    masks a site emits and which register it writes; swapping PS2_VADD for PS2_VSUB inside a
    translator changes neither.
  • The declared shape is a hand-written reading of the dispatch. An opcode added to
    instructions.h without a row fails the test rather than passing unanchored, but an opcode
    whose row is wrong in the same direction as the generator is wrong would agree with it.
grep -n 'vu0MacroShape' ps2xTest/src/code_generator_tests.cpp

Working-tree cleanliness after mutation testing

git status --porcelain
git diff --stat

Both empty relative to the committed state once every mutation was reverted and rebuilt green,
before this document and PR-BODY.md were added as the only untracked files.

vf0 is the VU's hardwired constant register: (x,y,z,w) = (0,0,0,1) on both
VUs, and an instruction naming it as a destination executes but does not
change it. The code generator never checked the destination index, so every
VU0 macro-mode translator that writes a vector destination could emit a
store into ctx->vu0_vf[0].

The destination register index is known at recompile time, so this is a
codegen-time decision with no cost in generated code:

- Masked-blend writers take their field mask through codegen::vuVfDestMask,
  which yields an empty mask for a vf0 destination. The statement is still
  emitted, so everything else it does - the VI-register pointer update in
  LQI/LQD, which hardware performs whether or not a field is written, and
  the accumulator assignment these translators already carried - is left
  exactly as it was. Only the register write is neutered.
- QMTC2 naming vf0 emits no code, matching how a zero-destination ADDI,
  ADDIU or SLL is already handled.
- LQC2 naming vf0 keeps the load and discards the value, because READ128
  routes to the runtime loader for special addresses and the access itself
  is observable. Both paths now share one address expression.

Reading vf0 out via SQC2 is unchanged. This change stops generated code
from disturbing vf0; it does not establish the constant, which is a
separate runtime-initialization concern.
@smmathews

Copy link
Copy Markdown
Contributor Author

(I understand you're re-working vu0, will leave in draft)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant