Skip to content

fix(vu1): give both halves of a reordered instruction pair the pre-pair VF and Q - #200

Draft
smmathews wants to merge 2 commits into
ran-j:mainfrom
smmathews:feature/34-vu1-pair-register-file
Draft

fix(vu1): give both halves of a reordered instruction pair the pre-pair VF and Q#200
smmathews wants to merge 2 commits into
ran-j:mainfrom
smmathews:feature/34-vu1-pair-register-file

Conversation

@smmathews

Copy link
Copy Markdown
Contributor

fix(vu1): give both halves of an instruction pair the pre-pair VF and Q values

Problem

decodeInstructionPair already reorders a pair when the upper half writes a
VF register the lower half reads or writes, so the lower half sees the
pre-pair value. The gaps below remain.

  • Unclassified VF operands. vuLowerVfReadWriteMasks
    (ps2xRuntime/src/lib/vu/ps2_vu1_detail.h) fell through to
    default: return; for the EFU block (ESADD through EEXP) and the
    R-register block. The gap is live: ELENG, ERLENG and ERCPR read
    VF[fs] here, so a pair whose upper half wrote that register never
    reordered, and the EFU operation read the overwritten value.
  • Reordering fixes one direction only. Both halves issue at once and both
    read the pre-pair register file, so no order of execLower/execUpper
    serves both. Q has the same shape: DIV/SQRT/RSQRT write it from the
    lower half and q-form upper operations read it, so the reordered branch
    fed the upper half a Q the lower half had just produced. Q is the only
    register outside the VF file where this arises — no lower operation writes
    ACC or I, and P and R are read only by lower operations, of which a
    pair holds one.
  • Per-field write collisions. A VF write collision between the halves
    resolved per output field, not per register.

Fix

  • ps2_vu1_detail.h — classify the EFU cases (vuSetRegBit(readMask, is))
    and the R-register cases: RNEXT/RGET write VF[ft], RINIT/RXOR read
    VF[fs]. Every allocated lower-special selector naming a VF operand is now
    reported. Opcodes are classified whether or not their execution arm does
    anything, so implementing one later cannot reopen the gap. WAITP and the
    unallocated slots 0x77/0x7F name no vector operand and stay out.
  • ps2_vu1_core.cpp — in the lowerBeforeUpper branch, snapshot the VF
    registers named by the upper half's fs, ft and destination before the
    lower half runs. Restore them for the upper half, then re-apply the lower
    half's write everywhere except the upper half's own destination. Q is
    snapshotted and restored the same way, unconditionally: no upper instruction
    writes it.
  • Helpers vuLowerVfWriteMask and vuAddGuardedReg, beside vuSetRegBit.

Hardware basis

VU User's Manual, section 3.4 ("Pipeline Operation"); EVIDENCE.md carries
the full quotations.

  • 3.4.1 ("Hazards")ACC, I, Q, P and R generate no data
    hazards, and VF00 is a constant register outside hazard checks. The section
    adds: "It is possible to make the Q and P registers synchronize with each
    other by using the WAITQ/WAITP instruction."
    The exemption covers
    interlocks, not what a concurrently issued instruction observes.
  • 3.4.2 ("Upper Instruction and Lower Instruction") — the two issue
    concurrently; both pipelines stall on a hazard to either.
  • 3.4.3 ("Priority for Writing to a Register") — the upper half wins a
    write collision, in register units: "the result of the Lower instruction is
    discarded even when the Upper and Lower instructions write data to different
    fields"
    .
  • 3.4.4 ("FMAC Pipeline") — no data-dependency stalls between the two
    halves of one instruction.
  • 3.4.5 ("FDIV Pipeline")DIV/SQRT/RSQRT write Q back at the
    stage ending the FDIV pipeline, after the issuing pair has read its
    registers.
  • RGET/RNEXT instruction pages, Remarks — an upper-half write to
    VF[ft] in the same cycle discards this instruction's result regardless of
    field overlap.

Testing

Tests are in ps2xTest/src/ps2_vu1_tests.cpp, in the existing
MiniTest::Case("PS2VU1") block, built through the real encode/decode path.
They cover both read directions and the Q read, whole-register discard
through the fd destination in the shape of section 3.4.3's worked example
(with vf3 where the manual writes VF23) and through the upper-special ft
destination, the destination guard alone, and an upper half whose fs and ft
name one register the lower half writes. They also assert the classifier's
reads, writes and unclassified selectors, so the completeness claim above is
asserted, not inspected.

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

