fix(vu0): discard macro-mode writes that name vf0 as the destination - #201
Draft
smmathews wants to merge 1 commit into
Draft
fix(vu0): discard macro-mode writes that name vf0 as the destination#201smmathews wants to merge 1 commit into
smmathews wants to merge 1 commit into
Conversation
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.
Contributor
Author
|
(I understand you're re-working vu0, will leave in draft) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix(vu0): discard macro-mode writes that name vf0 as the destination
vf0is the VU's hardwired constant register. The code generator never checked the destinationindex on a VU0 macro-mode write, so any instruction naming
vf0emitted a real store intoctx->vu0_vf[0], at three shapes: masked-blend writers,QMTC2, andLQC2(OPCODE_LDC2). Itresolves no observed failure.
(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.
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'."
SUBRemarks: "When VF00 is specified as the destination, the instruction is used tocompare VF[fs] with VF[ft]." Chapter 6's
VSUBpage binds this to the COP2 instruction patchedhere: "Same as the micro instruction SUB. Refer to '4.2. Upper Instruction Reference'."
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.
// NOPand""forr0, where nothing else is at stake.SPECIAL_MULTandSPECIAL_MULTU, whoselo/hiupdates must survive a zero destination — the existingr0precedent for the case below.
VLQI/VLQDpointerupdate and the accumulator assignment. The manual puts
LQI'sVI[is] = VI[is] + 1outside everyfield guard, and Chapter 6's
VLQIpage binds the macro form: "Same as the micro instruction LQI.Refer to '4.3. Lower Instruction Reference'." Suppressing the statement would delete that update.
QMTC2, which carries nothing else.(void)discard forLQC2, whoseREAD128routes toruntime->Load128()for specialaddresses: the access is observable, only the loaded value is discarded.
codegen::vuVfDestMasksits besidecodegen::vuMaskExprincodegen_helpers.h;code_generator.his 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 ofinstructions.histhen 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
vf0destination empties that mask and changes nothing else, and that neither depends on anyoperand but the destination.
QMTC2andLQC2each get a suppresses / still-writes pair run overthe operand space; the
LQC2pair also runs over the memory-path table — the MMIO flag, thecaller'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.mdcarries the mutation table, aclaim-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_testsBlast radius
Recompiler output changes: regenerate your recompiled corpus; a runtime-only rebuild changes
nothing. Output for a non-
vf0destination is byte-identical, so the diff is exactly thevf0sites. Complementary to #187, which fixes the initial value of
vu0_vf[0]atR5900Contextconstruction — disjoint files, either order, neither alone sufficient.
What this does not fix
vf0-destination arithmetic op is acomparison 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 ownchange: disjoint write sites, bare
ctx->vi[...]assignments with no shared mask helper, severaldiscarding the register write while keeping a memory effect.
vf[0]handling and is untouched.translateVU_VRNEXTnever writesVF[ft]though the manual'sRNEXTdoes; that gap predatesthis 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_testsThe 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
mainas here; the guard searchreturns 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.cppEvery write site is guarded on this branch
The first block returns the pre-existing
OPCODE_ADDIandOPCODE_ADDIUguards plus the newOPCODE_LDC2one. The second pair returns the identical set of lines. The third returns thedest_maskassignments that skip the helper: the accumulator "A" family and theVSQI/VSQDstores, none of which names a VF register as its destination. Read the split off these commands
rather than from any number written here.
grep -rn 'uint8_t dest_mask = inst.vectorInfo.vectorField;' ps2xRecomp/src/lib/The three inline S2 cases guard
inst.rt, notinst.rdClaim: in
VU0_S2_VABS,VU0_S2_VMOVEandVU0_S2_VMR32the destination isinst.rt, the sourceinst.rd. Carried bySH, whose declared shape table namesvfDestSlot = 2— thertslot — forall three, and which runs every opcode parsed out of
instructions.hrather than a sampled one. Thepre-existing
"VU0 S2 vector ops use rd as source and rt as destination"is not cited here: itdrives
VABSalone, so it cannot carry a claim about three opcodes.Non-
vf0generated output is unchangedClaim: for a destination other than
vf0, output is byte-identical to before this change.codegen::vuVfDestMask(vf_dest_reg, dest_mask)returnsdest_maskunchanged whenevervf_dest_reg != 0, and theQMTC2andLDC2guards branch only at destination0.The in-suite carriers are
SW's absolute assertion — that a non-vf0destination still emits theinstruction'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.
OPCODE_LDC2andtranslateVU_VRGETwere restructured, so byte-identity was re-established forthem with a throwaway driver (not committed) diffing
translateInstructionoutput against amainbuild,
rtandrsswept0..31: every differing row hadrt == 0, and everyrt == 0rowdiffered.
grep -n 'case OPCODE_LDC2' -A6 ps2xRecomp/src/lib/instruction_translator.cppThe in-suite counterpart is stronger than an address substring: both
LQC2cases hold the wholeemitted 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.
Tests added
In
ps2xTest/src/code_generator_tests.cpp, registered insideregister_code_generator_tests()."VU0 macro mappings cover all S1/S2 enums"was rewritten onto the sharedvu0MacroOps/makeVu0MacroOphelpers with its marker assertion unchanged and a non-empty assertion added besideit, because the marker check is a negative that an empty emission also satisfies.
vuVfDestMask yields an empty mask for a vf0 destinationvuVfDestMask passes the field mask through for every non-vf0 destinationevery VU0 macro site emits the write masks and VF destination its opcode declares(itsexpectation 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 ofinstructions.h, run atevery field mask the decoder can produce)
VU0 non-zero destination keeps its write mask— a one-point regression spot-check, not thegeneral property its name suggests: one opcode, one destination index, one field mask. The general
form is carried by
SWandMD, which walk every parsed opcode and every field maskQMTC2 to vf0 emits no vf0 writeQMTC2 to a non-zero vf still writesVU0 macro emission does not depend on the post-decode MMIO fieldsQMTC2 emission does not depend on the post-decode MMIO fieldsLQC2 to vf0 emits no vf0 writeLQC2 to vf0 still performs the loadLQC2 to a non-zero vf still writesVU0 macro write masks depend only on the destination index and the field maskThe shared
vu0MacroOpshelper now asserts that its opcode list is complete, not merelynon-empty.
parseEnumValuesonly matches a hexadecimal initialiser, so an enumerator added with adecimal 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.
The mask helper is pinned over its whole reachable domain
Claim:
codegen::vuVfDestMaskis not merely sampled at one index and one mask. The VF registerindex reaching it comes from the five-bit
SA/RD/RTextractions ininstructions.h, and thefield mask from
(raw >> 21) & 0xFinr5900_decoder.cpp, so its reachable domain is 0..31 by0..15 and the two added tests walk all of it: a
vf0destination yields an empty mask for everyfield 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 thedomain claim is the same either way.
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.
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.Checking a corpus for these write sites
A recipe, not a reproduction. Claim, conditional: if an executable contains VU0 macro-mode
instructions naming
vf0as their destination, recompiling it emits them and these find them. Use amainbuild, where no guard exists and all three shapes assignctx->vu0_vf[0]. An empty firstresult means that executable has no such instructions, which the claim leaves open.
On this branch a
QMTC2namingvf0becomes a comment and anLQC2namingvf0becomes a(void)discard, so neither emitsctx->vu0_vf[text. Each shape needs its own pattern: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:
LQC2pattern — all three load forms and the(void)spelling — is pinned byLQC2 to vf0 still performs the load, which drives all thirteen memory-path rows and holds thewhole 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 ofFAST_READ128(, so no single substring assertion can tell the threeapart; only whole-statement equality does.
QMTC2comment text is pinned byQMTC2 to vf0 emits no vf0 write, which now asserts theemitted comment itself and not only the absence of a
ctx->vu0_vf[write. An empty emission alsosatisfies an absence check, so the absence check alone could not carry this pattern.
SW, which asserts that avf0destination empties themask 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.
LQC2 to vf0 still performs the loadrs0..31 × five immediates; whole emitted statement held against an expected stringREAD128form only — one of three forms, one of thirteen rows — and asserted two substrings, one of which (READ128() is a substring ofFAST_READ128(and so could not discriminate the form even where that form was drivenLQC2 to a non-zero vf still writesrsplus the instruction's own immediatert1..31 ×rs0..31 × five immediates; whole emitted statement(void)discard spelling(void)and the trailing semicolon(void)anywhere in the suiteQMTC2 to vf0 emits no vf0 writevf0QMTC2emits no write, and (for the recipe) emits the discard commentctx->vu0_vf[, plus exact equality against the comment textQMTC2 to a non-zero vf still writesVU0 macro mappings cover all S1/S2 enums(COV), via the shared opcode listVU0_S1_*/VU0_S2_*enum"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"ctx->vu0_vf[N] =assignment in the emission, collected by index and compared as a whole against the declared destinationevery VU0 macro site emits the write masks and VF destination its opcode declares(SH), mask-count halfevery VU0 macro write site emits its own field mask, and a vf0 destination changes only that mask(SW)vf0destination changes only the maskSHso its loops cannot be vacuousVU0 macro write masks depend only on the destination index and the field mask(MD)vuVfDestMask yields an empty mask for a vf0 destination(H1) andvuVfDestMask passes the field mask through for every non-vf0 destination(H2)vf0at every mask; every non-vf0destination at every maskVU0 macro emission does not depend on the post-decode MMIO fields(MMIO) andQMTC2 emission does not depend on the post-decode MMIO fieldsLQC2 to vf0 emits no vf0 writevf0LQC2emits no writers0..31 × five immediatesVU0 non-zero destination keeps its write maskSWandMDcarry the propertyVU0 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 registerSH; the source-operand half is notMutation discipline
For each row: the one-line production mutation was applied by hand, rebuilt with
cmake --build build -j 12, and./build/ps2xTest/ps2x_testsrun to completion. The tests named in the row failedand 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.codegen::vuVfDestMaskbody →return (vf_dest_reg <= 1) ? 0 : dest_mask;codegen_helpers.hcodegen::vuVfDestMaskbody →return (vf_dest_reg <= 6) ? 0 : dest_mask;codegen_helpers.hcodegen::vuVfDestMaskbody →return (vf_dest_reg == 0) ? 0 : (uint8_t)(dest_mask | 0xF);codegen_helpers.h0x5) pass also fails because the widened mask diverges from the instruction's own fieldcodegen::vuVfDestMaskbody →return (vf_dest_reg == 0) ? 0 : (uint8_t)0xF;codegen_helpers.htranslateVU_VADD_Field, change tocodegen::vuVfDestMask(vfd, 0xF), dropping the instruction's own fieldvu_translation_helpers.cpptranslateVU_VMINI_Field, revert touint8_t dest_mask = inst.vectorInfo.vectorField;vu_translation_helpers.cpptranslateVU_VMADD, insertif (vfd == 0) return "";before thereturnvu_translation_helpers.cpptranslateVU_VMSUB, insertif (vfd == 0) return "";before thereturnvu_translation_helpers.cppVMADDrowtranslateVU_VMADD_Field, insertif (vfd == 0) return "";before thereturnvu_translation_helpers.cppVMADDrowtranslateVU_VOPMSUB, insertif (vfd == 0) return "";before thereturnvu_translation_helpers.cppVMADDrowtranslateVU_VLQI, insertif (inst.rt == 0) return "";before thereturnvu_translation_helpers.cpptranslateVU_VLQD, insertif (inst.rt == 0) return "";before thereturnvu_translation_helpers.cppVLQIrowtranslateVU_VADDA, change touint8_t dest_mask = codegen::vuVfDestMask(inst.sa, inst.vectorInfo.vectorField);vu_translation_helpers.cppVADDAhas no VF destination register; the sweep catches a helper misapplied to an operand that is not this op's VF destinationtranslateVU_VSQI, change touint8_t dest_mask = codegen::vuVfDestMask(inst.rt, inst.vectorInfo.vectorField);vu_translation_helpers.cppVSQI'srtis a blend source, not a destination; the same misapplication class on the S2 sidetranslateVU_VMAX, change the call tocodegen::vuVfDestMask(vfd, inst.vectorInfo.vectorField == 0xA ? 0 : inst.vectorInfo.vectorField)vu_translation_helpers.cpptranslateVU_VMINI_Field, change the call tocodegen::vuVfDestMask(0, inst.vectorInfo.vectorField)vu_translation_helpers.cppVMINIx/y/z/wwrite 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-vf0destination must still carry the instruction's own field masktranslateVU_VRGET, change the call tocodegen::vuVfDestMask(0, inst.vectorInfo.vectorField)vu_translation_helpers.cpprtcase VU0_S2_VABS, change the call tocodegen::vuVfDestMask(0, inst.vectorInfo.vectorField)vu_translator.cpptranslateVU_VADDA, change touint8_t dest_mask = codegen::vuVfDestMask(0, inst.vectorInfo.vectorField);vu_translation_helpers.cppcodegen::vuVfDestMask, change the body toreturn 0;codegen_helpers.hVU0 non-zero destination keeps its write maskvf0destination, 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-vf0regression test fires on its own live-mask literal.case COP2_QMTC2, guard →rd == 0 && (inst.function & 1) == 0vu_translator.cppQMTC2 to vf0 emits no vf0 writevf0write for every interlockedQMTC2a real recompile producescase COP2_QMTC2, guard →rd == 0 && inst.vectorInfo.vectorField == 0vu_translator.cppQMTC2 to vf0 emits no vf0 write0xFfor every conformingQMTC2, so this restores the write for everyQMTC2a recompile producescase COP2_QMTC2, guard →rd == 0 && inst.address == 0vu_translator.cppQMTC2 to vf0 emits no vf0 writetranslateVU_VADD_Field,dest_mask→(vfd == 0 && inst.address == 0) ? 0 : inst.vectorInfo.vectorFieldvu_translation_helpers.cpptranslateVU_VMINI_Field,dest_mask→(vfd == 0 && !inst.modificationInfo.modifiesVFR) ? 0 : inst.vectorInfo.vectorFieldvu_translation_helpers.cppVMINIfield form is among themcase VU0_S2_VABS,dest_mask→(inst.rt == 0 && !inst.isVU) ? 0 : inst.vectorInfo.vectorFieldvu_translator.cpptranslateVU_VLQI,dest_mask→(inst.rt == 0 && inst.address == 0) ? 0 : inst.vectorInfo.vectorFieldvu_translation_helpers.cpptranslateVU_VMUL_Field,dest_mask→(vfd == 0 && inst.rs == COP2_CO) ? 0 : inst.vectorInfo.vectorFieldvu_translation_helpers.cppCOP2_COin real output only when the mask is already empty. Fails at every non-zero field maskcase OPCODE_LDC2, guard →inst.rt == 0 && !inst.isVUinstruction_translator.cppLQC2 to vf0 emits no vf0 write,LQC2 to vf0 still performs the loadLDC2case COP2_QMTC2, delete theif (rd == 0)guardvu_translator.cppQMTC2 to vf0 emits no vf0 writecase COP2_QMTC2, change the guard condition fromrd == 0tord < 32vu_translator.cppQMTC2 to a non-zero vf still writesrd < 32suppresses every valid VF register toocase COP2_QMTC2, change the guard condition fromrd == 0tord == 0 || rd == 20vu_translator.cppQMTC2 to a non-zero vf still writescase COP2_QMTC2, replace the source operand with the literalGPR_VEC(ctx, 9)vu_translator.cppQMTC2 to a non-zero vf still writesrtfixed; it is caught now the test crosses every source register with every destination indexcase OPCODE_LDC2, delete theif (inst.rt == 0)guardinstruction_translator.cppLQC2 to vf0 emits no vf0 write,LQC2 to vf0 still performs the loadcase OPCODE_LDC2's guard, change the return toreturn "";instruction_translator.cppLQC2 to vf0 still performs the loadvu0_vfreference either, so the branch test cannot see it; the load test is what pins that the access survivescase OPCODE_LDC2, change the guard condition frominst.rt == 0toinst.rt < 32instruction_translator.cppLQC2 to a non-zero vf still writesQMTC2rowcase OPCODE_LDC2, changeinst.rstoinst.rtin the hoisted address expressioninstruction_translator.cppLQC2 to vf0 still performs the load,LQC2 to a non-zero vf still writescase OPCODE_LDC2, changeinst.simmediateto0in the hoisted address expressioninstruction_translator.cppLQC2 to vf0 still performs the load,LQC2 to a non-zero vf still writescase OPCODE_LDC2, change the guard condition frominst.rt == 0toinst.rt == 0 || inst.rt == 20instruction_translator.cppLQC2 to a non-zero vf still writesQMTC2row, closed the same waycase OPCODE_LDC2, replaceinst.rs, inst.simmediatein the hoisted address with the literals4, 16instruction_translator.cppLQC2 to vf0 still performs the load,LQC2 to a non-zero vf still writesLQC2test used to drive. It is caught now that both of those cases walk the base register and the immediatecase COP2_QMTC2, change the guard fromrd == 0tord == 0 && rt == 9vu_translator.cppQMTC2 to vf0 emits no vf0 writevf0case used to drive. Caught now that thevf0half runs over every source register, closing the last thirty-one cells of the(rd, rt)gridcase OPCODE_LDC2, change the guard frominst.rt == 0toinst.rt == 0 && inst.rs == 4instruction_translator.cppLQC2 to vf0 emits no vf0 write,LQC2 to vf0 still performs the loadrsbut asserts only the read and the address, both of which the unguarded path also emits, so it never saw thiscase OPCODE_LDC2, change the guard toinst.rt == 0 && !inst.isMmioinstruction_translator.cppLQC2 to vf0 emits no vf0 write,LQC2 to vf0 still performs the loadcase OPCODE_LDC2, change the guard toinst.rt == 0 && !effectiveMemoryHint.hasAddressinstruction_translator.cppLQC2 to vf0 emits no vf0 write,LQC2 to vf0 still performs the loadLDC2is a direct memory access, so a constant-folded base gives it a resolved address in a real recompile — this one would restore thevf0write in generated output while the suite stayed greencase COP2_QMTC2, change the guard tord == 0 && !inst.isMmiovu_translator.cppQMTC2 emission does not depend on the post-decode MMIO fieldsLDC2MMIO 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 thevf0write for anyQMTC2listed theretranslateVU_VADD_Field,dest_mask→inst.isMmio ? inst.vectorInfo.vectorField : codegen::vuVfDestMask(vfd, inst.vectorInfo.vectorField)vu_translation_helpers.cppVU0 macro emission does not depend on the post-decode MMIO fieldscase COP2_QMTC2, change the guard tord == 0 && inst.mmioAddress == 0vu_translator.cppQMTC2 emission does not depend on the post-decode MMIO fieldscase COP2_QMTC2, change the guard tord == 0 && !(inst.isMmio && inst.mmioAddress == 0)vu_translator.cppQMTC2 emission does not depend on the post-decode MMIO fieldscase OPCODE_LDC2, change the guard toinst.rt == 0 && !(inst.isMmio && inst.mmioAddress == 0)instruction_translator.cppLQC2 to vf0 emits no vf0 write,LQC2 to vf0 still performs the loadQMTC2row 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 statecase OPCODE_LDC2, change the guard toinst.rt == 0 && !(inst.isMmio && !memoryHint.hasAddress && inst.mmioAddress == 0)instruction_translator.cppLQC2 to vf0 emits no vf0 write,LQC2 to vf0 still performs the loadcase OPCODE_LDC2, change the guard toinst.rt == 0 && !(effectiveMemoryHint.hasAddress && effectiveMemoryHint.address == 0)instruction_translator.cppLQC2 to vf0 emits no vf0 write,LQC2 to vf0 still performs the loadcase OPCODE_LDC2, change the guard toinst.rt == 0 && !(!inst.isMmio && memoryHint.hasAddress && memoryHint.address == 0)instruction_translator.cppLQC2 to vf0 emits no vf0 write,LQC2 to vf0 still performs the loadcase OPCODE_LDC2, change the guard toinst.rt == 0 && !(inst.isMmio && memoryHint.hasAddress && memoryHint.address == 0)instruction_translator.cppLQC2 to vf0 emits no vf0 write,LQC2 to vf0 still performs the loadcase OPCODE_LDC2, change the guard toinst.rt == 0 && !(!inst.isMmio && effectiveMemoryHint.hasAddress && Ps2IsSpecialAddress(effectiveMemoryHint.address))instruction_translator.cppLQC2 to vf0 emits no vf0 write,LQC2 to vf0 still performs the loadcase OPCODE_LDC2, change the guard toinst.rt == 0 && !(inst.isMmio && !memoryHint.hasAddress && Ps2IsSpecialAddress(inst.mmioAddress))instruction_translator.cppLQC2 to vf0 emits no vf0 write,LQC2 to vf0 still performs the loadcase OPCODE_LDC2, change the guard toinst.rt == 0 && !(inst.isMmio && memoryHint.hasAddress && Ps2IsSpecialAddress(inst.mmioAddress))instruction_translator.cppLQC2 to vf0 emits no vf0 write,LQC2 to vf0 still performs the loadtranslateVU_VADD_Field, changedest_maskto(vfd == 0 && vfs == 11) ? 0 : inst.vectorInfo.vectorFieldvu_translation_helpers.cpptranslateVU_VADD_Field, changedest_maskto(vfd != 0 && vfs == 5) ? 0 : inst.vectorInfo.vectorFieldvu_translation_helpers.cppvf0half, taken literally as a direct replacement of thedest_maskexpression. Because the replacement no longer routes thevfd == 0case through the helper, it also corrupts the onevfd == 0cell the pre-existing sweep already drives (sazeroed to0,rdat11), 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 assumedcase OPCODE_LDC2, change the guard toinst.rt == 0 && memoryHint.address == inst.mmioAddressinstruction_translator.cppLQC2 to vf0 emits no vf0 write,LQC2 to vf0 still performs the loadvf0write wherever a recompile resolves an address the configuration map does not namecase OPCODE_LDC2, change the guard toinst.rt == 0 && !(inst.isMmio && !memoryHint.hasAddress && memoryHint.address != inst.mmioAddress)instruction_translator.cppLQC2 to vf0 emits no vf0 write,LQC2 to vf0 still performs the loadcase OPCODE_LDC2, change the guard toinst.rt == 0 && !(!inst.isMmio && memoryHint.hasAddress && memoryHint.address != inst.mmioAddress)instruction_translator.cppLQC2 to vf0 emits no vf0 write,LQC2 to vf0 still performs the loadLQC2produces: the decoder leaves the MMIO address at zero while the resolved address is not. It fires at one memory-path row onlycase OPCODE_LDC2, change the guard toinst.rt == 0 && !(inst.isMmio && memoryHint.hasAddress && memoryHint.address != inst.mmioAddress)instruction_translator.cppLQC2 to vf0 emits no vf0 write,LQC2 to vf0 still performs the loadcodegen::vuVfDestMask, narrow the index parameter back touint8_tcodegen_helpers.htranslateVU_VADDA,dest_mask→(inst.sa == 10) ? 0 : inst.vectorInfo.vectorFieldvu_translation_helpers.cppVADDAa recompile emits, and it is the one slot a fixture sets after the decodetranslateVU_VADDA,dest_mask→(inst.rd == 17) ? 0 : inst.vectorInfo.vectorFieldvu_translation_helpers.cpptranslateVU_VADDA, insertif (inst.rd == 5) return "";before thereturnvu_translation_helpers.cpptranslateVU_VADDA, insertif (inst.rd == 17) return "";before thereturnvu_translation_helpers.cpptranslateVU_VADDA, insertif (inst.rd == 0) return "";before thereturnvu_translation_helpers.cpptranslateVU_VSQI, insertif (inst.rt == 5) return "";before thereturnvu_translation_helpers.cpprtis a blend source rather than a destinationtranslateVU_VADD_Field, insertif (inst.rd == 5) return "";before thereturnvu_translation_helpers.cpptranslateVU_VADD_Field, insertif (inst.vectorInfo.vectorField == 0xF) return "";after the field extractionvu_translation_helpers.cppMDstays green even now — at this field value its destination-free branch is not taken, and its destination branch compares two empty mask liststranslateVU_VADD_Field,return "";as the first statementvu_translation_helpers.cppMDstays green for the same reason as the row abovetranslateVU_VRGETbody →return "{ __m128 res = ctx->vu0_r; }";vu_translation_helpers.cppSHfires on the declared mask count,SWon the declared destination slot — recorded because it pins that the anchor is not merely a non-emptiness checktranslateVU_VMULq, insertif (vfd == 9) return "// discarded";before thereturnvu_translation_helpers.cppcase OPCODE_LDC2, guard →return inst.isMmio ? std::string("") : fmt::format("(void){};", load);instruction_translator.cppLQC2 to vf0 still performs the loadREAD128path, so it never reached this branch, and the no-write test cannot see a deletion because an empty emission carries noctx->vu0_vf[eithercase OPCODE_LDC2, guard →return effectiveMemoryHint.hasAddress ? std::string("") : fmt::format("(void){};", load);instruction_translator.cppLQC2 to vf0 still performs the loadLDC2is 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 rewritecase OPCODE_LDC2, guard →return fmt::format("(void)READ128(ADD32(GPR_U32(ctx, {}), {}));", inst.rs, inst.simmediate);instruction_translator.cppLQC2 to vf0 still performs the loadcase OPCODE_LDC2, guard →return fmt::format("{};", load);(drop the(void))instruction_translator.cppLQC2 to vf0 still performs the load(void)spelling the corpus recipe greps for. Nothing in the suite asserted it beforetranslateMemoryRead, drop the special-address test:if (inst.isMmio)in place ofif (inst.isMmio || Ps2IsSpecialAddress(resolvedAddress))instruction_translator.cppLQC2 to vf0 still performs the load,LQC2 to a non-zero vf still writestranslateMemoryReadis shared by every load width, yet only the twoLQC2cases fire — no other memory-op test distinguishesruntime->Load*fromFAST_READ*at a resolved special address. See "What stays open"case COP2_QMTC2, change the discard toreturn "// vf0 write discarded";vu_translator.cppQMTC2 to vf0 emits no vf0 writectx->vu0_vf[eithertranslateVU_VRGET, append a second write to the format string —… ctx->vu0_vf[{}] = _mm_blendv_ps(…); ctx->vu0_vf[3] = res; }}vu_translation_helpers.cppSW's string-replace comparison andMD's mask lists are unaffected, soSHfires alone — which is what makes it a check on the exhaustive scan rather than on the sweep around itinstructions.h, rewrite oneVU0_S1_enumerator's initialiser from hexadecimal to the equivalent decimal literal (value unchanged)instructions.h(header — dependent objects recompile and the test binary relinks)Rows whose emission or assertion this change touched were re-measured rather than carried forward.
Every
LDC2guard row gainedLQC2 to vf0 still performs the loadalongside the test it alreadynamed, because that test now holds the whole emitted statement and so also catches a guard that
stops firing. The address-expression rows —
inst.rstoinst.rt,inst.simmediateto0, andthe 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
LQC2cases. TheQMTC2rows didnot 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.
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 alsodiscards that opcode's writes to
vf16- survives, and so would an early return keyed the sameway.
walked, because none of their domains can be walked in full: the instruction address (thirty-two
values, driven non-zero and varying), the
LQC2immediate (five of its sixty-five thousand odd),the
QMTC2encoding low bits (four of two thousand odd), and the post-decode MMIO address (twovalues with the flag set, plus the cleared state). A guard keyed on a value outside those samples
survives.
crossed with each other.
sa,rdorrt. No such siteexists today, and the sweep would not reach one added later.
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) andVU0 S2 VI memory ops use rd as VI base register(VLQI) — one operand set and one field maskapiece. 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.
translateMemoryReadpicks, for every load width except theLQC2one. Thathelper is shared by every read the generator emits, but dropping its special-address test fires
only the two
LQC2cases — so no other memory-op assertion distinguishesruntime->Load*fromFAST_READ*at a resolved special address. Pre-existing, wider than this change, and not closedhere; recorded because the mutation that shows it is in the table above.
VSQI/VSQDunguarded is right. That is a readingof the manual; the suite pins the emission, not the reading.
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.
masks a site emits and which register it writes; swapping
PS2_VADDforPS2_VSUBinside atranslator changes neither.
instructions.hwithout a row fails the test rather than passing unanchored, but an opcodewhose row is wrong in the same direction as the generator is wrong would agree with it.
grep -n 'vu0MacroShape' ps2xTest/src/code_generator_tests.cppWorking-tree cleanliness after mutation testing
Both empty relative to the committed state once every mutation was reverted and rebuilt green,
before this document and
PR-BODY.mdwere added as the only untracked files.