Skip to content

fix(vu1): maintain MAC/STATUS/CLIP flag registers, correct the flag-reading lower-op table, and saturate FTOI - #189

Draft
smmathews wants to merge 1 commit into
ran-j:mainfrom
smmathews:feature/26-vu1-flag-model
Draft

fix(vu1): maintain MAC/STATUS/CLIP flag registers, correct the flag-reading lower-op table, and saturate FTOI#189
smmathews wants to merge 1 commit into
ran-j:mainfrom
smmathews:feature/26-vu1-flag-model

Conversation

@smmathews

@smmathews smmathews commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Problem

VU1State carries mac, status and clip, and the FM, FS and FC compare ops that read them
have been present throughout. Nothing in the FMAC pipeline ever wrote mac. Before this PR:

git grep -n "m_state\.mac" -- ps2xRuntime/src/lib/vu     # 3 reads, 0 writes
git grep -n "m_state\.status" -- ps2xRuntime/src/lib/vu  # FSSET is the only write

FMAND/FMEQ/FMOR therefore always read zero. Further defects sit in the same path:

  • The lower-op table has the FMAND body at 0x18 and the FMEQ body at 0x1A (swapped), nothing at
    0x1B (FMOR), and the FMOR body at 0x1C, which is FCGET.
  • m_state.clip = (m_state.clip << 6) | flags; accumulates unmasked. After five CLIPs, bits
    above 23 are set permanently and FCOR's (clip | imm24) == 0xFFFFFF can never go true again.
    FCSET masks its own write; the shift-register accumulation is what is unmasked.
  • FTOI0/4/12/15 cast the scaled float straight to int32_t. An out-of-range cast is undefined
    behavior; on x86 cvttss2si returns 0x80000000 whatever the sign, so a large positive
    input produced INT_MIN.

Fix

  • applyDest splits into writeDestMasked (the destination-mask copy) and computeFmacFlags,
    so which sites raise flags is visible at the call site (table below).
  • MAC is computed per DEST lane and folded into STATUS: a live half (bits 5:0) replaced by every
    FMAC, and an OR-only sticky half (bits 11:6) that FSAND/FSOR read.
  • Lower-op table corrected to 0x18 FMEQ, 0x1A FMAND, 0x1B FMOR (added), 0x1C FCGET (added; reads
    CLIP's low 12 bits, guarded on a nonzero destination).
  • FSSET extracts imm12 = ((instr >> 21) & 1) << 11 | (instr & 0x7FF) and writes the sticky half
    only, leaving the live half as the last FMAC set it. It previously used (instr >> 6) & 0xFC0.
  • CLIP accumulation is masked with & 0xFFFFFF.
  • FTOI saturates: a scaled magnitude of 2^31 or more, tested on the exponent field so inf and NaN
    are covered, clamps to 0x7fffffff or 0x80000000 by sign bit; otherwise the existing cast runs.

Reference basis

Sourced against PCSX2:

  • VUflags.cpp, VU_MAC_UPDATE / VU_STAT_UPDATE — MAC nibble layout, paired U+Z on a flushed
    denormal (0x0101 << shift), per-lane clear outside DEST, live/sticky STATUS fold.
  • VUops.cpp, _LOWER_OPCODE[128] — 0x18 FMEQ, 0x19 unassigned, 0x1A FMAND, 0x1B FMOR, 0x1C FCGET.
  • VUops.cpp, _vuFSSET — the immediate extraction.
  • VUops.cpp, _vuCLIP — the 24-bit mask after each accumulation.
  • VUops.cpp, floatToInt<Offset> — sign-based clamp with no separate NaN case.

MAX/MINI are excluded from the flag path: the min/max helper (applyMinMax) performs no flag
update.

Testing

cmake -S . -B build -DCMAKE_CXX_FLAGS="-msse4.1 -include cstdint" -DCMAKE_C_FLAGS=-msse4.1
cmake --build build --target ps2x_tests
./build/ps2xTest/ps2x_tests

New cases in the PS2VU1 suite pin:

  • MAC Z and S on zero and negative results; U and Z together on a denormal; lanes outside DEST
    reading 0.
  • MAX and MINI, then ITOF0/FTOI0/ABS, placed between a producing FMAC and an FMAND, all leave
    MAC unchanged.
  • The STATUS live half resetting per FMAC while the sticky half accumulates; FSSET preserving the
    live half and writing the sourced immediate into the sticky half.
  • FMEQ, FMAND, FMOR and FCGET decoding at their corrected opcodes.
  • Five CLIPs that overflow the 24-bit window, after which FCOR goes true.
  • FTOI4 clamping ±1e30 to INT_MAX/INT_MIN, converting ±0.5 and 0.0 exactly, and clamping a
    positive NaN to INT_MAX.

Risk and not in scope

  • The decode cache from Feature vu1 cache #158 caches the decode of an instruction pair (opcode fields, iBit/eBit,
    order), not execution results, so a cached instruction still recomputes MAC/STATUS/CLIP from
    current operands each run.
  • No pipeline latency is modelled: the FDIV/Q delay, the EFU/P delay, and a multi-cycle flag or
    CLIP commit delay all stay immediate. Each needs its own citation.
  • STATUS bits 4, 5, 10 and 11 (I and D, live and sticky) are not computed by computeFmacFlags.