EVIDENCE.md has the selector tables, fail-before/pass-after values, the
mutation table, and the disclosures.

Risk and not in scope

  • One direction removes a write that used to land. The discard is in
    whole-register units. A reordered pair whose lower half writes the upper
    half's destination now loses that write on fields the upper half does not
    name and previously left alone. It is the only place where this change
    removes a value rather than reading one earlier.
  • Everything else reads an earlier value or is bit-identical: pairs the
    predicate rejects, LOI pairs, and reordered pairs whose lower half writes
    neither a VF register nor Q. In the earlier-value half rather than the
    bit-identical one: a reordered DIV, SQRT or RSQRT pair, whose upper half
    now reads the pre-pair Q, and a pair this change newly reorders onto an
    ELENG, ERLENG or ERCPR lower half, whose EFU operation now reads the
    pre-pair VF[fs]. Everything else this change reclassifies has an empty or
    absent execution arm in this tree, the R-register block included, so its
    classification changes nothing observable today.
  • No public header change. DecodedInstructionPair is not widened and
    ps2xRuntime/include/runtime/ps2_vu1.h is untouched, so nothing outside the
    interpreter is affected.
  • Inter-pair Q/P visibility latency is unchanged and still wrong. This
    tree makes a Q or P write visible immediately; hardware makes it visible
    only at the end of the FDIV or EFU pipeline. That window is the same width
    after this change as before it; closing it belongs with whichever change
    introduces a latency model.
  • No latency model, no WAITQ/WAITP sequencing, no EFU opcode bodies.
    Nothing here reads or writes a cycle counter; EVIDENCE.md records why each
    is out of scope and the state of every EFU execution arm.
  • Touches ps2_vu1_core.cpp and ps2_vu1_tests.cpp, which other VU1 work in
    this series also touches; expect to rebase this branch if that work lands
    first. Within this file, EVIDENCE.md §7 gives the command that checks no test
    name collides.
Evidence — probe output, mutation table, mirror sweep, and reproduction commands

Evidence

1. Defect reproduction

Probe over the lower-special selectors this change touches, run from the clone
root against the tree before this change (HEAD~2) and after it. No project
build needed.

cat > /tmp/efu_probe.cpp <<'EOF'
#include "vu/ps2_vu1_detail.h"
#include <cstdio>
#include <initializer_list>
static uint32_t lowerSpecial(uint8_t so, uint8_t is, uint8_t it)
{
    return (0x40u << 25) | (0xEu << 21) | ((uint32_t)it << 16) | ((uint32_t)is << 11) |
           ((uint32_t)(so & 0x7Cu) << 4) | (uint32_t)(so & 0x3u) | 0x3Cu;
}
int main()
{
    for (uint8_t so : {0x40,0x41,0x42,0x43,
                       0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x78,0x79,0x7A,0x7B,0x7C,0x7D,0x7E})
    {
        uint32_t r = 0, w = 0;
        vuLowerVfReadWriteMasks(lowerSpecial(so, 3u, 5u), r, w);
        std::printf("specialOp=0x%02X read=0x%08X write=0x%08X\n", so, r, w);
    }
    return 0;
}
EOF
mkdir -p /tmp/pretree/vu
git show HEAD~2:ps2xRuntime/src/lib/vu/ps2_vu1_detail.h > /tmp/pretree/vu/ps2_vu1_detail.h
g++ -std=c++20 -I/tmp/pretree -Ips2xRuntime/src/lib -o /tmp/efu_probe_pre /tmp/efu_probe.cpp && /tmp/efu_probe_pre
g++ -std=c++20 -Ips2xRuntime/src/lib -o /tmp/efu_probe /tmp/efu_probe.cpp && /tmp/efu_probe

is = 3 gives read bit 0x00000008; it = 5 gives write bit 0x00000020.

Before — every selector reports neither read nor write, including 0x72
(ELENG), 0x73 (ERLENG) and 0x7A (ERCPR), which this interpreter
already executes as reading VF[fs]:

