mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
iOSv2.5.2
420
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
38ba87f359 |
Test: pin the multiplier deficit in the upper binade
The Booth predicate is keyed to fixed bit positions of ft, but the truncation column moves one bit between the binades -- and every zero-tail row I measured sat in the lower binade, at fs = 2^23. So the region where the decrement fires with the product in the upper binade had never been observed on silicon. The fpmul3 capture already had it. The counts and the row provenance are at the tables. One dead end worth recording, since the data invites it: do not split this by binade, split it by T. fs = 0xFFFFFF looks like the predicate carries no information there (0.1907% low with Booth on, 0.1976% with it off), but it has exactly one T == 0 row, so that is measuring the unmodelled 0 < tail < deficit class instead. At fs = 0x800001, lower binade and also almost entirely that class, Booth is strictly necessary: 13,248/13,248 on, 0/262,144 off. |
||
|
|
e1f3048cc4 |
Optimization: pin the multiplier's predicate mask
emitDefectiveFmul materialised the 0x2AA Booth-digit mask on every
mode-3 multiply (mov + fmov). It is now placed into d10 for the whole
JIT session by _DynGen_EnterRecompiledCode, next to the s8/s9 clamp
scalars and under the same AAPCS64 argument: the low 64 bits of d8-d15
are callee-saved, so a pinned constant survives every C call with no
compile-time tracking. Six instructions become four:
cmtst d30, d11, d10 ; d11 = narrow guest ft, d10 = the parked mask
fmul d0, d0, d1
fcmeq d31, d0, #0.0
bic v30.8b, v30.8b, v31.8b
add d0, d0, d30
Why a pinned callee-saved register beats a caller-saved one behind a
liveness flag, and why q10 specifically, is on NEON_RESERVED_FPU_MULMASK
in iCore-arm64.h. It costs one of the six call-surviving FPR homes.
|
||
|
|
bfc99f2bf3 |
Accuracy: give clamp mode 3 the EE multiplier's one-ULP deficit
The console's multiply array does not round correctly. When the exact product has nothing below the single's ULP to absorb it, the result comes back exactly one step closer to zero -- and whether it does is decided by ft's mantissa alone, so mul.s is not commutative. iFPUd, the eeClampMode:3 codegen, computed the IEEE product; the interpreter has modelled the law since the previous commit. Six instructions per multiply, at both emit sites: recMULop, and recMaddsub's multiply stage, which round through different helpers. The mechanism, the FPCR dependency it rests on, and the one term of the measured predicate it knowingly drops are documented at emitDefectiveFmul. kGuardMaskWitnesses moved on 35 of its 72 rows -- fs is 1.0 throughout, so every product there is zero-tailed. Each was re-derived against the measured law rather than re-pinned against the emitter that moved it; the one row where the dropped boundary term decides is called out at the table. |
||
|
|
9d7d11cd0e |
Fix: model the EE multiplier's one-ULP deficit in the interpreter
The console's multiply array is not a correctly-rounding multiplier: it comes back exactly one step closer to zero on a large fraction of operands, and which operands depends on operand order. Upstream states the rule in a comment (x86/iFPU.cpp) and never tests it; FpuMulHack is a one-point sample of it. What is modelled, what deliberately is not, and where the rule was measured are at eeMulDefectiveFt. All six multiply forms route their product through the new eeMulProduct, which also localises the fpuDouble() calls the sites used to make inline. MADDA/MSUBA gain a temp for it; the accumulate itself is unchanged, and still does not round-trip the product through fpuDouble the way MADD/MSUB do. MulSFpuMulHackOffGivesNativeProduct asserted the opposite -- that the IEEE product is the console's. Silicon returns 0x3F490FDA for those operands and 0x3F490FDB reversed, so the gamefix matched hardware in both orders all along. The deficit only reaches what fpuDouble() hands it. An exponent-0xff operand is clamped to +/-Fmax before the multiply and the product saturates at FLT_MAX, so this engine still has no EE top binade; that gap is fpuDouble's and is untouched here. |
||
|
|
ebf7124f25 |
Test: pin the MADD family's guard mask and rounding
Two new tests exist so that the format-churn work that follows has something to fail against. |
||
|
|
8f67945c9e |
Fix: RSQRT.S's zero-divisor sign comes from the dividend
Two sign rules, and they are not the same rule. DIV.S takes the xor of both operands. RSQRT.S takes the DIVIDEND's sign alone -- it divides by sqrt(|Ft|), so the divisor has no sign left to contribute by the time the division happens. Both of our engines took Ft's sign, and the arm64 emitter was alone among recompilers in it: x86 recRSQRThelper1 (iFPU.cpp) has always taken Fs's. The console rows that separate the rules: rsqrt(+0, -0) is positive and rsqrt(-0, -0) is negative on silicon; an xor rule, or Ft's sign, flips both. Fixing the sign moves the arm64 emitter's agreement with the console and with the x86 JIT, and keeps the two local engines in exact agreement on the whole zero path. The MAGNITUDE stays at the fast tier's +/-fMax saturation. Silicon returns 0x7FFFFFFF there -- the EE's real maximum, one binade up -- but that is the top-binade compromise shared by every fast-path op, not the sign rule, and it moves as a class or not at all. Pinned by EeRecFpuRsqrt.ZeroDivisorSignComesFromTheDividend, six rows across both zero-sign combinations and nonzero dividends, both engines diffed. DenormalDivisorTreatedAsZero's expectation flips to the new rule. Idea by pstef. |
||
|
|
665533c738 |
Fix: MAX.S/MIN.S select a word on silicon, the fast path computed one
The EE does not take a maximum, it picks one: the two raw words are
ordered by (sign, magnitude) and the winner's 32 bits are written
through untouched. That is fp_max/fp_min in FPU.cpp, and it is what the
interpreter and the DOUBLE tier have always done.
The arm64 fast path clamped both operands to +/-fMax and then used
Fmaxnm/Fminnm, which loses two whole operand classes:
denormals Fmaxnm/Fminnm are arithmetic ops, so FPCR.FZ flushed the
operand first and the winner's word was destroyed:
max(0x00000001, 0x00000000) read back 0x00000000 where
the console says 0x00000001.
exponent 255 the clamp folded the entire top binade onto 0x7F7FFFFF:
max(0x7F7FFFFF, 0x7FFFFFFF) read back 0x7F7FFFFF where
the console says 0x7FFFFFFF.
Both classes are exactly what ABS.S/NEG.S carried until cbf04acba1, for
exactly the same two reasons.
Replaced with an integer ordering key, k(x) = x ^ ((x >>s 31) >>u 1),
compared signed and resolved with a Csel between the untouched
originals -- nine instructions, and no arithmetic for FZ to act on. The
scratch registers stay inside fpuEmitGuardedAddSub's contract
(w0/w1/w8/w9), so a resident FCR31 in the x2-x7/x14/x15 pool is safe.
Measured on the 1147-case SCPH-90000 capture, corpus v3:
MAX 28/66 -> 66/66, MIN 50/66 -> 66/66
whole corpus, result axis: 755 -> 809 of 1147
54 cases changed, 54 onto the console value, 0 away, 0 outside MAX/MIN
FCR31 axis unmoved at 978
and 66/66 on both ops in all five regimes measured: eeClampMode 0/1/2,
fpuFullMode, and DenormalsAreZero off. The interpreter column is
untouched at 1067 and remains the control.
CHECK_FPU_OVERFLOW now gates no arm64 emitter path at all -- SQRT.S gave
up its operand clamp in 1a09344ba6, ABS.S in cbf04acba1, and MAX/MIN
here. The knob is still live on x86 and still set from the GameDB, but
eeClampMode 0 and 1 emit identical code on this port. The liveness
witness that rode on it is retired in place, with that stated, rather
than replaced by one that cannot fail.
Four tests in ee_rec_fpu_tests.cpp asserted the clamp and are inverted
here; their premise was that the x86 JIT is the FPU-clamp oracle, which
the capture refutes (upstream's fast tier is wrong on 41 MAX and 22 MIN
of the same cases). New file ee_fpu_minmax_console_tests.cpp carries the
54 distinct console triples, the aliased register forms, the O|U clear
against capture rows 734/735, and the FCR31-residency hazard. It fails
on 3 of 6 tests against the unpatched emitter and passes on all 6 here.
Idea by pstef.
|
||
|
|
5dbb14458d |
Fix: the fast path never cleared the O and U cause flags
The EE clears the O and U CAUSE bits (the sticky SO/SU survive) on every op that can raise them, whether or not it does: ADD, SUB, MUL, the four A-forms, the four multiply-accumulates, and MAX/MIN/ABS/NEG, which clear the pair and do nothing else. DIV, SQRT, RSQRT and MOV leave both alone. Measured on FCR31-seeded capture rows: ABS, NEG, ADD, ADDA, MADD, MSUB, MUL, MULA, MAX and MIN all read back 0x0183C079 where the console gives 0x01830079; SUB, SUBA, MADDA and MSUBA have no seeded row and follow on the interpreter's authority (checkOverflow/checkUnderflow/clearFPUFlags clear the pair on all fourteen). The arm64 fast path cleared the pair only on ABS/NEG, so an O or U raised by an earlier instruction stayed visible to every later cfc1 in the block. The interpreter has always cleared them, which made this a live JIT-vs-interp FCR31 divergence as well as a console one. x86 iFPU.cpp has the identical defect -- the clear is commented out at 13 sites. The clear goes FIRST in each emitter, before the op writes anything: the fast path raises neither flag today so the order is not yet observable, but an emitter that later learns to raise O must not have its flag wiped by a clear placed after it. One Bic on the block-resident FCR31 per op. RAISING O and U is a separate, harder obligation -- a correct raise needs the exact magnitude of the result, which a saturating single cannot carry -- and stays with the FULL tier and the DISABLED tripwires in the FCR conformance file. Pinned by EeFpuFcrConsoleConformance.EnginesAgreeOnTheOverflowFlagClear: fourteen clearing ops plus the four leave-alone controls, both engines, seeded with the capture's word. Idea by pstef. |
||
|
|
ac88a41f95 |
Fix: ABS.S/NEG.S clamped operands the console passes through
The arm64 fast path clamped both results to +/-fMax. The EE does neither:
ABS.S is `& 0x7fffffff` and NEG.S is `^ 0x80000000`, which is what the
interpreter has always done, what the FULL path (DOUBLE::recABS_S_xmm) has
always emitted, and what silicon does. The clamp corrupted 22 of the 54
ABS/NEG operands in the first-party capture, in two distinct ways:
* exponent-255 in, +/-fMax out (16 rows). Those are ordinary large PS2
floats, not infinities -- abs(7F800000) is 7F800000, not 7F7FFFFF.
* denormal in, ZERO out (6 rows), on ABS only. Its clamp was an Fminnm,
an ARITHMETIC op, so FPCR.FZ flushed the operand before the compare
happened. NEG's clamp was an integer Smin/Umin and never did this,
which is exactly why the defect showed on one op and not the other --
and why an operand pool built only from exponent-255 patterns missed
it entirely.
Fabs and Fneg alone are correct and total: non-arithmetic bit operations,
no exceptions, no flush, payloads through with only the sign changed.
Found while removing SQRT.S's operand clamp (1a09344ba6) -- same finding,
one op over. Note the upstream x86 JIT is wrong on the same 22 rows; both
interpreters are right on all 54. This aligns our JIT with our interpreter
and with the console, and diverges it from upstream-x86, which is not a
cost when upstream-x86 is not the reference.
Second, independent defect in the same two emitters, fixed here because it
lives on the lines being rewritten: the fast path never cleared the O and U
cause flags. Interp ABS_S/NEG_S call clearFPUFlags(FPUflagO | FPUflagU) and
the FULL path emits ClearOUFlags; only the fast path skipped it, so an
overflow raised by an earlier op survived an ABS.S. Capture rows 729/730
seed FCR31 with flags set and confirm it against silicon: FCR31 goes
0183C079 -> 01830079, which is hardware's value.
Verified over the full 1147-case corpus, both engines, stock regime, on top
of the SQRT fix: 34 engine-cases moved, all 34 onto the silicon value, 0
away, 0 outside the two expected classes, 2260 identical. The 22 ABS/NEG
moves are arm64-JIT-only -- the interpreter did not move, which is the
control that its console rows were not quietly re-fitted.
EeFpuAbsNegClamp.DISABLED_JitMatchesConsoleInEveryClampMode is graduated.
Its console table gains 8 rows from the first-party capture covering the
denormal and signalling-NaN shapes ps2autotests does not reach, tagged by
source; the interpreter leg passes on those rows both before and after this
change, which is what validates the transcription independently of the fix.
EeRecFpu.NegSPreservesSignOnPoisonedNan pinned the second of three answers
this op has had (clamp losing the sign -> clamp keeping it -> no clamp). It
is rewritten to pin the console's answer and now runs the engine diff,
since its premise that no rec matches the interpreter no longer holds.
Idea by pstef.
|
||
|
|
ef6d720e39 |
Fix: SQRT.S of an exponent-255 operand, by scaling instead of clamping
Exponent 255 is an ordinary binade on the EE -- no Inf, no NaN, and the
representable max is 0x7FFFFFFF rather than FLT_MAX -- so an exponent-255
operand never needed saturating. Both engines clamped it to +/-FLT_MAX
anyway (the interpreter inside fpuDouble, arm64 with an integer Umin gated
on CHECK_FPU_OVERFLOW, mirroring x86's xMIN.SS) and landed two binades
below the console:
sqrt.s 7F800000 -> 5F7FFFFF, silicon 5F800000
sqrt.s 7FFFFFFF -> 5F7FFFFF, silicon 5FB504F3
sqrt.s 7FC00000 -> 5F7FFFFF, silicon 5F9CC471
All three engines agreed with each other and none agreed with the console.
Agreement is a weaker property than accuracy and it was bought at the cost
of accuracy.
Both engines now compute sqrt(|Ft|/4)*2. sqrt halves exponents, so the
scaled operand (exponent field 253) and the doubled result are both
ordinary singles: this needs no wider format and so leaves the fast path
single-precision, which is what the fast path is for. 4 is an even power of
two, so its own square root is exact and the identity contributes no
rounding -- the sqrt remains the only rounding step. It is the same
power-of-two prescale ToDouble() already uses to carry these operands into
FULL mode, with the factor picked to suit sqrt.
Ungated, because there was no mode in which the old code was right: with
CHECK_FPU_OVERFLOW off the same operands came back as 0x7F7FFFFF instead,
wrong a different way. Nothing with exponent field <= 254 is affected --
the old Umin was already a no-op on those, the new branch is not taken.
The JIT lands on the silicon value on every exponent-255 shape, both
signs, plus the exponent-254 control -- expected values computed
independently by exact integer arithmetic (no host float), validated
against silicon on the six witnessed operands. The interpreter moves two
binades onto the same values except where the sqrt is inexact in single
precision: there it narrows under the ambient ChopZero rather than the
divide unit's round-to-nearest and sits one ULP below silicon. That gap
predates this change, is documented as CLASS 3 in the conformance file's
divergence list, and closes when the interpreter models the div-unit
rounding law.
RSQRT.S deliberately unchanged: its two clamped operands currently cancel
on rsqrt(2^128, 2^128), so unclamping only the sqrt breaks a row that is
right today. It is all-or-nothing and is a separate change.
The two conformance tests that pinned the clamp are rewritten to pin the
console value instead, keeping their anti-vacuity clauses and gaining an
exponent-254 negative control. EeFpuAbsNegClamp's liveness witness for
DisableFpuOverflow() rode on SQRT's gate; it moves to MAX.S, now the only
remaining CHECK_FPU_OVERFLOW-gated emitter path.
Idea by pstef.
|
||
|
|
318dd102dc |
Tests: ABS.S/NEG.S against hardware — the EE never clamps them
The console says both are pure sign-bit operations. From ps2autotests
tests/cpu/ee_fpu/arithmetic.expected:
abs 7fffffff: 7fffffff neg 7fffffff: ffffffff
abs ffffffff: 7fffffff neg ffffffff: 7fffffff
abs 7f800000: 7f800000 neg 7f800000: ff800000
An exponent-255 operand comes back exactly, sign bit aside. The
interpreter reproduces every console row; the arm64 recompiler does not.
recABS_S_xmm and recNEG_S_xmm call fpuClampResultPositive/fpuClampResult
with no CHECK_FPU_* gate, so exp-255 operands all collapse to
±0x7F7FFFFF and eeClampMode has no effect whatsoever — x86 at least gates
ABS on CHECK_FPU_OVERFLOW, arm64 gates neither op on anything.
That defect is pre-existing (present at the merge-base), so the JIT leg
lands as a DISABLED tripwire, not a failing test. It fails on 30 of its
60 assertions today — the 5 exponent-255 rows × 2 ops × 3 clamp modes —
so it is live, not vacuous, and it should pass unchanged once the clamp
is removed.
Also promotes what was a printf-only measurement probe into assertions:
- InterpMatchesConsoleInEveryClampMode: enabled must-not-regress control
on the side that matches silicon.
- JitIgnoresEeClampModeForAbsAndNeg: pins the inertness itself, so wiring
the gate up fails here and points at the tripwire instead of going
unnoticed.
- DisableFpuOverflowReachesTheEmitter: liveness witness for the new
harness knob. DisableFpuOverflow() is observationally a no-op on
ABS.S/NEG.S precisely because they ignore the mode, so without this the
switch would ship with nothing proving it reaches the emitter. SQRT.S
is the discriminator — its operand clamp is gated on
CHECK_FPU_OVERFLOW, giving 0x5F7FFFFF clamped vs 0x7F7FFFFF not.
1551 pass / 0 fail / 27 disabled.
Idea by pstef.
|
||
|
|
525355bc58 |
Fix: SQRT.S raises invalid on -0 and negative denormals
Both this branch's engines gated SQRT.S's I|SI on `exp != 0 && sign`. The
console gates on the sign bit alone, so -0 and the negative denormals -- which
flush to -0 and produce a perfectly ordinary +0 -- raise invalid-operation
there too. That gate is why exactly those two operand classes lost the flag and
nothing else did.
From the first-party capture that records FCR31 alongside the result,
cases 227 and 236:
sqrt 80000000 : console 00000000/01020041 both engines 00000000/01000001
sqrt 80000001 : console 00000000/01020041 both engines 00000000/01000001
Read across all 38 SQRT.S rows the rule holds without exception: every
sign-set operand raises 01020041, every sign-clear one leaves 01000001,
whatever the exponent. Case 248, a POSITIVE qNaN, raises nothing -- it is the
sign bit and not "is this operand strange".
x86's recSQRT_S_xmm has always tested MOVMSKPS's sign bit alone (iFPU.cpp:1767),
which is why upstream-x86-jit is the one column in the capture that answers
both rows correctly, and the arm64 FULL-mode DOUBLE::recSQRT_S_xmm already
tested the sign alone too and was unaffected. So the fix is a deletion on both
sides -- the Tst(0x7F800000)/B.eq pair in the arm64 fast path, and the hoisting
of the flag set out of FPU.cpp's negative-normal arm. Neither touches the value
path or the |Ft| clamp the exponent-255 rows depend on.
Commit 6e1c28f fixed the value half of case 227 (the interpreter returned
_FtValUl_ & 0x80000000 and so answered -0 where the console answers +0). This
is the flag half of the same two rows, and case 236 is a second witness that
commit did not know about.
Verified bidirectionally. EeRecFpu.SqrtSInvalidFlagFollowsTheSignBitAlone is
the ten-row sign x exponent matrix from the capture, each engine scored on the
full FCR31 word: before the patch the -0 and -MIN_DENORM rows fail on both
engines with 01000001 against 01020041 and the other eight pass; after, all ten
pass.
The six positive rows are controls, and because the fix is a deletion they were
checked live rather than assumed: with the sign test deleted as well, all six
fail on both engines and the four negative rows still pass. Without them, a
deletion that went one step too far would raise I on every SQRT.S and nothing
in the suite would notice.
EeRecFpu.SqrtSOfNegativeZeroIsPositiveZero had asserted the opposite -- "the
zero path is not the negative path: no I|SI" -- on nothing but the two engines
agreeing with each other. ps2autotests' sqrt.expected prints results only,
never FCR31, so it never supported that claim. Its value assertion stands; the
flag rule moves to the new test, and it keeps the one flag statement that is
still true, that SQRT.S never raises D.
recompiler_tests 1582 pass / 0 fail / 46 disabled, and the other four ctest
binaries rebuilt against the changed libpcsx2 and rerun: core_test 86,
common_test 31, gs_vertex_tests 21, mvu_progcache_versioning_tests 13.
Noticed in the same sweep and NOT addressed here, recorded as leads in the
capture's handoff: the arm64 JIT reports 01000001 on every RSQRT.S row in the
capture including ordinary-negative operands, where interp, x86-jit and
hardware all say 01020041 -- recRSQRT_S_xmm does contain the I|SI set, so that
looks like a lost flag write rather than a missing one. And hardware raises
I|SI, not D|SD, on RSQRT's 0/0 rows.
Idea by pstef.
|
||
|
|
b3dfef14e3 |
Fix: interp SQRT.S of -0.0 returns +0.0, not -0.0
IEEE-754 says sqrt(-0) is -0, and the interpreter said so too:
_FdValUl_ = _FtValUl_ & 0x80000000;
The EE does not. ps2autotests tests/cpu/ee_fpu/sqrt.expected, captured on
hardware:
sqrt 80000000/-0.00: 00000000/+0.00
sqrt CF_NEGZERO: 00000000/+0.00
Both recompilers already agreed with the console by construction --
recSQRT_S_xmm takes |Ft| before the Fsqrt, so the sign is gone before the
zero case is reached -- which makes this an interp-vs-JIT divergence with
the interpreter on the deficient side.
Found by a randomized SQRT.S differential over signed zeros, +/-fMax and
full-range normals. It went unnoticed for as long as it did because every
hand-written SQRT.S case in the suite uses +/-4.0; the operand pool that
found it is going in with the next commit.
Bidirectional per the repo's evidence rule: the new test fails on the
unpatched tree with
fpr[2]: JIT=0x0 INTERP=0x80000000
and passes with the patch. Full suite 1518 pass / 0 fail / 22 disabled.
Idea by pstef.
|
||
|
|
d80101329e |
Fix: SQRT.S clamps its operand in the arm64 fast path
recSQRT_S_xmm was the one emitter in iFPU-arm64.cpp that never clamped its
source. fpuClampInput has twelve call sites covering ADD/SUB/MUL/DIV/RSQRT and
the six accumulator forms; SQRT called it zero times. An exponent-255 Ft is an
ordinary large PS2 float, but it reaches the host as Inf, so Fsqrt returned Inf
and fpuClampResult flattened it to 0x7F7FFFFF -- two binades from the
interpreter's sqrt(fpuDouble(Ft)).
Found by the hardware capture landed in 47d910efa6, rows 44/45:
sqrt +EEMAX : console 5fb504f3 interp 5f7fffff jit 7f7fffff
sqrt 2^128 : console 5f800000 interp 5f7fffff jit 7f7fffff
Unlike the six operand-clamp rows beside them, these did not close under
CHECK_FPU_EXTRA_OVERFLOW -- there was no gate to turn on. That is what made it a
defect rather than the clamp-mode axis.
The gate is CHECK_FPU_OVERFLOW (eeClampMode >= 1, ON by default), not the
arithmetic family's CHECK_FPU_EXTRA_OVERFLOW. x86 recSQRT_S_xmm clamps at that
same lower threshold (iFPU.cpp:1777), and SQRT is alone in it: x86 gates RSQRT's
operand clamp on CHECK_FPU_EXTRA_OVERFLOW (recRSQRThelper1/2, iFPU.cpp:1835/1853),
which recRSQRT_S_xmm already matched, and every other x86 clamp reaches the FPU
through fpuFloat/fpuFloat2 under the same higher gate. Matching x86 rather than
DIV.S is what aligns all three engines in the mode games actually run in.
Direction per the standing rule: the interpreter was the side nearer the console,
so the recompiler moved. At eeClampMode 0 nothing is emitted, exactly as before.
One-sided, since Fabs has already made the operand non-negative -- the same
positive-only shape as x86's xMIN.SS. It is NOT Fminnm, which is what the first
cut of this used, and that was wrong: FPMinNum only prefers the number when the
other operand is a QUIET NaN, so a signalling operand goes down FPProcessNaNs
and comes back merely quieted, surviving the clamp. x86's MINSS returns src2 for
ANY NaN, and half of the EE's exponent-255 mantissa space is signalling, so
Fminnm covered only half the class the comment claims ("any Ft whose exponent
field is 255"). Measured exhaustively on this host over all 2^31 non-negative
operands, against a model of MINSS(x, +FLT_MAX):
UMIN mismatches vs MINSS: 0
FMINNM mismatches vs MINSS: 4194303 (first at 7f800001)
End to end, sqrt(0x7F800001) came back 0x7F7FFFFF where the interpreter -- whose
fpuDouble switches on the exponent FIELD alone, mantissa irrelevant -- gives
0x5F7FFFFF. Same for 0xFF800001 and 0x7FBFFFFF. The capture's three SQRT rows
are a qNaN, an Inf and a finite number, so nothing in it could reach the
signalling half.
So the clamp is done in the integer domain instead: the operand is post-Fabs, so
bit 31 is clear, and over non-negative floats the IEEE ordering IS the unsigned
integer ordering. Umin against 0x7F7FFFFF clamps Inf, sNaN and qNaN alike and
passes every representable finite value -- exact MINSS agreement on every input,
at one instruction. Umin has no scalar form so it is a 2S vector op; only lane 0
carries the operand and the scalar Fsqrt that follows zeroes the rest.
Kept as a SQRT-local helper rather than folded into fpuClampResultPositive,
whose other caller recABS_S_xmm emits its clamp with no CHECK_FPU_* gate at all,
where the interpreter and the console both leave exponent-255 operands alone.
That is a separate pre-existing defect whose fix is to delete the clamp, not to
change which wrong answer it produces; editing the shared helper would have
moved ABS.S's output for an unfixed case. Verified unchanged: ABS.S/NEG.S still
diverge on the same 30 of 48 rows (EeFpuAbsNegClamp.DISABLED_DumpAllLegs).
FULL mode was checked and does not share the gap: DOUBLE::recSQRT_S_xmm widens
through ToDouble, which carries exponent 255 across exactly, and
EeRecFpuFull.SqrtPseudoInfExact already pins the true sqrt(2^128) = 0x5f800000.
Its inline note about the fast path was describing behavior the fast path did
not yet have; it does now, so the note is updated with the measured value.
Verified bidirectionally. On the unpatched emitter with these tests present,
SqrtClampsItsOperandLikeTheRestOfTheFamily fails on rows 44 and 45 in both clamp
modes (interp 5f7fffff vs jit 7f7fffff) and passes on row 46, and
EnginesAgreeExceptOnTheDocumentedRows fails because the two rows no longer
belong on the allowance list. SqrtClampCoversSignallingOperandsToo sweeps every
exponent-255 shape in both signs and fails on exactly the three signalling rows
under Fminnm. With the patch all enabled tests in the file pass, and the console
tally is unchanged at 20 match / 19 value-only / 3 flag-only / 15 both -- the JIT
moved onto the interpreter's answer without changing what the file says about
the hardware.
The tripwire is promoted to an enabled regression test that asserts the value as
well as the agreement -- agreement alone could be reached by degrading the
interpreter, which is the side nearer the console here.
Idea by pstef.
|
||
|
|
2951dd5fb0 |
Tests: run the EE harness in the FP environment a game runs in
The recompiler suite ran at FPCR 0 -- round-to-nearest, denormals live
-- while a real game runs 0x1c00000, FZ plus ChopZero, from
EmuConfig.Cpu.FPUFPCR. So the suite was answering questions about an FP
environment no player has.
Production's model is already consistent and every engine implements its
half: ambient is FPUFPCR, the EE FPU DIV/SQRT emitters swap to
FPUDivFPCR and back, the microVU dispatcher loads VU0FPCR/VU1FPCR and
restores FPUFPCR, and the VU micro interpreters scope-guard to the same.
Only the harness never established the baseline the rest of that model
assumes -- EeRecTestHarness's own comment said as much, and chose to
contain each JIT block's FPCR mutation instead. VuTestHarness had
already worked around the consequence by pinning both of its passes to
the VU FPCR; ScopedEeFpcr is the EE-side equivalent, and it establishes
rather than contains.
mVU's skip-the-FPCR-load-when-equal gate is the sharpest illustration:
it compares FPUFPCR against VU0FPCR and skips when they match, which is
only sound if ambient really is FPUFPCR. It was not, so the VU micro JIT
ran at the host default while the VU micro interp applied VU0FPCR.
15 tests then failed, none of them from an engine disagreeing with the
other in the environment they were written for. One root cause covers
most: round-toward-zero saturates an overflow to +/-FLT_MAX, so nothing
is ever Inf, and every path that infers overflow from Inf is inert --
the VU O flag, the EE FPU "unclamped intermediate product" cases, and
FCR31's overflow bit alike. FZ accounts for the rest by erasing the
mantissa the VU U bit is defined over.
Rather than disable them, the environment becomes an explicit per-test
axis: ScopedFpEnv, which rewrites EmuConfig's four FPCRs for its scope
so the whole stack agrees -- poking only the host register would leave
the baked FPUFPCR immediate and mVU's sentinel disagreeing with it. Two
kinds, both states a user can actually configure: IeeeNearest for the VU
tests, which need denormals to exist, and FlushNearest -- bit-for-bit
the default FPUDivFPCR -- for the EE FPU tests, which need Inf but are
built around FZ and diverge between engines without it.
No coverage is lost. One test is repaired instead of tagged:
EmptyDestMaskRetiresTheMacFlag now raises S off a plain -1.0, because
its subject holds in every FP environment and an underflow witness tied
it to one. One is added: ProductionFpEnvironmentErasesUnderflowAndOverflow
pins what a game gets -- the engines agreeing, on a value the console
contradicts -- so nobody re-derives the U/O work from a green suite and
concludes it is reachable in play.
Two findings the old environment was hiding get DISABLED tripwires,
both confirmed to fail when force-enabled:
- RSQRT_S is half-fixed. It rounds the sqrt to single but still
divides in double, and double-rounding a quotient is benign often
enough to vanish at nearest. Truncation is not so forgiving: at
ChopZero the interpreter lands one ULP above the JIT again, the
identical 0x3F5105EC/0x3F5105EB pair the original defect produced.
- FCR31 misses overflow on BOTH engines in production, reading
0x1000001 where the console says 0x1008011.
The second leaves a real question for hardware rather than for us: the
EE FPU truncates, so does silicon raise O from the magnitude of the
exact result, independently of rounding? A capture of FCR31 after an
overflowing ADD.S would settle it -- and the same answer decides the VU
O flag.
recompiler_tests 1517 pass / 0 fail / 22 disabled; core 86, common 31,
mvu_progcache 13, gs_vertex 21.
Idea by pstef.
|
||
|
|
0d7e6df7fe |
Tests: FCR31 O and SO against hardware, as tripwires
pcsx2/FPU.cpp runs every EE FPU arithmetic op through checkOverflow(result, FPUflagO|FPUflagSO): an infinite result saturates to +/-fMax, raises O and the sticky SO, and returns early (so U keeps whatever it had); a finite one clears O and then clears U. ABS/NEG/MAX/MIN clearFPUFlags(O|U). DIV/SQRT/RSQRT pass 0 and must leave both alone. The recompiler's fast path models none of it, so an overflowing MUL.S leaves FCR31 reading a bare 0x01000001 where the interpreter -- and the console capture in ps2autotests tests/cpu/ee_fpu/fcr.expected -- say 0x01008011. Measured as a 27-row sweep over the whole class rather than the two rows the capture happens to cover: 24 of 27 diverge between the engines, and the interpreter matches the checkOverflow model on all 27. The three that already agree are the DIV/SQRT/RSQRT negative controls, and they are live ones -- the other 24 rows in the same table prove the probe can see an FCR31 change at all. The harness gains DisableFpuOverflow/EnableFpuExtraOverflow so the clamp-mode axis can be measured instead of assumed. An emitter that closed all 24 was written and measured, then reverted, and five tests go in DISABLED to record what it did not settle: DISABLED_EnginesAgreeExceptOnTheOverflowFlags DISABLED_EnginesAgreeOnOverflowFlagsAcrossTheArithmeticFamily DISABLED_OverflowFlagsComposeAcrossOneBlock DISABLED_ExceptionFlagsMatchConsole DISABLED_NanMathOverflowIsAnOperandClampModeDifference fpuEmitOverflowFlags detected overflow as `fabs(result) > FLT_MAX` -- that is, by sniffing for a HOST infinity, which makes an architectural flag a function of eeRoundMode. Measured on this host: under the shipping ChopZero default it never raises at all, and under eeRoundMode 1 or 2 it raises for ONE SIGN ONLY, because directed rounding produces 0x7f7fffff on one side and 0xff800000 on the other. It also fired on operations that are not overflows at all -- mul 2^128 by 1.0 or 0.5, add 2^128 + 0 -- contradicting the console on rows the JIT had got right. And it cost +8 host instructions per arithmetic op (MUL.S 3 -> 11) for a flag the x86 recompiler does not maintain at all -- every O/U write in pcsx2/x86/iFPU.cpp is commented out. The redesign should port iFPUd-arm64.cpp's ToPS2FPU_Full magnitude thresholds, which are round-mode and FZ independent, rather than test for a host Inf. Direction is unchanged: one correct engine against two, so the recompiler is the side that moves -- making the interpreter stop raising O/SO would align all three cheaply by destroying the only correct reference in the tree. Two things deliberately left, both recorded rather than papered over: - x86. pcsx2/x86/iFPU.cpp is in a separate CMake source list and is not built on this host, so the mirror could not even be compiled, let alone diffed against the interpreter. Its commented-out xAND lines are not the fix on their own either: they sit before the op and clear O|U unconditionally, which is only half of checkOverflow. - The underflow half. checkUnderflow can only SET U from a denormal result, and every FP environment PCSX2 runs the EE under has FZ set, so the host flushes one to signed zero before either engine looks. With FZ off the engines also disagree on the VALUE, which is the denormal work item; DISABLED_UnderflowFlagsNeedFzOff pins it. The fifth tripwire is a different question wearing the same clothes. "NAN math" feeds ADD.S two raw exp-255 words, so the engines compute different things before any flag logic runs -- interp clamps operands through fpuDouble and gets Inf, the fast path gets a host NaN. Turn on CHECK_FPU_EXTRA_OVERFLOW and the row aligns exactly, which is what DISABLED_NanMathOverflowIsAnOperandClampModeDifference measures: it attributes the row to the operand-clamp mode axis, a deliberate x86-JIT-parity compromise, instead of leaving it as an unexplained entry in a known-divergence list. It asserts FCR31 as part of that alignment, so it rides on the O/SO revert and is disabled with the rest; the row itself stays in kFcrEngineDivergences either way. Idea by pstef. |
||
|
|
bf4e1089a0 |
Tests: EE FPU overflow against hardware — the max is 0x7FFFFFFF, not FLT_MAX
ps2autotests' fpu/fcr.cpp has run MUL.S(0x7F7FFFFF, 0x7F7FFFFF) on hardware all along, but prints the result with %f, so the only thing it ever recorded was the string "NaN". This captures the bits: 57 EE FPU rows and 8 VU0 macro-mode rows from a real PS2 over ps2link, every value a raw word. One rule accounts for every row: The EE FPU's representable maximum is 0x7FFFFFFF == (2 - 2^-23) * 2^128. Exponent 255 is an ordinary exponent -- there is no Inf and no NaN. Overflow means exceeding THAT, it saturates there, and only then are O and SO raised. So +FLT_MAX + +FLT_MAX is not an overflow on this machine: the exact sum is representable and the console returns it with FCR31 untouched. 2^127 * 2 is likewise fine; 2^127 * 4 is not. The generator asserts both halves of that in exact rational arithmetic across all 47 arithmetic rows, plus an underflow law (denormal operands flush to signed zero first, U follows from the flushed result), and rejects a capture that fails either rather than reshaping it. Both laws were confirmed live by corrupting the input. div 1.0/+0 is carried as a known-answer control, and the run is byte-identical across two resets. That max is one binade above what IEEE single can hold, which is why the fast path cannot match the console here however the flag test is written -- the host cannot represent the EE's top octave, so a result the console returns exactly necessarily arrives as a host overflow. The FULL double path can, and does. Also settles the VU half named in the same work item: VU0 saturates to 0x7FFFFFFF too and raises MAC O, and a row that overflows x, y and z while leaving w in range confirms the x=8 y=4 z=2 w=1 nibble layout. Nothing is "fixed" here. All three console divergences are shared by both engines and deliberate -- 19 rows are the +/-FLT_MAX saturation compromise, 3 are underflow U|SU needing FZ off, 15 are the overflow pair -- so they are recorded and left to the hardware-alignment stage. What is not deliberate, and is what the capture surfaced: SQRT.S is the only op in iFPU-arm64.cpp whose emitter never clamps its operand. fpuClampInput has twelve call sites covering ADD/SUB/MUL/DIV/RSQRT and the six accumulator forms; recSQRT_S_xmm calls it zero times, so an exponent-255 Ft reaches Fsqrt as a host +Inf and comes back 0x7F7FFFFF where the interpreter lands two binades away. Unlike the six operand-clamp rows beside it, this does not close under CHECK_FPU_EXTRA_OVERFLOW, because there is no gate to turn on. The interpreter is nearer the console on both rows, so the direction is to give SQRT the clamp the rest of the family has. Recorded as a divergence with a DISABLED tripwire, not fixed in this commit. The engine-agreement test asserts the listed rows still diverge as well as that the unlisted ones agree, so the allowance list cannot go stale silently. kEngineDivergences does not list rows 3, 11 and 16 (mul 2^128 by 2.0, by 1.0, add 2^128 + 0) even though they sit in the middle of the operand-clamp block they look like they belong to. They are not divergences: both engines return the same result word on all three, and the only thing that ever differed there was FCR31, which is the O/SO question deferred to the redesign -- see the DISABLED tripwires in ee_fpu_fcr_console_conformance_tests.cpp. The file says so in place, so the omission cannot be read as an oversight. Idea by pstef. |
||
|
|
3dba206e8a |
Fix: FULL-mode RSQRT returned -0.0 for the largest magnitude
ToPS2FPU_Full has an arm for values the EE's top binade can hold but a
host single cannot: halve the double, narrow, add 0x00800000 back to the
single. Its guard was |x| >= 2^129, inherited from x86 iFPUd.cpp's
dbl_ps2_overflow. But the largest number this FPU has is 0x7FFFFFFF ==
(2 - 2^-23) * 2^128, a whole binade below 2^129, so everything in the band
(kEeFpuMax, 2^129) was routed into the halving arm when it should have
saturated.
Halved, such a value sits just under 2^128. Under the divide unit's
round-to-NEAREST FPCR the narrow rounds it up to a host infinity and the
+0x00800000 carries out of the exponent field into the sign bit:
0x7f800000 + 0x00800000 == 0x80000000
so the largest magnitude the FPU can produce came back as negative zero --
sign flipped and exponent field 0 rather than 255, which the NFS Carbon
corner (DivZeroOverZeroKeepsPseudoInfExponent) already established is
game-visible through guest softfloat classifiers.
Under the arithmetic FPCR the narrow chops to 0x7f7fffff and the arm is
correct, which is why only the ops that swap to FPUDivFPCR could reach it.
The interpreter's eeRoundToSingle is immune by construction -- it scales by
2^-4, and its comment says why: "the +4 lands on 255 exactly -- it can
never carry into the sign."
ONLY RSQRT REACHES THE BAND, which is why this survived. A DIV quotient
cannot: for 24-bit significands with a < b, a/b <= 1 - 2^-24 strictly, and
the band's relative width is exactly 2^-24. A sweep of the four reachable
exponent differences found 0 hits, and the first probe written for this --
DIV.S(0x7FFFFFFF, 0x3F7FFFFF) -- lands on 2^129 *exactly* and came back
correct, which is what sent me looking for the algebra. SQRT halves
exponents and cannot get near. RSQRT divides by a 53-bit sqrt result, so
the significand argument does not apply; a coarse sweep found 2.5M hits.
The fix is the bound, not the arm: compare against kEeFpuMax's double bit
pattern, with `hi` rather than `hs` because kEeFpuMax itself is
representable and the halving arm handles it exactly (halved it is
+FLT_MAX, and 0x7f7fffff + 0x00800000 == 0x7fffffff). Costs 2 extra
instructions to materialise the constant, on the cold toComplex arm; the
in-range path is untouched.
Verified bidirectionally: the new test's 5 pairs fail on the unpatched
source (jit 80000000, interp 7fffffff on all five) and pass after. The
liveness companion, DivKeepsTopBinadeResultsBelowTheEeMaximum, is green
both ways -- it holds 1.5*2^128 in the halving arm, so over-tightening the
guard down to 2^128 turns it red rather than letting the first test go
green for the wrong reason.
All six ctest binaries green on exit code: recompiler_tests 1640,
core_test 86, mvu_progcache_versioning_tests 13, gs_vertex_tests 21,
common_test 31, demangler_test. The 53 console-conformance and FULL-mode
tests (EeFpuOverflowConsole, EeFpuZeroDivisorConsole, EeRecFpuFull) pass
unchanged, so no capture row moved.
x86 iFPUd.cpp carries the identical constant (s_const DOUBLE(0, 1152, 0)
at :115, consumed at :185) and so has the same defect. Not touched here:
this is an aarch64 host, that column is never executed, and an unverifiable
port is not a fix.
Idea by pstef.
|
||
|
|
9eb45cf590 |
Fix: the recompiler-test GIF Path 1 sink missed a wrapped XGKICK's head
`mVU_XGKICK_` splits a packet that runs past the top of VU1 memory in two: the pre-wrap head goes to `Gif_Path::CopyGSPacketData`, and only the post-wrap tail goes to `Gif_Unit::TransferGSPacketData`. The PCSX2_RECOMPILER_TESTS sink hooked only the second one, so a wrapped kick was captured as its tail alone — no GIFtag at all. Measured while landing the console XGKICK cases: 64 captured bytes from the recompiler against 112 from the interpreter, which loops through TransferGSPacketData and never calls CopyGSPacketData. That reads exactly like a serious microVU miscompile and is entirely our instrumentation; both engines emit the same correct stream. CopyGSPacketData now feeds the same sink and skips the ring, since nothing drains it while the sink is installed. The gif_test_hooks declaration moves above Gif_Path so the member function can see it. Consequence, which is larger than the bug: before this, no test could observe a wrapped XGKICK's GIFtag. Every XGKICK test used packets that fit inside VU1 memory, so the blind spot never showed. Test-build only — the whole block is inside #ifdef PCSX2_RECOMPILER_TESTS. vu1_xgkick_drain_tests.cpp moves with it. That file is ours and postdates the branch this came from, and its wrap case was written against the blind spot: it asserted the JIT capture was the 32-byte tail, with a header comment explaining that the head was unreachable. Both halves now arrive, so it asserts the whole 48 bytes — head carrying the GIFtag, tail resuming at offset 0 — which is the stronger property and the one that was never testable before. The XgKickHack wrap case already asserted that shape and only loses a stale "unlike the non-hack path" aside. Idea by pstef. |
||
|
|
63e7904e55 |
Fix: VU replay inherited the interpreter's stale sticky STATUS flags
vu_capture::CapturedState carries microVU's four-deep micro_statusflags[]
shadows but not the interpreter's live scalar accumulators
VU->statusflag/macflag/clipflag, and RestoreState left them alone. So the
interpreter pass of a replay started from whatever the previously executed
VU program had left in VURegs.
Most of that is harmless because it gets recomputed, but one field is not:
_vuFMACAdd snapshots VU->statusflag into fmac[i].statusflag
_vuFMACflush ORs (fmac[i].statusflag & 0xFC0) into VI[REG_STATUS_FLAG]
0xFC0 is the STICKY field (ZS/SS/US/OS/IS/DS). Every op recomputes the 0xF
cause nibble, but nothing clears the sticky bits except FSSET -- so a stale
one rides straight through into the architectural result. The JIT derives its
status entirely from the restored micro_statusflags[] and never grows the
phantom bit, and the replay reports a divergence that belongs to neither
engine.
Seeded from VI[] rather than zeroed, because that is the exact inverse of the
flush above (VI[REG_MAC_FLAG] = fmac[i].macflag; STATUS takes the sticky field
plus the cause nibble). A capture taken with sticky flags already raised now
replays with them instead of silently losing them. No format bump -- VI[] is
already carried in full.
Found as an order-dependent failure of VuReplay.ReplayVu0Vadd... under
--gtest_shuffle. Diagnosed by execution, not by reading: with the triggering
predecessor in place VU0.statusflag was measured at 0x82 on entry to
ReplayCapture, and injecting that value directly reproduced the exact diff
`vi16: JIT=0x0 INTERP=0x80`. Injecting 0x02 (cause-only) did not -- only the
sticky half survives, which is the field the interpreter never recomputes.
Tests: the new ReplayDoesNotInheritStaleInterpreterStickyFlags seeds the
accumulator directly, so the fault is pinned without depending on a shuffle
seed; verified live (fails with the identical vi16 diff on the unpatched
RestoreState). ReplayVu0Vadd... also gained the diff_lines printout its VU1
twin already had -- a bare EXPECT_FALSE on `diverged` names no register, which
is most of why this took as long to triage as it did.
1538 pass, 0 fail. VuReplay no longer fails under any shuffle seed tried
(17, 4242, 99, 31337, 8, 2); EeRecCarbonSelfLoop.PinnedValueLoopCarriedBaseByteFill
still does and is unrelated.
Idea by pstef.
|
||
|
|
5bd3e7deb8 |
Fix: arm64 EATAN paired four coefficients with the wrong power of x
mVU_Globals (microVU_Misc.h) stores the EFU atan series in ascending power order but names the entries T1, T5, T2, T3, T4, T6, T7, T8 -- that is, T5 is the x^3 coefficient and T2 the x^5 one. mVU_EATAN_arm called the helpers in NAME order, so c5 landed on x^3, c7 on x^5, c9 on x^7 and c3 on x^9. Only the first term and the last three were on the right power. Against unknownbrackets/ps2autotests tests/vu/lower/efu.expected that is worth up to 919642 ULP, worst on the largest reduced arguments since the misplacement is in the low-order terms: case before after console EATAN CVF_3PI_OVER2 3fbc5441 3fae4be7 3fae4be7 919642 -> 0 ULP EATAN CVF_PI 3fa99875 3fa19dc5 3fa19dc4 522929 -> 1 EATAN CVF_INCREASING 3fa72d09 3f9fe0ba 3f9fe0ba 478287 -> 0 EATANxz CVF_DECREASING 3ee3f44a 3eed633d 3eed6339 -618223 -> 4 EATAN CVF_MAX_MANTISSA 3f9012c8 3f8db70b 3f8db70b 154557 -> 0 EATAN CVF_PI_OVER2 3f813895 3f807f4c 3f807f4c 47433 -> 0 Every EATAN-family row moved toward silicon or stood still; none moved away. Clears bad_jit on eight cases (kEfuBadJit 126 -> 118) and drops EATAN CVF_PI_OVER2 and EATAN CVF_PI from kEatanEngineDivergences, where the JIT now agrees with the interpreter exactly. On the rows that still differ the JIT is the side nearer the capture: it is exact where the interpreter is 1-2 ULP out, because it evaluates in single precision throughout while _vuCalculateEATAN goes through double-precision pow(). Free: the eight constants live within one struct at offsets 96..224, all encoding as the same single scaled-immediate LDR, so the emitted sequence is a permutation of identical instruction pairs -- same count, same bytes. Upstream x86 mVU_EATAN_ has the identical defect and is left alone; it is never executed on this host and is not updated upstream. DISABLED_DumpEatanFamily is the measurement that produced the table, kept so the reading can be re-made from data rather than from emitter source. It was cross-checked against a standalone single-precision evaluation of both orderings outside the emulator, which reproduces the same magnitudes to within 3 ULP. Idea by pstef. |
||
|
|
fc5ca4aca4 |
Fix: interp EATAN's x^7 coefficient was T3 with a digit dropped
_vuCalculateEATAN's eatanconst[] is a hand-transcription of the atan
coefficients microVU keeps in mVU_Globals (microVU_Misc.h). Eight of the
nine round-trip to those globals bit for bit, and so do all five of the
sinconsts[] added alongside them. One did not:
T3 printed: -0.139085337519646
in tree: -0.13085337519646 <- the 9 after "0.13" is gone
Origin:
|
||
|
|
1d16981a29 |
Fix: interp EATAN family was missing the range-reduction argument transform
Deficient: interp. Correct: arm64 JIT + x86 JIT (both emit the same shape).
_vuCalculateEATAN evaluates the EFU's atan series and then adds
eatanconst[8] = 0.785398185 = pi/4. That constant is only correct as the
second half of
atan(x) = pi/4 + atan((x - 1) / (x + 1))
so the polynomial has to be fed the REDUCED argument. Both recompilers do
that -- arm64 mVU_EATAN computes (Fs-1)/(Fs+1) before mVU_EATAN_arm, x86
mVU_EATAN the identical SUBSS/ADDSS/DIVSS -- and the interpreter passed the
raw argument, adding a pi/4 offset nothing had earned. The xy/xz forms want
the same identity for atan(y/x), i.e. (y-x)/(y+x), which is again what both
recompilers emit.
Re-derived arithmetically rather than read off the source: for Fs = 1.0 the
unreduced expression evaluates to 0x3FCA1D99, bit-for-bit what the
interpreter returned, and the reduced one to 0x3F490FDB, bit-for-bit what
both recompilers return (console 0x3F490FDA). A second witness at Fs = 3.0
agrees: unreduced gives 0xC6F69D96, i.e. -3e4 for an arctangent.
Dropping the `if (x != 0)` guard in the xy/xz forms is part of the same fix:
with the reduced argument the divisor is (y+x), not x, and the guard was
returning +0 where both recompilers and the console return a NaN pattern.
HOW THIS WAS INVISIBLE, which is the part worth keeping. Every test in
vu1_efu_console_conformance_tests.cpp scores each engine against silicon
SEPARATELY and records what it cannot reproduce per engine. That is
deliberate and the file says why: a pure JIT-vs-interp differential is blind
to anything both engines get wrong together. But it left the mirror-image
blind spot -- nothing asserted the two engines agree with EACH OTHER, and
CaseMatches() reduces each run to a bool and discards the value, so two
engines returning DIFFERENT wrong answers are flagged twice as "known bad"
and look settled. Measured: 111 of the 208 EFU cases had the engines
disagreeing. This fix brings 15 into agreement, leaving 96.
New tests, since that assertion class did not exist:
- EatanFamilyEnginesAgreeExceptWhereListed asserts engine agreement across
all 48 EATAN-family cases, with the 33 still-diverging labels listed
explicitly so movement in either direction fails loudly.
- EatanAppliesTheRangeReductionBeforeThePolynomial pins the defect as
arithmetic (EATAN(1.0) is the bare pi/4 constant), so it stays meaningful
even if both engines are later changed together.
The 33 remaining EATAN-family divergences are two further classes, both
documented in the test file and neither this commit's subject: the
recompilers hand raw exponent-255 patterns to the polynomial and get NaNs
where the interpreter clamps operands through vuDouble first, and the
interpreter evaluates the series in double-precision pow() against the
recompilers' single-precision Horner chain. On both, the interpreter is the
side nearer the console.
Bidirectional: with VUops.cpp reverted, both new tests fail (15 red
assertions naming jit=3f490fdb interp=3fca1d99). Suite 1530 pass / 0 fail /
22 disabled, plus core 86, common 31, gs_vertex 21, mvu_progcache 13.
Idea by pstef.
|
||
|
|
3d15e04c6c |
Fix: VU sticky bits accumulate where the flags are produced, not at flush
VU_STAT_UPDATE assigned the bare ZSUO cause nibble to statusflag, throwing the sticky field away, and _vuFMACflush re-derived the stickies from that cause nibble on every pipeline entry it retired. That is idempotent right up until an FSSET clears the sticky field: the stale cause then regenerates the bit FSSET just cleared. Traced on VUSTICKY_MICRO_FSSET_ASSIGNS_NOT_ORS. The FSSET merge itself is correct -- it produces VI=801 -- and the bit comes back one entry later: [FSSET] imm=800 sf 001 -> 801 [FLUSH] pipe[2] flagreg=10000 entry.sf=801 VI=041 -> VI=801 (FSSET arm) [ADD] pipe[3] lower VIwrite=8 snap.sf=801 [FLUSH] pipe[3] flagreg=8 entry.sf=801 VI=801 -> VI=841 (FMAC arm) pipe[3] is not an FMAC at all; flagreg=8 is a lower op writing integer register VI[3]. It took the non-FSSET arm only because every retired entry rewrites STATUS, and (sf & 0xF) << 6 turned the long-dead Z cause back into sticky Z. So the sticky OR moves to VU_STAT_UPDATE, where the flags are produced, and both flush sites take the sticky field from the snapshot instead of re-deriving it. statusflag is seeded from the whole STATUS register in vu0ExecMicro, so carrying the sticky field in it is the documented intent -- VU_STAT_UPDATE's original comment claimed exactly this preservation while the code did the opposite. Both halves are load-bearing: reverting VU_STAT_UPDATE alone fails the Vu0AluUpper suite, reverting the flush arms alone leaves the FSSET rows diverging. Graduates VUSTICKY_MICRO_FSSET_CLEARS and VUSTICKY_MICRO_FSSET_ASSIGNS_NOT_ORS from kMicroDivergences, which no longer holds any interp-only row. Idea by pstef. |
||
|
|
4a7284ea4a |
Fix: ESQRT/ERSQRT take the operand's magnitude
The EFU square root roots a negative operand as if it were positive; the interpreter's `p >= 0` guard returned the operand unchanged instead, so ESQRT and ERSQRT of -1.0 gave -1.0 where the console gives 1.0, and ESQRT of -0.0 gave -0.0 where the console gives +0.0. Both recompilers already AND the raw bits with absclip before FSQRT (mVU_ESQRT, mVU_ERSQRT), so masking the sign off before vuDouble puts the interpreter on the same order of operations rather than on a new one. Clears bad_interp on ERSQRT CVF_NEGONE, ESQRT CVF_NEGZERO and ESQRT CVF_NEGONE; kEfuBadInterp 119 -> 116. The remaining negative-operand rows (CVF_MIN, CVF_GARBAGE2, ...) still fail on BOTH engines and are untouched here: they are Inf/NaN inputs that vuDouble only clamps to fMax under CHECK_VU_OVERFLOW, which ships off. That is a separate, all-engines-wrong defect. Idea by pstef. |