MAC layout and flag-call membership

MAC is 16 bits. Within each nibble the lanes run x, y, z, w from the high bit to the low bit.

Field Bits Set when
Z 3:0 result is ±0.0, or a denormal (an underflow flushed to zero, so U and Z fire together)
S 7:4 IEEE754 sign bit, unconditionally
U 11:8 exponent field 0 with a nonzero mantissa
O 15:12 exponent field all ones

Z and U can be set together. Lanes outside DEST are not evaluated and read 0 in every field.

Call site group writeDestMasked computeFmacFlags
FMAC arithmetic vf-writers: ADD/SUB/MUL/MADD/MSUB and their bc/q/i variants, OPMSUB yes yes
FMAC ACC-writers: ADDA/SUBA/MULA/MADDA/MSUBA and variants, OPMULA yes yes, inside applyDestAcc
MAX/MINI and their bc/i variants yes no
ITOF0/4/12/15, FTOI0/4/12/15, ABS yes no
Lower-op vf writers: LQ, MOVE, MR32, LQI, LQD, MFIR, MFP yes no

The ACC-writer group is uniform, so applyDestAcc bundles both calls; MAC reflects the FMAC result
whether the destination is a vf register or the accumulator. The vf-writer group is mixed, so the
flag call stays a separate line at each site.

…eading lower-op table, and saturate FTOI

The VU1 interpreter declared mac/status/clip fields and the lower-op instructions
that read them, but the upper FMAC pipeline never wrote mac, and STATUS was
written by exactly one instruction using an unsourced immediate extraction. Every
arithmetic FMAC vf-writer and ACC-writer now computes MAC over its DEST lanes and
folds it into a STATUS live/sticky pair; MAX/MINI (which the PS2 FMAC pipeline
runs without any flag update, matching PCSX2's applyMinMax) and the nine
ITOF/FTOI/ABS unary ops and the plain dest-mask lower ops (LQ, MOVE, MR32, LQI,
LQD, MFIR, MFP) are explicitly excluded from the flag path.

Separately, the flag-reading lower-op table at 0x18-0x1C had two of its four case
bodies swapped (0x18/0x1A) and was missing FMOR (0x1B) and FCGET (0x1C, which ran
a duplicate FMOR body instead). FSSET's immediate extraction is replaced with the
formula sourced from PCSX2's reference VU interpreter, since none of the formulas
previously in circulation for this codebase matched it. CLIP's shift-register
accumulation is now masked to 24 bits, since without it FCOR permanently loses
the ability to compare true once five or more CLIP instructions have run. FTOI0/
4/12/15 now saturate on overflow instead of relying on undefined behavior, which
on x86 silently produced INT_MIN for large positive values too.

All of this is sourced against PCSX2 pcsx2/VUops.cpp and pcsx2/VUflags.cpp (see
EVIDENCE.md); pipeline latency (FDIV/Q, EFU/P, and any multi-cycle flag commit
delay) is out of scope here and left for a follow-up with its own citation.
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