specialOp=0x40 read=0x00000000 write=0x00000000
specialOp=0x41 read=0x00000000 write=0x00000000
specialOp=0x42 read=0x00000000 write=0x00000000
specialOp=0x43 read=0x00000000 write=0x00000000
specialOp=0x70 read=0x00000000 write=0x00000000
specialOp=0x71 read=0x00000000 write=0x00000000
specialOp=0x72 read=0x00000000 write=0x00000000
specialOp=0x73 read=0x00000000 write=0x00000000
specialOp=0x74 read=0x00000000 write=0x00000000
specialOp=0x75 read=0x00000000 write=0x00000000
specialOp=0x76 read=0x00000000 write=0x00000000
specialOp=0x78 read=0x00000000 write=0x00000000
specialOp=0x79 read=0x00000000 write=0x00000000
specialOp=0x7A read=0x00000000 write=0x00000000
specialOp=0x7B read=0x00000000 write=0x00000000
specialOp=0x7C read=0x00000000 write=0x00000000
specialOp=0x7D read=0x00000000 write=0x00000000
specialOp=0x7E read=0x00000000 write=0x00000000

After — RNEXT/RGET report the VF[ft] write, the EFU selectors and
RINIT/RXOR report the VF[fs] read, WAITP still reports neither:

specialOp=0x40 read=0x00000000 write=0x00000020
specialOp=0x41 read=0x00000000 write=0x00000020
specialOp=0x42 read=0x00000008 write=0x00000000
specialOp=0x43 read=0x00000008 write=0x00000000
specialOp=0x70 read=0x00000008 write=0x00000000
specialOp=0x71 read=0x00000008 write=0x00000000
specialOp=0x72 read=0x00000008 write=0x00000000
specialOp=0x73 read=0x00000008 write=0x00000000
specialOp=0x74 read=0x00000008 write=0x00000000
specialOp=0x75 read=0x00000008 write=0x00000000
specialOp=0x76 read=0x00000008 write=0x00000000
specialOp=0x78 read=0x00000008 write=0x00000000
specialOp=0x79 read=0x00000008 write=0x00000000
specialOp=0x7A read=0x00000008 write=0x00000000
specialOp=0x7B read=0x00000000 write=0x00000000
specialOp=0x7C read=0x00000008 write=0x00000000
specialOp=0x7D read=0x00000008 write=0x00000000
specialOp=0x7E read=0x00000008 write=0x00000000

2. Selector coverage

Selectors use the interpreter's own (instr & 0x3) | ((instr >> 4) & 0x7C).
Mnemonic lines are the manual's. "Execution arm" is the opcode's state in
ps2_vu1_lower.cpp, read back from the source by the command below. A selector
this change reclassifies but which the output does not list has no arm at all.

awk '/case 0x40: \/\/ RNEXT/,0' ps2xRuntime/src/lib/vu/ps2_vu1_lower.cpp |
awk '/^ *case 0x(4[0-3]|7[0-9A-E]):/{sel=$2; sub(/:$/,"",sel); getline n; sub(/^[ \t]+/,"",n); print sel, (n == "return;" ? "empty arm" : "has a body")}'
0x40 empty arm
0x41 empty arm
0x42 empty arm
0x43 empty arm
0x70 empty arm
0x71 empty arm
0x72 has a body
0x73 has a body
0x7A has a body
0x7B empty arm
0x7D empty arm

EFU block — every operation takes VF[fs] as its only vector operand and
writes P:

Mnemonic Manual mnemonic line Selector Classified as Execution arm
ESADD ESADD P, VF[fs] 0x70 reads VF[fs] empty return;
ERSADD ERSADD P, VF[fs] 0x71 reads VF[fs] empty return;
ELENG ELENG P, VF[fs] 0x72 reads VF[fs] writes P from VF[fs]
ERLENG ERLENG P, VF[fs] 0x73 reads VF[fs] writes P from VF[fs]
EATANxy EATANxy P, VF[fs] 0x74 reads VF[fs] absent
EATANxz EATANxz P, VF[fs] 0x75 reads VF[fs] absent
ESUM ESUM P, VF[fs] 0x76 reads VF[fs] absent
ESQRT ESQRT P, VF[fs]fsf 0x78 reads VF[fs] absent
ERSQRT ERSQRT P, VF[fs]fsf 0x79 reads VF[fs] absent
ERCPR ERCPR P, VF[fs]fsf 0x7A reads VF[fs] writes P from VF[fs]
ESIN ESIN P, VF[fs]fsf 0x7C reads VF[fs] absent
EATAN EATAN P, VF[fs]fsf 0x7D reads VF[fs] empty return;
EEXP EEXP P, VF[fs]fsf 0x7E reads VF[fs] absent
WAITP (no vector operand) 0x7B unclassified empty return;
unallocated 0x77 unclassified absent
unallocated 0x7F unclassified absent

R-register block:

