fix(vu0): saturate VDIV/VRSQRT on a zero divisor and take the VSQRT/VRSQRT radicand magnitude - #198
Draft
smmathews wants to merge 2 commits into
Draft
fix(vu0): saturate VDIV/VRSQRT on a zero divisor and take the VSQRT/VRSQRT radicand magnitude#198smmathews wants to merge 2 commits into
smmathews wants to merge 2 commits into
Conversation
VRSQRT divides fs by the square root of ft, but the translator only read ft and emitted a bare reciprocal, silently substituting 1.0 for the numerator. Read fs and its fsf component selector, which the decoder already populates for this opcode.
…RSQRT radicand magnitude VDIV, VSQRT and VRSQRT wrote 0.0 for a zero divisor and for a negative radicand. The manual's exception table gives +MAX/-MAX for a zero divisor and the root of the magnitude for a negative radicand. That magnitude rule covers VRSQRT as well as VSQRT: the previous ft > 0.0f guard sent every negative radicand to 0.0, and VRSQRT now divides fs by the root of the radicand's magnitude. The VU's own float format holds one binade more than a host float does, so the largest finite float stands in for +MAX rather than equalling it. Put the three Q-producing results behind inline runtime helpers and have the translators emit calls to them, following the arithmetic translators in that file, which emit PS2_VADD, PS2_VSUB, PS2_VMUL and PS2_VBLEND rather than intrinsics. Not every translator there does this - the min/max forms emit _mm_min_ps and _mm_max_ps, the integer forms emit raw C operators, and the load/store forms emit the READ/WRITE macros - so this follows the arithmetic precedent rather than a universal one.
Owner
|
Before you put more time on this, I'm working a big refactor on vu1 and vf1 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix(vu0): saturate macro-mode VDIV/VRSQRT on a zero divisor and take the VSQRT/VRSQRT radicand magnitude
Problem
The three VU0 macro-mode FDIV-unit translators in
ps2xRecomp/src/lib/vu_translation_helpers.cppwrite0.0fwhere the hardware saturates or takes a magnitude.VDIVandVRSQRTemit0.0fon a zero divisor. The manual gives+MAX/-MAX.VSQRTemitssqrtf(std::max(0.0f, ft)), so a radicand of-16.0yields0.0instead of4.0.translateVU_VRSQRTnever reads itsfsoperand and emits a bare reciprocal, substituting1.0for the numerator. AnRSQRT Q, VFax, VFbywhose numerator is not1.0therefore computes a wrong quotient on all but the operand pairs where the substitution happens not to show: a+0numerator over a finite negative radicand, and a positive finite numerator over an infinite one, both give+0either way.Fix
ps2xRuntime/include/ps2_runtime_macros.hgains threestatic inlinehelpers behindPS2_VU_DIV_Q,PS2_VU_SQRT_QandPS2_VU_RSQRT_Q, as it already placesPs2ExtractEpi32behindPS2_EXTRACT_EPI32. One include is added,<limits>.Ps2VuDivQsaturates to the signed float maximum on a zero divisor, signed bysignbit(fs) != signbit(ft)— read from the bit, since-0.0f >= 0.0fistruein C++.Ps2VuSqrtQissqrt(fabs(ft)).Ps2VuRsqrtQcomposes the two, so its divisor is never negative and its saturation sign issignbit(fs)alone. The two operations therefore disagree on a negative-zero divisor.PS2_*call, followingtranslateVU_VADD/VSUB/VMULin the same file. Format arguments are now indexed.PS2_VDIV/PS2_VSQRT/PS2_VRSQRTare unchanged: no callers, and neither fits.PS2_VDIVis a packed four-lane_mm_div_ps;PS2_VRSQRTtakes one operand where the instruction takes two.VRSQRToperand read, then the saturating helpers. The first builds and passes on its own.Hardware basis
0/0and0 divisiongive+MAX/-MAX;√x (x < 0)gives√|x|; "0 is valid sign"; note, "D flag is not set for 0/0."+MAXis not IEEEinf.DIV/RSQRTon "0 division (except 0/0)", cleared bySQRTregardless of result; I raised bySQRT/RSQRTon a negative root.SQRT/RSQRTpages:Q = √|VF[ft]ftf|;RSQRT Q, VF10x, VF20ygivesQ = VF10x ÷ √|VF20y|.VRSQRTpage: "Divides the fsf field of VF[fs] by the square root of the ftf field of VF[ft]"; mnemonicVRSQRT Q, fsfsf, ftftf, two operands.VDIV/VSQRT/VRSQRTpages: "Same as the micro instruction". §5.3: macro-mode flags are the same as micro-mode.This change implements §2.3's calculation-result column for the rows cited above and nothing else in that table. EVIDENCE.md tabulates what is quoted here and what is derived.
Testing
ps2xTest/src/code_generator_tests.cppassert the entire emitted string per instruction, over operand tuples chosen to close constant and linear substitution of one field by another. They fail against the unmodified translators and are the regression gates for the codegen change.ps2xTest/src/ps2_vu_tests.cppcall the helpers directly. They pin the ordinary quotient at every sign combination, the zero-dividend quotient by magnitude and by sign bit, the saturation value and its sign, the magnitude root, and NaN and overflow behaviour on both paths.Risk and not in scope
0.0and now writes the float maximum, so a later multiply that stayed at zero can reach an infinity. Saturating the rest of the pipeline is the follow-up; Floating-point precision issues in VU0 (macrocode/microcode) and VU1 #165 is the nearest open issue.CCR[2,16](§5.1.3) andCFC2.Qalone would be undone by the next multiply.div.s/rsqrt.sinfpu_translator.cpphave analogous problems, in a separate file and a separate change.DIV's sign by>= 0.0fand saturatesRSQRTunsigned.PS2Runtime::executeVU0Microprogramruns VU0 microprograms on it and copies itsqintoctx->vu0_q, so one program can see+FmaxforDIV -0 / +0in aVCALLMSand-Fmaxfor the same macro-modeVDIV. This change narrows that gap rather than widening it. fix(vu1): maintain MAC/STATUS/CLIP flag registers, correct the flag-reading lower-op table, and saturate FTOI #189 and refactor: refactor VU1 #191 are open against that file and touch neither sign rule.sceVu0DivVector/sceVu0DivVectorXYZcarry the same finite-zero guard as each other — a library-routine question. An existing test pins the singular form; both are unchanged.VDIVsaturation construction first, and the sign rule is its author's. Closed unreviewed in a batch cleanup spanning elf_parser: clamp overlapping function boundaries from external sources #108 through codegen: force continuation on JAL/JALR resume-PC mismatch instead of aborting caller #117, it leftVSQRTand bothVRSQRTdefects unaddressed.Evidence — mutation table, saturation grids, and reproduction commands
Evidence
Build and run
-msse4.1on both C and C++ flagsps2_runtime.huses_mm_extract_epi32, which fails to inline without themps2x_testslinksps2_analyzer_liband fails to link without it./build/ps2xTest/ps2x_testsis the entry pointctestregisters no tests in this projectFailed: 0with exit code0. Reproduced before any change was made, and again after each mutation row below was restored.What is quoted and what is derived
0/0and0 divisiongive+MAX/-MAX√x (x < 0)gives√|x|SQRTpage states the operation directly asQ = √|VF[ft]ftf|0carries a valid sign, so-0/+0differs from+0/+00/0VDIV/VSQRT/VRSQRTpages, "Same as the micro instruction"; §5.3VRSQRTtakes two operandsVRSQRTpage text and itsVRSQRT Q, fsfsf, ftftfmnemonic; microRSQRTexampleQ = VF10x ÷ √|VF20y|+MAXis not IEEEinf+MAXisstd::numeric_limits<float>::max()E = 255an ordinary VU value at every mantissa,F = 0included, so the VU format reaches0x7FFFFFFF(about6.81e38) — a binade above the host maximum0x7F7FFFFF(about3.40e38). A hostfloatcannot hold1.F × 2^(+128), so the largest finite hostfloatis the nearest stand-in and saturates a factor of two low. §2.4's exclusion of infinity is what makes that top binade ordinary; it does not move+MAXdown.signbit(fs) != signbit(ft)+MAX/-MAXcase. This is the ordinary quotient-sign rule, consistent with §2.3.VRSQRT's saturation sign issignbit(fs)alone√|x|rule and §3.3.2's placement ofRSQRTamong root-taking instructions make the divisor√|ft|, which is never negative and is+0for a-0radicand. The radicand's sign is gone before the divide.VDIVandVRSQRTdisagree at a-0divisorVDIV 1 / -0saturates negative;VRSQRT 3 / -0saturates positiveThe test set
Emission layer —
ps2xTest/src/code_generator_tests.cppVDIV emits exactly the statements for a saturating divide of fs by ftVSQRT emits exactly the statements for the magnitude square root of ftVRSQRT emits exactly the statements that read fs and ft from their own register and selectorfsfsfftftftranslateInstructionreturns each translator'sfmt::formatresult verbatim for these three opcodes, so the emitted string is fixed by the register and selector pair. Each test asserts the whole string with==, which no appended statement and no alternate spelling survives.fsfandftfare 2-bit fields and tuples A and B already spend all four legal values, so C necessarily reuses two. C is chosen so that for every ordered pair of fields the three points are non-collinear.a·y + b = xacross two tuples is two equations in two unknowns, unsolvable only when the source field repeats while the target differs — which pairwise-distinct tuples rule out. Two tuples therefore always leave a linear survivor for every ordered pair of fields.VSQRTNumeric layer —
ps2xTest/src/ps2_vu_tests.cppCalls the three helpers directly on real operands and asserts the computed
float.VDIV divides ordinarily on any non-zero divisor, a zero dividend included, and saturates to the signed float maximum on a zero divisorVDIV saturation is chosen by sign bits, so signed zero operands are honouredVSQRT returns the root of the radicand magnitudeVRSQRT divides fs by the root of the radicand magnitude at every combination of the operands' sign bits, a zero dividend includedVRSQRT saturates on a zero radicand with the dividend's sign aloneVU0 FDIV-unit results are finite across the zero and float-maximum operand set, a non-finite operand propagates on the ordinary path, the zero-divisor branch saturates by sign bit alone, and neither end of the range is clampedvolatileround-tripMiniTest::Equalsis exact==, which cannot separate+0from-0, so every cell whose expected value is a zero carries a pairedstd::signbitassertion.{±Fmax, ±0, ±1}produces under the three operations is finite — the largest quotient it builds isFmax / 1— so it cannot construct an overflowing quotient and cannot support a general finiteness claim. Finiteness is the property the argument needs; the set is not closed set-theoretically, sinceSQRT(Fmax)andDIV(1, Fmax)both land outside it.floatis at most about1.84e19. Both signs of the infinite radicand are asserted.Fail before, pass after
Each row was green — the whole suite passing with the mutation applied — against the tree named, and fails against the delivered tree.
std::signbit(fs)rewritten to(fs < 0.0f)Ps2VuRsqrtQif (ft != 0.0f)rewritten toif (ft > 0.0f)Ps2VuDivQif (ft != 0.0f)rewritten toif (ft < 0.0f || ft > 0.0f)Ps2VuDivQif (ft < 0.0) ctx->vu0_q = 0.0;translateVU_VDIVif (ft < 0.0) ctx->vu0_q = 0.0;translateVU_VSQRT(std::signbit(fs) || std::signbit(ft))Ps2VuDivQfsfformat argument rewritten as3 - ftftranslateVU_VDIVftfformat argument rewritten asft_reg - 5translateVU_VSQRTif (ft != 0.0f && fs != 0.0f)Ps2VuDivQ(fs == 0.0f) ? fs : (fs / ft)Ps2VuDivQif (fs == 0.0f) return fs;before the saturation returnPs2VuDivQPs2VuDivQ(signbit(fs) && signbit(ft)) ? -div : divPs2VuRsqrtQPs2VuRsqrtQPs2VuRsqrtQPs2VuRsqrtQPs2VuRsqrtQPs2VuDivQPs2VuRsqrtQPs2VuDivQPs2VuSqrtQPs2VuDivQMutation table — measured
One production mutation per row, applied to the finished tree, rebuilt, and run. Predicted was written before the run; Actually observed is what it reported.
E1–E3andN1–N6are the emission and numeric tests named above, in that ordercp-made backup and confirmed byte-identical withcmpbefore the next was applied.cmpreported identity every time, and a full rebuild afterward reproduced the clean baseline pass.MiniTestnames each failing assertion, so the assertion sets quoted in the notes were read directly off the runs, and re-measured against the tree as delivered rather than against the tree each row was first written for.translateVU_VDIV:fsextraction reads theftregister andftfselectortranslateVU_VSQRT: readsinst.rdinstead offt_regtranslateVU_VSQRT: readsfsfinstead offtftranslateVU_VDIV: emitsctx->vu0_q = PS2_VU_DIV_Q(fs, ft) * 2.0f;translateVU_VRSQRT:fsextraction reads theftregister andftfselectortranslateVU_VSQRT: appendsif (ft < 0.0f) ctx->vu0_q = 0.0f;Ps2VuDivQ: guardif (ft != 0.0f)replaced withif (ft > 0.0f)Ps2VuDivQ: sign test(std::signbit(fs) != std::signbit(ft))replaced with(fs < 0.0f)DIV(NaN, -0)in N6 andDIV(NaN⁻, +0)in N2Ps2VuDivQ: guardif (ft != 0.0f)replaced withif (true)Ps2VuDivQ: both saturation returns replaced with-1.0f/1.0f±kFmaxfrom the saturation branch on non-finite dividends; the mutant returns±1.0fPs2VuSqrtQ:std::sqrt(std::fabs(ft))replaced withstd::sqrt(std::max(0.0f, ft))Ps2VuSqrtQ:std::fabsdropped,std::sqrt(ft)Ps2VuSqrtQ: root dropped,return std::fabs(ft);Ps2VuDivQ: sign XOR inverted (!=to==)Ps2VuDivQ: ordinary-path return reversed toft / fsPs2VuRsqrtQ: arguments transposed toPs2VuDivQ(Ps2VuSqrtQ(ft), fs)Ps2VuDivQ(1e-15, kFmax) ≈ 0, finitePs2VuRsqrtQ: root dropped,return Ps2VuDivQ(fs, ft);Ps2VuDivQ(fs, ft)without the root disagrees with the composition atRSQRT(NaN, -0)PS2_VU_RSQRT_Qalias transposed toPs2VuRsqrtQ((ft), (fs))1e-30 / sqrt(kFmax) ≈ 0, finitePS2_VU_DIV_Qalias transposed toPs2VuDivQ((ft), (fs))translateVU_VDIV: appendsif (ft < 0.0) ctx->vu0_q = 0.0;translateVU_VSQRT: appendsif (ft < 0.0) ctx->vu0_q = 0.0;Ps2VuDivQ: guardif (ft != 0.0f)replaced withif (ft < 0.0f || ft > 0.0f)translateVU_VDIV: thefsfformat argument replaced with the literal1fsf, so tuples B and C are what catch ittranslateVU_VRSQRT: theft_regformat argument replaced with the literal7ft_reg, so tuples B and C are what catch itPs2VuDivQ: ordinary path wrapped in a clamp,return std::clamp(fs / ft, -max, max);Ps2VuDivQ: sign test(std::signbit(fs) != std::signbit(ft))replaced with(std::signbit(fs) || std::signbit(ft))(1,1)row, N4/N5 are structurally unreachable becausefabspinssignbit(den)false, and N6 stays finite either waytranslateVU_VDIV:fsfformat argument replaced with(uint8_t)(3 - ftf)3−2=1,3−0=3), caught by tuple C (3−3=0 ≠ 2)translateVU_VSQRT:ftfformat argument replaced with(uint8_t)(ft_reg - 5)7−5=2,5−5=0), caught by tuple C (19−5=14 ≠ 3)PS2_VU_SQRT_Qmacro redefined to dispatch toPs2VuRsqrtQ(1.0f, (ft))Ps2VuRsqrtQ(1.0f, (ft))does not reproducePs2VuSqrtQ's magnitude-then-root behaviour at an infiniteft. M42 is the discriminating row for N3.Ps2VuDivQ: guardif (ft != 0.0f)replaced withif (ft != 0.0f && fs != 0.0f)Equals(0.0f)rows see±Fmax, and N4's zero-dividend rows route a zero dividend with a non-zero divisor into the divide, where the mutant saturatesPs2VuDivQ: ordinary return replaced withreturn (fs == 0.0f) ? fs : (fs / ft);signbitrows at the zero-dividend, negative-divisor cells, which the pairedEquals(0.0f)rows cannot see; the mutant returns the dividend unchanged, so its sign is the dividend's where the true quotient's is the exclusive-orPs2VuDivQ:if (fs == 0.0f) { return fs; }inserted before the saturationreturnEquals(±kFmax)rows and by all four of N5's zero-dividend saturation rows, two of them at a positively-signed zero. This is the row that justifies thesignbit-to-Equalsupgrades.Ps2VuDivQ: ordinary return replaced withreturn (std::signbit(fs) && std::signbit(ft)) ? -(fs / ft) : (fs / ft);(-8,-2) = 4.0f, where the mutant returns-4.0f, and by its(-0,-2)signbitrow, whose sign the negation flipsPs2VuRsqrtQ:return Ps2VuDivQ(fs, Ps2VuSqrtQ(ft) * 2.0f);Ps2VuRsqrtQ:return (Ps2VuSqrtQ(ft) == 0.0f) ? std::numeric_limits<float>::max() : Ps2VuDivQ(fs, Ps2VuSqrtQ(ft));— unsigned saturation, matching the discarded prior-art form-kFmaxagainst the mutant's+kFmax. N4 is unreachable because non-zero radicands never reach the saturation branch. N6's zero-radicand row for a negative infinite dividend sees it too. M37 is the discriminating row for N5.Ps2VuRsqrtQ:return (std::signbit(fs) && std::signbit(ft)) ? -Ps2VuDivQ(fs, Ps2VuSqrtQ(ft)) : Ps2VuDivQ(fs, Ps2VuSqrtQ(ft));(-8,-4) = -4and(-0,-4)sign rows, and by all four of N5's two-negatively-signed-operand rows(-3,-0),(-0,-0),(-inf,-0)and(NaN⁻,-0). It shows both halves of the outer helper's sign rule closed, the ordinary quotient and the saturation.Ps2VuRsqrtQ:return (fs == 0.0f) ? fs : Ps2VuDivQ(fs, Ps2VuSqrtQ(ft));Ps2VuRsqrtQ:return std::clamp(Ps2VuDivQ(fs, Ps2VuSqrtQ(ft)), -std::numeric_limits<float>::max(), std::numeric_limits<float>::max());RSQRT(±kFmax, 1e-30)and both infinite-dividend rowsRSQRT(±inf, 4), and nothing else. M25 closed the clamp family on the divide and left it open one level up.Ps2VuRsqrtQ:return std::isnan(fs) ? 0.0f : Ps2VuDivQ(fs, Ps2VuSqrtQ(ft));RSQRT(NaN⁻, ±0), which pin-kFmaxwhere the mutant returns a zero, plus N6's two NaN-propagation rows and both positively-signed NaN-dividend saturation rows. N6 fed NaN to the reciprocal root's radicand and never to its dividend at the discriminating sign.Ps2VuSqrtQ:return (ft == 0.0f && !std::signbit(ft)) ? -0.0f : std::sqrt(std::fabs(ft));-0root also reachesPs2VuDivQas a negatively-signed divisor, flipping theRSQRT(NaN, 0)andRSQRT(-inf, 0)saturation rows, so N6 sees it too.translateVU_VRSQRT: appendsif (ft < 0.0) ctx->vu0_q = 0.0;PS2_VU_SQRT_Qmacro redefined toPs2VuSqrtQ((ft) * 0.25f)Ps2VuDivQ: ordinary path clamped on the low side only,float q = fs / ft; return (q < -max) ? -max : q;DIV(-kFmax, 0.5)andRSQRT(-kFmax, 1e-30), and the negative-infinity rowsDIV(-inf, 2)andRSQRT(-inf, 4).Ps2VuRsqrtQ: result clamped on the low side only,float q = Ps2VuDivQ(fs, Ps2VuSqrtQ(ft)); return (q < -max) ? -max : q;RSQRT(-inf, 4)andRSQRT(-kFmax, 1e-30). The ternary spelling is required: written withstd::fmaxthis would be a different mutation, becausestd::fmaxreturns its non-NaN argument and so suppresses a NaN as well as clamping.Ps2VuDivQ:if (std::isnan(fs) || std::isnan(ft)) { return fs + ft; }inserted before the saturation returnDIV(NaN⁻, ±0), N5'sRSQRT(NaN⁻, ±0), and N6's four positively-signed-NaN saturation rowsDIV(NaN, ±0)andRSQRT(NaN, ±0).Ps2VuDivQ:if (std::isinf(fs)) { return fs; }inserted before the saturation returnDIV(+inf, -0)andDIV(-inf, -0), N5'sRSQRT(+inf, ±0)andRSQRT(-inf, -0), and N6'sDIV(±inf, 0)andRSQRT(-inf, 0)Ps2VuSqrtQ:float r = std::sqrt(std::fabs(ft)); return (!std::signbit(ft) && r > max) ? max : r;SQRT(+inf)andRSQRT(1, +inf)and nothing else. Spelled withstd::fminthis would also suppress a NaN and be caught for an unrelated reason.Ps2VuDivQ: an underflowed quotient saturated,float q = fs / ft; return (q == 0.0f && fs != 0.0f && std::isfinite(ft)) ? std::copysign(max, q) : q;DIV(±1e-30, 1e20)andRSQRT(-1e-30, kFmax). Thestd::isfinite(ft)conjunct keeps it off the infinite-divisor rows, which pin a different property.Ps2VuDivQ: an underflowed quotient's sign dropped,float q = fs / ft; return (q == 0.0f && fs != 0.0f && std::isfinite(ft)) ? 0.0f : q;DIV(-1e-30, 1e20)andRSQRT(-1e-30, kFmax);==cannot separate the two zeros, so thestd::signbithalf is the only thing that can see itPs2VuSqrtQ:float r = std::sqrt(std::fabs(ft)); return (std::signbit(ft) && r > max) ? max : r;SQRT(-inf)and the equality half ofRSQRT(1, -inf)and nothing else: clamping the root at a negative infinite radicand leaves a finite maximum, and the composition divides into it, giving a nonzero denormal where a zero is expected, so thesignbithalf still passes.SQRT(-inf)and that equality are jointly rather than individually necessary against this table;SQRT(-inf)is kept because it asserts the root's magnitude rule directly and sits in the identical position toSQRT(+inf)under M47.Ps2VuRsqrtQ:return (std::isinf(ft) && std::signbit(ft)) ? -0.0f : Ps2VuDivQ(fs, Ps2VuSqrtQ(ft));signbithalf of theRSQRT(1, -inf)pair; the equality half cannot see it, because==cannot separate+0from-0Ps2VuRsqrtQ:return (std::isinf(ft) && std::signbit(ft)) ? 1.0f : Ps2VuDivQ(fs, Ps2VuSqrtQ(ft));Ps2VuDivQ:if (std::isinf(fs)) { return std::copysign(std::numeric_limits<float>::max(), fs); }inserted before the saturation returnDIV(+inf, -0)andDIV(-inf, -0): the mutant saturates an infinite dividend by its own sign instead of by the exclusive-or, and the two forms agree at every positively-signed divisor.VRSQRTcannot see it, because the inner divisor isPs2VuSqrtQ(±0), whose sign bit is always clear.Ps2VuDivQ:if (std::isnan(fs)) { return std::copysign(std::numeric_limits<float>::max(), ft); }inserted before the saturation returnDIV(NaN⁻, ±0)in N2 andRSQRT(NaN⁻, ±0)in N5, and nothing in N6: the mutant saturates a NaN dividend by the divisor's sign instead of the dividend's, and the two forms agree at a positively-signed NaN. It shows the NaN family closed at both helpers and at both signs of the dividend.Ps2VuRsqrtQ:return (std::isnan(ft) && std::signbit(ft)) ? 0.0f : Ps2VuDivQ(fs, Ps2VuSqrtQ(ft));Ps2VuDivQ: ordinary return replaced withreturn (std::isnan(ft) && std::signbit(ft)) ? 0.0f : (fs / ft);std::fabsclears the radicand's sign bit before the root, so the inner divisor is never a negatively-signed NaN.Ps2VuSqrtQ:return (std::isnan(ft) && std::signbit(ft)) ? 0.0f : std::sqrt(std::fabs(ft));Ps2VuDivQ: ordinary return replaced withreturn (std::isnan(fs) && std::signbit(fs)) ? 0.0f : (fs / ft);Every row is caught, and every row except the two below matched its prediction exactly.
The two rows that came out wider than predicted
NaN > 0.0fisfalse, so a NaN divisor saturates instead of propagating, and so does every negative divisor. In N6 that fails the NaN divisor through the divide and the NaN radicand through the composition, at both signs, plus the negative infinite divisor — alongside N1's negative-divisor assertions.std::max(0.0f, NaN)returns0.0f, since0.0f < NaNisfalse, soPs2VuSqrtQ(NaN)becomes0.0finstead of propagating. N6's four NaN-radicand rows fail. The same substitution sends a negative infinite radicand to0.0f, soSQRT(-inf)and the equality half ofRSQRT(1, -inf)fail too — the two assertions M50 also reaches.Neither is a coverage gap. N6 reaches them because it asserts non-finite behaviour rather than only finiteness.
Operand-space enumeration
A cell is enumerated over the parameters of the helper being mutated, never credited to it from a sibling's test. "Structurally unreachable" means no input to that function's own parameters produces the cell. These spaces exclude the non-finite classes; the grids below cover those.
Ps2VuDivQ(fs, ft){+0, -0, positive, negative}Equals(0.0f)line plus astd::signbitline, two assertion lines to one cell; and the two(positive|negative) × +0saturation cells, whose±Fmaxexpectation pins magnitude and sign at once. N2 covers the other 6: the(positive|negative) × -0pair and the whole(+0|-0) × (+0|-0)quadrant.Ps2VuSqrtQ(ft)+0cell directly rather than relying on N5's saturation rows to pin it jointly.Ps2VuRsqrtQ(fs, ft)Ps2VuDivQ(fs, den)call,denisPs2VuSqrtQ(ft)and can only be+0or positive — a fact about that call site, not about this helper's operand space.signbit(ft)is erased between the outer body and the inner call, so it is live in the outer body and a mutant placed there reads it.The non-finite saturation grid
Reached only when the divisor is exactly zero. Product: non-finite class × dividend sign × divisor sign × which helper carries the branch.
Ps2VuDivQ+0+FmaxPs2VuDivQ-0-FmaxPs2VuDivQ+0-FmaxPs2VuDivQ-0+FmaxPs2VuDivQ+inf+0+FmaxPs2VuDivQ+inf-0-FmaxPs2VuDivQ-inf+0-FmaxPs2VuDivQ-inf-0+FmaxPs2VuRsqrtQ+0+FmaxPs2VuRsqrtQ-0+FmaxPs2VuRsqrtQ+0-FmaxPs2VuRsqrtQ-0-FmaxPs2VuRsqrtQ+inf+0+FmaxPs2VuRsqrtQ+inf-0+FmaxPs2VuRsqrtQ-inf+0-FmaxPs2VuRsqrtQ-inf-0-FmaxPs2VuSqrtQcontributes no cellsSQRT(NaN),SQRT(+inf)andSQRT(-inf)are asserted.std::numeric_limits<float>::quiet_NaN()has that bit clear, so the negatively-signed NaN is built withstd::copysign, and both itsisnanand itssignbitare asserted as premises where it is used.floats with nothing filtering either argument, so every row above is constructible with a literal.Ps2VuDivQ(fs, den)call,denisPs2VuSqrtQ(±0), which is+0at either sign of the radicand, so that site never occupies a-0-divisor cell. The divide's two-0-divisor infinity cells therefore cannot be reached throughVRSQRTat any radicand, which is why they are asserted on the divide directly; the reciprocal root's own-0-radicand cells are asserted at the outer body, wheresignbit(ft)is still live.Ps2VuDivQinfinity cells at a negatively-signed divisor and nothing else; M54 moves the negatively-signed NaN cells at both helpers and nothing else; M46 moves every infinity cell of both helpers; M45 does the same for the NaN class. Each is a rule a reader could plausibly write.The non-finite ordinary-path grid
The product is: which of five operand positions the NaN occupies × the NaN's sign bit × which path the operand pair selects.
Ps2VuDivQdividendPs2VuDivQdivisorPs2VuSqrtQradicandPs2VuRsqrtQradicandPs2VuSqrtQ(NaN)is a NaN, so the inner divide takes the ordinary path. This rests on NaN's comparison behaviour, a different argument from the-0unreachability above, which rests onfabs.Ps2VuRsqrtQdividend, read at the outer bodyThe macro layer
The three
PS2_VU_*_Qmacros are pass-throughs to identically-shaped functions, a transposition surface distinct from the functions themselves.PS2_VU_DIV_QPS2_VU_SQRT_QPS2_VU_RSQRT_QReciprocal-root equivalence
Ps2VuRsqrtQisPs2VuDivQ(fs, Ps2VuSqrtQ(ft)), and the two forms agree on every input.-0std::fabsclears the sign bit unconditionallysignbit(fs) != signbit(den)degenerates tosignbit(fs), becausesignbit(den)is always falseftbit pattern at each of thirteen dividendsThe sweep
Save the program below as
vu_mirror_sweep.cppin the clone root, then compile and run it from there with the commands that follow it. It is a one-off harness and is not carried in the tree, so it is reproduced here in full. It compiles against the header under test rather than a transcription of it. The third form is wrong on purpose: it saturates unsigned, so its disagreement count has to come out non-zero. That is what shows the comparison runs instead of being folded away at compile time.Output, after about fifty seconds of CPU time:
The control's twelve are the two zero radicands against the six negatively-signed dividends, which is where an unsigned saturation is wrong.
Mirror axes swept
+inf,-infPs2VuDivQ,Ps2VuSqrtQ,Ps2VuRsqrtQThese are the axes the assertions vary, not a product asserted in full. The magnitude axis is one of the open ones, and the result-sign axis is open too at a NaN result. The near-minimum class appears only as
±1e-30, in the underflow rows and as the reciprocal root's overflowing radicand: absent atPs2VuSqrtQ, absent as aPs2VuDivQdivisor, and present at one sign apiece of the twoPs2VuRsqrtQoperands. What stays open, below, records why no finite operand set closes the magnitude axis and why a NaN result's sign is left unasserted. Two cells that no operand can construct are covered by proof instead.floatis at most about1.84e19. The infinite radicands that do overflow are asserted at both signs with a mutation row apiece, so the clamp family is closed on the root by assertion rather than by this argument.std::fabsyields a non-negative value or a NaN,std::sqrtof a non-negative value is non-negative, andstd::sqrtof a NaN is a NaN. Both zeros are asserted positively signed.Standalone commit verification
The first commit's tree — the
VRSQRToperand read alone — builds and reportsFailed: 0on its own, under the build and run commands at the top of this document.Prior art — #113
VDIV's zero divisor. The sign rule used here is its author's, and he reached the operand semantics first on the sibling instruction. A single-file change whose bulk is divide-by-zero saturation, for EE COP1div.s/rsqrt.sand for VU0VDIV/VRSQRT.rsqrt.soperand fix,fd = fs / sqrt(ft), on the same "the emitter ignoredftentirely" reasoning applied here to VU0VRSQRTVRSQRTnever readfs;VRSQRTsaturated unsigned, droppingsign(fs); theft > 0.0fguard stayed, so every negative radicand still saturated wherefs / sqrt(|ft|)is an ordinary finite result;VSQRTwas untouched and still clamped to zeroVU1 micro-mode interpreter — divergences left open
PS2Runtime::executeVU0Microprogramruns VU0 microprograms on this interpreter and copies itsqintoctx->vu0_q, the register these translators write.DIV, zero divisor>= 0.0fsignbit(fs) != signbit(ft)+0divisor the two forms agree at every dividend except a-0and a positively-signed NaN; over a-0divisor they agree at exactly those two dividends and diverge at every other one. So a-0operand is not what the divergence needs:DIV NaN⁺ / +0gives+Fmaxhere and-Fmaxin the interpreter, with no-0anywhere in the pair.RSQRT, zero radicandsignbit(fs)RSQRTof a negative dividend by an ordinary+0radicand gives+Fmaxin a microprogram and-Fmaxin macro mode.ERLENG1.0fnumerator, and a divisor that is the root of a sum of squares, never a negative zeroERCPR-0operand only; the numerator is a literal1.0fERLENGandERCPRare EFU operations, so unlikeDIVthey have no second implementation here to disagree withOpen against that file: #189 reworks the VU1 flag registers and the flag-reading lower-op table, #191 the status-flag lower ops. Neither touches these sign rules.
Compilation of the emitted expression
function_emitter.cppandps2_recompiler.cppemitps2_runtime_macros.hinto every generated translation unit that contains translated instructions. A stub-only unit gets a shorter include list without it, and contains no translated instruction that could call these helpers.An anomaly, observed and ruled out
What stays open
+0/-0answer and one that keeps them does not, so this is stated rather than asserted.std::isnanof the result and not its sign bit. IEEE 754 leaves a NaN result's sign unspecified, so a sign assertion there would pin host behaviour rather than the helpers' rule.SQRT(-inf)has no row that fails it alone, for the same reason. RedefiningPS2_VU_SQRT_Qto special-case a negative infinite radicand would do it, sinceVRSQRTnever routes through that macro, but a mutant tailored to one assertion demonstrates nothing the assertion does not already state.