Mnemonic Manual mnemonic line Selector Classified as Execution arm
RNEXT RNEXT.dest VF[ft]dest, R 0x40 writes VF[ft] empty return;
RGET RGET.dest VF[ft]dest, R 0x41 writes VF[ft] empty return;
RINIT RINIT R, VF[fs]fsf 0x42 reads VF[fs] empty return;
RXOR RXOR R, VF[fs]fsf 0x43 reads VF[fs] empty return;

Remaining lower-special selectors: integer registers or no operand,
unclassified, absence asserted by lower-special ops that name no VF operand report neither read nor write:

Mnemonic Selector
WAITQ 0x3B
ILWR 0x3E
ISWR 0x3F
XTOP 0x68
XITOP 0x69
XGKICK 0x6C

3. Build and test

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

ps2x_tests statically links ps2_analyzer_lib, so -DPS2X_BUILD_ANALYZER=OFF
must never be passed to the configure step — it breaks the test link. ctest
registers no tests in this project; the binary's own summary line is the source
of truth.

4. Fail-before / pass-after

Each row fails on the tree before this change with the base-tree value shown
and passes after. Base-tree values come from running the fixture with the tests
added and no part of the fix applied.

Test Assertion Base-tree observed Expected (post-fix)
EFU lower op reads the pre-pair VF value the upper half overwrites state().p 52.0 13.0
upper half reads the pre-pair value of an fs operand the lower half overwrites vf5 (110, 220, 330, 440) (11, 22, 33, 44)
upper half reads the pre-pair value of an ft operand the lower half overwrites vf5 (100, 400, 900, 1600) (2, 8, 15, 40)
upper half write discards the lower half write to the same register in whole-register units vf1 (11, 22, 3, 777) (11, 22, 3, 4)
upper-special ft destination discards the lower half write to that register vf4 (1, 2, 7, 8) (1, 2, 9, 9)
upper half destination is guarded even when it is not one of its own operands vf1 (110, 220, 7, 8) (110, 220, 3, 4)
upper half reads the pre-pair Q value the lower half overwrites vf3 (12, 12, 12, 12) (108, 108, 108, 108)
upper half with fs and ft aliased to one register keeps the lower half's write to it vf5 (200, 400, 600, 800) (2, 4, 6, 8)

The remaining new tests, and what each does on the tree before this change:

Test Behaviour on the tree before this change
EFU lower ops report their fs read and WAITP reports none Fails at the classifier: every EFU selector reports neither read nor write, the same defect §1 shows.
R-register lower ops report their VF operand Fails at the classifier: every R selector reports neither read nor write, the same defect §1 shows.
EFU pair with no VF write leaves the reordered branch behaviourally unchanged Negative control. The pair does not reorder, so ERLENG reads the post-write vf3: state().p is 0.019231 (1 over 52) against an expected 0.076923 (1 over 13). The vf3 write is (12, 16, 48, 4) before and after.
lower-special ops that name no VF operand report neither read nor write Passes, by design — it pins selectors the classifier leaves unclassified, which were already unclassified. It is pinned by mutation P16 instead.

5. First commit stands alone

The classifier widening and its tests were built and run with
ps2_vu1_core.cpp at its pre-change state. The build succeeded and every test
passed, including the EFU tests: the classifier change is observable end-to-end
through state().p without any change to run().

6. Mutation table

Each mutation was applied alone to the fixed tree, rebuilt, and run with the
§3 command; the listed tests fail and the rest of the suite stays green. The
tree was restored between rows with git checkout <file> && touch <file>, so
ninja would not skip a stale object. P2/P3 and P6/P7 are mirrors, so a
one-sided fix is still caught. P18, P19 and P20 each add a bit to an arm
from the instruction field that arm does not name; the classifier tests encode a
non-zero is and a non-zero it together so that such a bit lands on a register
index the assertion can see.

# Mutation Tests that fail
P1 delete case 0x72: // ELENG from the EFU arm EFU lower op reads the pre-pair VF value the upper half overwrites; EFU lower ops report their fs read and WAITP reports none
P2 delete case 0x7E: // EEXP from the EFU arm EFU lower ops report their fs read and WAITP reports none
P3 add case 0x7B: to the EFU arm EFU lower ops report their fs read and WAITP reports none
P4 delete the vuAddGuardedReg(..., FS(decoded.upper), ...) call upper half reads the pre-pair value of an fs operand the lower half overwrites
P5 delete the vuAddGuardedReg(..., FT(decoded.upper), ...) call upper half reads the pre-pair value of an ft operand the lower half overwrites
P6 change if (guarded[i] != upperWriteReg) to if (true) upper half write discards the lower half write to the same register in whole-register units; upper-special ft destination discards the lower half write to that register; upper half destination is guarded even when it is not one of its own operands
P7 change the same if to if (false) upper half reads the pre-pair value of an fs operand the lower half overwrites; upper half reads the pre-pair value of an ft operand the lower half overwrites; upper half with fs and ft aliased to one register keeps the lower half's write to it
P8 delete the vuAddGuardedReg(..., upperWriteReg, ...) call upper half destination is guarded even when it is not one of its own operands
P9 change return ft; to return 0u; in vuUpperVfWriteReg's upper-special switch upper-special ft destination discards the lower half write to that register
P10 move the prePair snapshot loop to immediately after execLower upper half reads the pre-pair value of an fs operand the lower half overwrites; upper half reads the pre-pair value of an ft operand the lower half overwrites; upper half write discards the lower half write to the same register in whole-register units; upper-special ft destination discards the lower half write to that register; upper half destination is guarded even when it is not one of its own operands; upper half with fs and ft aliased to one register keeps the lower half's write to it
P11 delete case 0x73: // ERLENG from the EFU arm EFU pair with no VF write leaves the reordered branch behaviourally unchanged; EFU lower ops report their fs read and WAITP reports none
P12 delete m_state.q = prePairQ; upper half reads the pre-pair Q value the lower half overwrites
P13 delete m_state.q = postLowerQ; upper half reads the pre-pair Q value the lower half overwrites
P14 delete case 0x41: // RGET from the R-register arm R-register lower ops report their VF operand
P15 delete case 0x42: // RINIT from the R-register arm R-register lower ops report their VF operand
P16 add case 0x6C: to the R-register read arm lower-special ops that name no VF operand report neither read nor write
P17 change if (regs[i] == reg) in vuAddGuardedReg to if (false) upper half with fs and ft aliased to one register keeps the lower half's write to it
P18 add vuSetRegBit(writeMask, it); to the EFU arm EFU lower ops report their fs read and WAITP reports none
P19 add vuSetRegBit(readMask, is); to the RNEXT/RGET arm R-register lower ops report their VF operand
P20 add vuSetRegBit(writeMask, it); to the RINIT/RXOR arm R-register lower ops report their VF operand

7. Duplicate test-name check

grep -o 'tc.Run("[^"]*"' ps2xTest/src/ps2_vu1_tests.cpp | sort | uniq -d

Produces no output, so no case in ps2xTest/src/ps2_vu1_tests.cpp is dropped
by the std::map that keys the harness's test registration.

8. Mirror sweep

Axis Cells Status
A. Direction of the intra-pair dependency lower reads what upper writes asserted — EFU lower op reads the pre-pair VF value the upper half overwrites, and the pre-existing lower side sees old VF value when upper writes the same register
upper reads what lower writes asserted — upper half reads the pre-pair value of an fs operand the lower half overwrites and its ft variant
B. Reorder-predicate polarity fires when it should asserted — EFU lower op reads the pre-pair VF value the upper half overwrites
does not fire when it should not asserted at the classifier for WAITP and the unallocated EFU slots; behaviourally unreachable-by-proof, since none of them writes any state — WAITP's arm is a bare return; and the unallocated slots have no arm at all, per §2 — so no state differs between orders
C. Which upper operand carries the hazard fs asserted
ft asserted
both aliased to the same register (fs == ft) asserted — upper half with fs and ft aliased to one register keeps the lower half's write to it. The de-duplication in vuAddGuardedReg is what makes the restore replay one entry instead of two, and P17 is its mutation
D. Which field supplies the upper half's destination fd (ordinary upper ops) asserted
ft (ITOF*/FTOI*/ABS) asserted — upper-special ft destination discards the lower half write to that register
accumulator / no VF destination unreachable-by-proofvuUpperVfWriteReg returns 0, so the predicate never enters the changed branch
E. Write-collision resolution collided register is the upper's destination — lower write discarded asserted
collided register is not the upper's destination — lower write survives asserted
F. Field overlap of the two writes overlapping fields asserted — ABS.xy over a full-width MOVE
disjoint fields (the "in register units" rule) asserted — ADD.xy over MOVE.w, whose written fields are wholly disjoint; and the z/w fields of ADD.xy over a full-width MOVE, which overlaps on x/y and is disjoint on the fields the assertions name
G. Lower half's VF write mask non-empty guarded set asserted throughout the run() tests
empty guarded set (lower writes no VF register) asserted — EFU pair with no VF write leaves the reordered branch behaviourally unchanged, plus the pre-existing SQ hazard test staying green
H. Branch of run() taken lowerBeforeUpper asserted throughout
plain upper-then-lower not edited; guarded by the existing suite staying green
LOI (iBit) unreachable-by-prooflowerBeforeUpper is computed as !iBit && ..., so an LOI pair can never enter the changed branch
I. Classifier coverage of the EFU block opcodes with a real execution arm (ELENG, ERLENG, ERCPR) end-to-end asserted for ELENG and ERLENG; ERCPR asserted at the classifier only — open
opcodes with an empty or absent arm asserted at the classifier; behaviourally unreachable-by-proof
WAITP and the unallocated slots asserted at the classifier to report neither read nor write
J. Register index 0 vf0 as a guarded register unreachable-by-proofvuSetRegBit never sets bit 0, and vuAddGuardedReg separately rejects reg == 0
K. Sign of asserted zeros any expectation with magnitude 0 none exist — every asserted component is non-zero in both the before and after state
L. Which register file carries the intra-pair dependency VF asserted — every run() test above
Q asserted — upper half reads the pre-pair Q value the lower half overwrites
I unreachable-by-proof — the only instruction that writes I is LOI, which is the iBit branch and can never enter the reordered branch
ACC unreachable-by-proof — no lower operation references ACC, so no lower half can clobber it inside a pair
P unreachable-by-proofP is written by EFU lower operations and read only by the lower operation MFP; a pair holds one lower instruction, and no upper instruction reads P
R unreachable-by-proof — same shape as P: written and read only by lower operations
inter-pair Q/P visibility latency out of scope by declaration — see §11
M. Classifier coverage of the R-register block RNEXT/RGET (VF[ft] write) asserted at the classifier; behaviourally unreachable-by-proof — neither has an execution arm in this tree
RINIT/RXOR (VF[fs] read) asserted at the classifier; behaviourally unreachable-by-proof — same
lower-special selectors that name no VF operand asserted to report neither read nor write for the §2 list; WAITP and the unallocated EFU slots are covered by axis I

9. Hardware citations

Source: Sony VU User's Manual, version 6.0, section 3.4 ("Pipeline Operation")
except where noted. Each citation was read from a rendered page image rather
than extracted text.

  • Section 3.4.1 ("Hazards") — data hazards are not generated for the
    special registers ACC, I, Q, P and R; the same paragraph continues:
    "It is possible to make the Q and P registers synchronize with each other by
    using the WAITQ/WAITP instruction."
    Separately, VF00 is a constant register
    and is not subject to hazard checks. The exemption is about interlocks, not
    about which value a concurrently issued instruction observes.
  • Section 3.4.2 ("Upper Instruction and Lower Instruction") — the Upper and
    Lower instructions are issued concurrently, so both instruction pipelines
    stall if hazards occur to either of them.
  • Section 3.4.3 ("Priority for Writing to a Register") — when the Upper and
    Lower instructions write the same register at the same time, priority is given
    to the Upper instruction and the result of the Lower instruction is discarded.
    This holds in register units: "the result of the Lower instruction is
    discarded even when the Upper and Lower instructions write data to different
    fields"
    . Worked example: ADD.xy VF01, VF01, VF23 as the upper half paired
    with MOVE.w VF01, VF09 as the lower half.
  • Section 3.4.4 ("FMAC Pipeline") — stalls due to data dependency do not
    occur between the Upper instruction and the Lower instruction in the same
    instruction.
  • Section 3.4.5 ("FDIV Pipeline")DIV and SQRT reach their write-back
    stage only at the end of the FDIV pipeline, and RSQRT at the end of a
    longer one. Q is therefore not visible to the pair that issued the divide.
  • RGET and RNEXT instruction pages, Remarks"When an Upper
    instruction in the same cycle writes data to the VF[ft] register, the result
    of this instruction is discarded with priority given to the Upper instruction,
    regardless of whether the data is written to the same field or not."

10. Blast radius

Behaviour that changes, and becomes more faithful to the hardware model:

# What changes Reachable when
1 A pair whose upper half writes a VF register an EFU lower operation reads now reorders, and the EFU operation reads the pre-pair value the EFU opcode has an execution arm: ELENG, ERLENG, ERCPR
2 A reordered pair whose lower half writes one of the upper half's source registers now feeds the upper half the pre-pair value any reordered pair with that dependency
3 A reordered pair whose lower half writes the upper half's destination register now has that write discarded entirely, including fields the upper half does not itself name any reordered pair with that collision — see the note below
4 A reordered pair whose upper half reads Q and whose lower half writes it now feeds the upper half the pre-pair Q a DIV, SQRT or RSQRT shares a pair with a q-form upper operation and the pair also hazards on VF

Row 3 is the only one that removes a write that previously landed. Before this
change, a reordered pair whose lower half wrote a field of the upper half's
destination that the upper half did not itself write kept that field from the
lower half's write. That field is now discarded with the rest of the register,
matching the §9 worked example. A microprogram whose observable output depended
on the old per-field behaviour changes output.

Behaviour provably unchanged:

What Why
Every pair for which the reorder predicate returns false the classifier only adds read and write bits to selectors that previously reported none, so a pair the widened predicate rejects was rejected before it too, and both trees run the unedited execUpper; execLower branch
Every LOI pair lowerBeforeUpper is !iBit && ..., so LOI never enters the changed branch
A pair that reordered before this change too, and whose lower half writes neither a VF register nor Q the guarded set is empty and postLowerQ equals prePairQ, so the branch runs the previous execLower; execUpper sequence and nothing else. Stores and MTIR are in this set, and the pre-existing SQ hazard test is an instance of it. DIV, SQRT and RSQRT write Q, so their pairs are not: they are changed row 4
A pair this change newly reorders whose lower half has an empty or absent execution arm the lower half mutates no state, so execLower; execUpper and execUpper; execLower leave the same machine state and the predicate's new answer is not observable. §2 gives the arm state of every selector this change reclassifies, across both the EFU and the R-register block; all of them are in this set except ELENG, ERLENG and ERCPR, which are changed row 1
Everything outside the VU1 interpreter no public header changes, so no effect on the recompiled-code ABI
Inter-pair Q and P visibility unchanged in both directions

Cost, on the hazard branch only: two extra classifier calls — the lower half's
VF write mask and the upper half's VF destination — and then, at most, one
16-byte register copy before the lower half runs, two between the halves (the
guarded register is captured and then rolled back), and one after the upper
half runs. guardedCount reaches one and goes no higher: no arm of
vuLowerVfReadWriteMasks sets more than one write bit, and vuAddGuardedReg
admits only a register that bit names. Its de-duplication is what holds the
count there when more than one of the upper half's fs, ft and destination
names that register, which is the shape mutation P17 kills; and the
guarded[3] array is sized by its three call sites, not by this bound. Pairs
that do not hazard are untouched, and the decode cache is unaffected.

The write-bit bound holds over every 32-bit lower encoding:

cat > /tmp/wmask_sweep.cpp <<'EOF'
#include "vu/ps2_vu1_detail.h"
#include <cstdio>
int main()
{
    int worst = 0;
    for (uint64_t v = 0; v <= 0xFFFFFFFFull; ++v)
    {
        uint32_t r = 0, w = 0;
        vuLowerVfReadWriteMasks((uint32_t)v, r, w);
        const int bits = __builtin_popcount(w);
        if (bits > worst)
            worst = bits;
    }
    std::printf("max write bits = %d\n", worst);
    return 0;
}
EOF
g++ -O2 -std=c++20 -Ips2xRuntime/src/lib -o /tmp/wmask_sweep /tmp/wmask_sweep.cpp && /tmp/wmask_sweep
max write bits = 1

11. What stays open

  • ERCPR end-to-end (axis I) is asserted at the classifier only. ELENG
    and ERLENG carry the end-to-end assertion for the EFU arm.

  • Inter-pair Q/P visibility latency is unchanged and still wrong. This
    tree makes a Q or P write visible immediately rather than at the end of
    the FDIV or EFU pipeline, which is a latency model. Intra-pair ordering is
    closed here, and the inter-pair window is the same width as before.

  • No WAITQ/WAITP sequencing. DIV/SQRT/RSQRT write Q immediately
    and EFU operations write P immediately, so there is no pending state for
    WAITQ/WAITP to interlock against and no test could pin a change.

  • The plain upper-then-lower branch and the LOI branch have no mutation.
    They are unedited code, and decodeInstructionPair still computes
    lowerBeforeUpper as !decoded.iBit && vuLowerShouldRunBeforeUpper(...);
    they are backed by the existing suite staying green under §3.

  • Opcodes with an empty or absent execution arm have no end-to-end
    mutation.
    Their classification is asserted directly at the classifier, but
    no mutation of their cases can be observed through run() today.

  • vuAddGuardedReg's lowerWrites filter has no mutation row. Dropping the
    ((lowerWrites >> reg) & 1u) == 0u test is behaviour-preserving here, so no
    test can distinguish it and none is added. It can only add registers the lower
    half did not write; for those postLower equals prePair, making the rollback
    and the re-apply no-ops. The re-apply cannot land on the upper half's own write
    either: the VF destinations execUpper uses are m_state.vf[fd] for
    op <= 0x2F and m_state.vf[ft] for the special selectors, matching what
    vuUpperVfWriteReg returns, and that register is excluded by
    if (guarded[i] != upperWriteReg). Check both statements with

    grep -n 'applyDest(m_state.vf\[' ps2xRuntime/src/lib/vu/ps2_vu1_lower.cpp
    grep -n 'float \*vd = \|float \*vtDest = ' ps2xRuntime/src/lib/vu/ps2_vu1_upper.cpp
    grep -n 'applyDest(vtDest,' ps2xRuntime/src/lib/vu/ps2_vu1_upper.cpp

    The filter is an optimisation: it keeps the guarded set to the registers that
    need guarding. If the classifier is ever found to under-report a lower-half VF
    write, dropping the filter is the safe response, which is a further reason not
    to pin it.

None of these is described as closed anywhere in this document or in
PR-BODY.md.

vuLowerVfReadWriteMasks stopped at MFP and reported no read or write
for the entire EFU opcode block (ESADD through EEXP), even for ELENG,
ERLENG and ERCPR, which this interpreter already implements as reading
VF[fs]. Add the thirteen EFU cases so the pair reorder predicate can
see the hazard; WAITP and the two unallocated EFU slots stay excluded
since they take no vector operand.

The read is reported regardless of whether an opcode has an execution
body today, so implementing one later cannot silently reopen the gap
the table exists to close.
Both halves of a VU1 instruction pair issue concurrently, so both must
observe the register state that predates the pair, and a VF write
collision resolves in favour of the upper half for the whole register,
not just the fields it names (VU User's Manual, section 3.4.3,
"Priority for Writing to a Register"). The lowerBeforeUpper branch of
run() previously ran the lower half first only to satisfy the
lower-reads-upper direction of the hazard; the upper half still saw
whatever the lower half had just written, and any lower write that
collided with the upper half's destination survived on the fields the
upper half did not name.

Snapshot the VF registers the upper half's fs, ft and destination touch
before the lower half runs, restore them for the upper half, and
re-apply the lower half's write afterward everywhere except the upper
half's own destination register. Snapshot and restore Q the same way:
DIV, SQRT and RSQRT write Q from the lower half and the upper half's
q-form operations read it, so the reorder fed the upper half a Q the
lower half had just produced. No upper instruction writes Q, so Q is
restored unconditionally and needs no priority arbitration. Q is the
only register outside the VF file where this arises - no lower
operation writes ACC or I, and P and R are read only by lower
operations, of which a pair holds exactly one.

Section 3.4.1 ("Hazards") exempts ACC, I, Q, P and R from hazard
generation, but that exemption is about interlocks, not about which
value a concurrently issued instruction observes; the same paragraph
points at WAITQ/WAITP for synchronising Q and P. Backed further by
section 3.4.2 ("Upper Instruction and Lower Instruction", concurrent
issue) and section 3.4.4 ("FMAC Pipeline", no intra-pair dependency
stalls).

Also classify the four R-register lower operations the hazard table
still left at its default arm: RNEXT and RGET write VF[ft], RINIT and
RXOR read VF[fs]. None has an execution body in this tree, so nothing
observable changes today; leaving them unclassified would have let an
implementation of RGET or RNEXT bypass both the reorder predicate and
the write-priority machinery. RGET's own instruction page is where the
manual states that whole-register discard rule for a lower operation.
That completes the lower-special selector space: every selector the
manual allocates that names a VF operand is now reported, and the ones
that name only integer registers or no operand are pinned absent.

One direction of this change removes a write that previously landed: a
reordered pair whose lower half wrote the upper half's destination now
has that write discarded in whole-register units, including fields the
upper half did not touch itself.

Inter-pair Q and P visibility latency is unchanged and remains out of
scope. This tree still makes a Q or P write visible immediately rather
than at the end of the FDIV or EFU pipeline.
@smmathews

smmathews commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

(I understand you're re-working vu1, 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