diff --git a/bin/resources/GameIndex.yaml b/bin/resources/GameIndex.yaml index ed33a02bea..63f4473268 100644 --- a/bin/resources/GameIndex.yaml +++ b/bin/resources/GameIndex.yaml @@ -2470,8 +2470,8 @@ SCAJ-20181: SCAJ-20182: name: "Tales of Destiny" region: "NTSC-Unk" - gameFixes: - - FpuMulHack + clampModes: + eeClampMode: 3 # Hangs otherwise: it needs the EE multiplier's one-ULP deficit, which the single-precision tier does not model. SCAJ-20183: name: "ワイルドアームズ ザ フィフスヴァンガード" name-sort: "わいるどあーむず ざ ふぃふすゔぁんがーど" @@ -2517,8 +2517,8 @@ SCAJ-20192: SCAJ-20193: name: "Tales of Destiny [Director's Cut] [Premium Box]" region: "NTSC-C-J" - gameFixes: - - FpuMulHack + clampModes: + eeClampMode: 3 # Hangs otherwise: it needs the EE multiplier's one-ULP deficit, which the single-precision tier does not model. SCAJ-20194: name: "Minna no Golf 4 [PlayStation2 the Best]" region: "NTSC-Unk" @@ -7967,8 +7967,8 @@ SCKA-20100: name: "테일즈 오브 데스티니" # Undumped on ReDump as of 2025-08-28 name-en: "Tales of Destiny" region: "NTSC-K" - gameFixes: - - FpuMulHack + clampModes: + eeClampMode: 3 # Hangs otherwise: it needs the EE multiplier's one-ULP deficit, which the single-precision tier does not model. SCKA-20101: name: "성검전설 4" name-en: "Seiken Densetsu 4" @@ -8069,8 +8069,8 @@ SCKA-20119: name: "테일즈 오브 데스티니 디렉터즈컷 [Premium Box]" # Undumped on ReDump as of 2025-08-28 name-en: "Tales of Destiny - Director's Cut [Premium Box]" region: "NTSC-K" - gameFixes: - - FpuMulHack + clampModes: + eeClampMode: 3 # Hangs otherwise: it needs the EE multiplier's one-ULP deficit, which the single-precision tier does not model. SCKA-20120: name: "라쳇 & 클랭크 - 공구들고 바캉스" # Undumped on ReDump as of 2025-08-28 name-en: "Ratchet & Clank - Vacation with Tools" # Ratchet & Clank - Size Matters @@ -61773,8 +61773,8 @@ SLPS-25715: name-sort: "ているず おぶ ですてぃにー" name-en: "Tales of Destiny" region: "NTSC-J" - gameFixes: - - FpuMulHack + clampModes: + eeClampMode: 3 # Hangs otherwise: it needs the EE multiplier's one-ULP deficit, which the single-precision tier does not model. SLPS-25716: name: "デジモンセイバーズ アナザーミッション" name-sort: "でじもんせいばーず あなざーみっしょん" @@ -62560,8 +62560,8 @@ SLPS-25841: name-sort: "ているず おぶ ですてぃにー でぃれくたーずかっと [ぷれみあむBOX]" name-en: "Tales of Destiny [Director's Cut] [Premium Box]" region: "NTSC-J" - gameFixes: - - FpuMulHack + clampModes: + eeClampMode: 3 # Hangs otherwise: it needs the EE multiplier's one-ULP deficit, which the single-precision tier does not model. memcardFilters: # Allows import of non-DC Tales of Destiny data. - "SLPS-25841" - "SLPS-25842" @@ -62571,9 +62571,9 @@ SLPS-25842: name-sort: "ているず おぶ ですてぃにー でぃれくたーずかっと" name-en: "Tales of Destiny [Director's Cut]" region: "NTSC-J" + clampModes: + eeClampMode: 3 # Hangs otherwise: it needs the EE multiplier's one-ULP deficit, which the single-precision tier does not model. compat: 5 - gameFixes: - - FpuMulHack memcardFilters: - "SLPS-25841" - "SLPS-25842" diff --git a/pcsx2-eerunner/Main.cpp b/pcsx2-eerunner/Main.cpp index 8233f0d8a7..1f1e477559 100644 --- a/pcsx2-eerunner/Main.cpp +++ b/pcsx2-eerunner/Main.cpp @@ -1577,9 +1577,9 @@ static bool NonGprDiffers(const ee_divtrace::FullSnap& jit, const ee_divtrace::F if (!ee_divtrace::g_fp_exclude) { for (int i = 0; i < 32; ++i) - if (jit.fpu.fpr[i].UL != interp.fpu.fpr[i].UL) + if (jit.fpu.fpr[i].Word() != interp.fpu.fpr[i].Word()) return true; - if (jit.fpu.ACC.UL != interp.fpu.ACC.UL) + if (jit.fpu.ACC.Word() != interp.fpu.ACC.Word()) return true; } if (jit.cpu.sa != interp.cpu.sa) @@ -1624,8 +1624,8 @@ static std::vector DiffFullSnaps(const ee_divtrace::FullSnap& jit, if (!ee_divtrace::g_fp_exclude) { for (int i = 0; i < 32; ++i) - d32(fmt::format("fpr[{}]", i), jit.fpu.fpr[i].UL, interp.fpu.fpr[i].UL); - d32("ACC", jit.fpu.ACC.UL, interp.fpu.ACC.UL); + d32(fmt::format("fpr[{}]", i), jit.fpu.fpr[i].Word(), interp.fpu.fpr[i].Word()); + d32("ACC", jit.fpu.ACC.Word(), interp.fpu.ACC.Word()); } // VU0 macro state — reported only when it participates in alignment // (EERUNNER_VU0FP), so the default report set stays what the benign-class @@ -2440,8 +2440,8 @@ static bool ZoomFromCheckpoint(const std::string& ckpt) Console.WriteLn("STEPDIFF zoom: offending-block ENTRY FPRs (interp | jit; f0-f3,f12 + diffs):"); for (int f = 0; f < 32; ++f) { - const u32 iv = ientry.fpu.fpr[f].UL; - const u32 jv = jentry.fpu.fpr[f].UL; + const u32 iv = ientry.fpu.fpr[f].Word(); + const u32 jv = jentry.fpu.fpr[f].Word(); if (iv != jv || f <= 3 || f == 12) Console.WriteLn(fmt::format(" f{:<2} {:#010x} | {:#010x}{}", f, iv, jv, iv != jv ? " <-- DIFFERS" : "")); diff --git a/pcsx2-qt/Settings/AdvancedSettingsWidget.cpp b/pcsx2-qt/Settings/AdvancedSettingsWidget.cpp index a05114c060..d450372017 100644 --- a/pcsx2-qt/Settings/AdvancedSettingsWidget.cpp +++ b/pcsx2-qt/Settings/AdvancedSettingsWidget.cpp @@ -73,6 +73,7 @@ AdvancedSettingsWidget::AdvancedSettingsWidget(SettingsWindow* settings_dialog, dialog()->registerWidgetHelp(m_ui.eeClampMode, tr("Clamping Mode"), tr("Normal (Default)"), tr("Changes how ARMSX2 handles keeping floats in a standard x86 range. " + "Exact is Full plus the rest of the EE multiplier's one-ULP deficit, and is slower than Full. " "The default value handles the vast majority of games; modifying this setting when a game is not having a visible problem can cause instability.")); dialog()->registerWidgetHelp(m_ui.eeRecompiler, tr("Enable Recompiler"), tr("Checked"), @@ -103,7 +104,7 @@ AdvancedSettingsWidget::AdvancedSettingsWidget(SettingsWindow* settings_dialog, dialog()->registerWidgetHelp(m_ui.eeFpuGuardedAddSub, tr("FPU Add/Sub Guard Bits"), tr("Checked"), //: "Guard bits" = extra low mantissa bits a standards-compliant FPU keeps during add/sub; the PS2's EE FPU does not, so the recompiler masks them to match. tr("Emulates the EE FPU's missing add/sub mantissa guard bits for hardware-accurate results. Leave checked; a few games (e.g. True Crime, Jak 3) misrender without it. " - "Unchecking is a minor speedup for EE-FPU-heavy games verified to render correctly without it. Has no effect when the EE Clamping Mode is set to Full.")); + "Unchecking is a minor speedup for EE-FPU-heavy games verified to render correctly without it. Has no effect when the EE Clamping Mode is set to Full or Exact.")); dialog()->registerWidgetHelp(m_ui.vu0RoundingMode, tr("VU0 Rounding Mode"), tr("Chop/Zero (Default)"), tr("Changes how ARMSX2 handles rounding while emulating the Emotion Engine's Vector Unit 0 (EE VU0). " "The default value handles the vast majority of games; modifying this setting when a game is not having a visible problem will cause stability issues and/or crashes.")); @@ -160,6 +161,10 @@ AdvancedSettingsWidget::~AdvancedSettingsWidget() = default; int AdvancedSettingsWidget::getGlobalClampingModeIndex(int vunum) const { + // eeClampMode 4 has no VU counterpart. + if (vunum < 0 && Host::GetBaseBoolSettingValue("EmuCore/CPU/Recompiler", "fpuExactMode", false)) + return 4; + if (Host::GetBaseBoolSettingValue( "EmuCore/CPU/Recompiler", (vunum >= 0 ? ((vunum == 0) ? "vu0SignOverflow" : "vu1SignOverflow") : "fpuFullMode"), false)) return 3; @@ -182,6 +187,9 @@ int AdvancedSettingsWidget::getClampingModeIndex(int vunum) const std::optional default_false = dialog()->isPerGameSettings() ? std::nullopt : std::optional(false); std::optional default_true = dialog()->isPerGameSettings() ? std::nullopt : std::optional(true); + std::optional fourth; + if (vunum < 0) + fourth = dialog()->getBoolValue("EmuCore/CPU/Recompiler", "fpuExactMode", default_false); std::optional third = dialog()->getBoolValue( "EmuCore/CPU/Recompiler", (vunum >= 0 ? ((vunum == 0) ? "vu0SignOverflow" : "vu1SignOverflow") : "fpuFullMode"), default_false); std::optional second = dialog()->getBoolValue("EmuCore/CPU/Recompiler", @@ -189,6 +197,8 @@ int AdvancedSettingsWidget::getClampingModeIndex(int vunum) const std::optional first = dialog()->getBoolValue( "EmuCore/CPU/Recompiler", (vunum >= 0 ? ((vunum == 0) ? "vu0Overflow" : "vu1Overflow") : "fpuOverflow"), default_true); + if (fourth.has_value() && fourth.value()) + return base + 4; if (third.has_value() && third.value()) return base + 3; if (second.has_value() && second.value()) @@ -203,11 +213,12 @@ int AdvancedSettingsWidget::getClampingModeIndex(int vunum) const void AdvancedSettingsWidget::setClampingMode(int vunum, int index) { - std::optional first, second, third; + std::optional first, second, third, fourth; if (!dialog()->isPerGameSettings() || index > 0) { const bool base = dialog()->isPerGameSettings() ? 1 : 0; + fourth = (index >= (base + 4)); third = (index >= (base + 3)); second = (index >= (base + 2)); first = (index >= (base + 1)); @@ -219,6 +230,12 @@ void AdvancedSettingsWidget::setClampingMode(int vunum, int index) "EmuCore/CPU/Recompiler", (vunum >= 0 ? ((vunum == 0) ? "vu0ExtraOverflow" : "vu1ExtraOverflow") : "fpuExtraOverflow"), second); dialog()->setBoolSettingValue( "EmuCore/CPU/Recompiler", (vunum >= 0 ? ((vunum == 0) ? "vu0Overflow" : "vu1Overflow") : "fpuOverflow"), first); + + // fpuExactMode is eeClampMode 4's bit, and the VUs have none. It is written + // with the other three because ApplySanityCheck drops a config whose bits + // are not a whole mode to the default, not to the mode that was picked. + if (vunum < 0) + dialog()->setBoolSettingValue("EmuCore/CPU/Recompiler", "fpuExactMode", fourth); } void AdvancedSettingsWidget::onSavestateCompressionTypeChanged() diff --git a/pcsx2-qt/Settings/AdvancedSettingsWidget.ui b/pcsx2-qt/Settings/AdvancedSettingsWidget.ui index ce17e93239..8974f7c982 100644 --- a/pcsx2-qt/Settings/AdvancedSettingsWidget.ui +++ b/pcsx2-qt/Settings/AdvancedSettingsWidget.ui @@ -127,6 +127,11 @@ Full + + + Exact + + diff --git a/pcsx2-qt/Settings/GameFixSettingsWidget.cpp b/pcsx2-qt/Settings/GameFixSettingsWidget.cpp index 899b5aa142..2796081764 100644 --- a/pcsx2-qt/Settings/GameFixSettingsWidget.cpp +++ b/pcsx2-qt/Settings/GameFixSettingsWidget.cpp @@ -17,7 +17,6 @@ GameFixSettingsWidget::GameFixSettingsWidget(SettingsWindow* settings_dialog, QW setupTab(m_ui); - SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.FpuMulHack, "EmuCore/Gamefixes", "FpuMulHack", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.GoemonTlbHack, "EmuCore/Gamefixes", "GoemonTlbHack", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.SoftwareRendererFMVHack, "EmuCore/Gamefixes", "SoftwareRendererFMVHack", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.SkipMPEGHack, "EmuCore/Gamefixes", "SkipMPEGHack", false); @@ -36,7 +35,6 @@ GameFixSettingsWidget::GameFixSettingsWidget(SettingsWindow* settings_dialog, QW SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.XgKickHack, "EmuCore/Gamefixes", "XgKickHack", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.BlitInternalFPSHack, "EmuCore/Gamefixes", "BlitInternalFPSHack", false); - dialog()->registerWidgetHelp(m_ui.FpuMulHack, tr("FPU Multiply Hack"), tr("Unchecked"), tr("For Tales of Destiny.")); dialog()->registerWidgetHelp(m_ui.GoemonTlbHack, tr("Preload TLB Hack"), tr("Unchecked"), tr("To avoid TLB miss on Goemon.")); dialog()->registerWidgetHelp(m_ui.SoftwareRendererFMVHack, tr("Use Software Renderer For FMVs"), tr("Unchecked"), tr("Needed for some games with complex FMV rendering.")); dialog()->registerWidgetHelp(m_ui.SkipMPEGHack, tr("Skip MPEG Hack"), tr("Unchecked"), tr("Skips videos/FMVs in games to avoid game hanging/freezes.")); diff --git a/pcsx2-qt/Settings/GameFixSettingsWidget.ui b/pcsx2-qt/Settings/GameFixSettingsWidget.ui index 393a90821b..1a3839c15e 100644 --- a/pcsx2-qt/Settings/GameFixSettingsWidget.ui +++ b/pcsx2-qt/Settings/GameFixSettingsWidget.ui @@ -17,13 +17,6 @@ Game Fixes - - - - FPU Multiply Hack - - - @@ -162,7 +155,6 @@ - FpuMulHack SoftwareRendererFMVHack SkipMPEGHack GoemonTlbHack diff --git a/pcsx2/CMakeLists.txt b/pcsx2/CMakeLists.txt index f877305972..7c49be0c77 100644 --- a/pcsx2/CMakeLists.txt +++ b/pcsx2/CMakeLists.txt @@ -158,6 +158,7 @@ set(pcsx2Headers COP0.h Counters.h Dmac.h + EeFpuFormat.h GameDatabase.h Elfheader.h FW.h diff --git a/pcsx2/Config.h b/pcsx2/Config.h index b933ca390f..5748953d27 100644 --- a/pcsx2/Config.h +++ b/pcsx2/Config.h @@ -769,6 +769,7 @@ struct Pcsx2Config fpuOverflow : 1, fpuExtraOverflow : 1, fpuFullMode : 1, + fpuExactMode : 1, fpuGuardedAddSub : 1; // EE FPU add/sub guard-bit emulation (single-precision fast path). ON by default — the PS2-accurate behavior. Opt-OUT globally via INI for EE-FPU-heavy titles verified to render fine without it (each ADD.S/SUB.S then costs one op instead of the guard sequence). Independent of the clamp tiers: Full mode runs the DOUBLE path, which guards unconditionally regardless of this bit. bool @@ -1301,6 +1302,10 @@ struct Pcsx2Config { BITFIELD32() bool + // No reader: eeMulRound (FPU.cpp) and emitDefectiveFmul + // (iFPUd-arm64.cpp) model the multiplier defect this patched one + // product of. The bit stays because its GamefixId indexes + // vu_capture's on-disk gamefix mask. FpuMulHack : 1, // Tales of Destiny hangs. GoemonTlbHack : 1, // Gomeon tlb miss hack. The game need to access unmapped virtual address. Instead to handle it as exception, tlb are preloaded at startup SoftwareRendererFMVHack : 1, // Switches to software renderer for FMVs @@ -1718,7 +1723,6 @@ namespace EmuFolders //------------ SPECIAL GAME FIXES!!! --------------- #define CHECK_VUADDSUBHACK (EmuConfig.Gamefixes.VuAddSubHack) // Special Fix for Tri-ace games, they use an encryption algorithm that requires VU addi opcode to be bit-accurate. -#define CHECK_FPUMULHACK (EmuConfig.Gamefixes.FpuMulHack) // Special Fix for Tales of Destiny hangs. #define CHECK_XGKICKHACK (EmuConfig.Gamefixes.XgKickHack) // Special Fix for Erementar Gerad, adds more delay to VU XGkick instructions. Corrects the color of some graphics. #define CHECK_EETIMINGHACK (EmuConfig.Gamefixes.EETimingHack) // Fix all scheduled events to happen in 1 cycle. #define CHECK_INSTANTDMAHACK (EmuConfig.Gamefixes.InstantDMAHack) // Attempt to finish DMA's instantly, useful for games which rely on cache emulation. @@ -1740,7 +1744,8 @@ namespace EmuFolders #define CHECK_FPU_OVERFLOW (EmuConfig.Cpu.Recompiler.fpuOverflow) #define CHECK_FPU_EXTRA_OVERFLOW (EmuConfig.Cpu.Recompiler.fpuExtraOverflow) // If enabled, Operands are checked for infinities before being used in the FPU recs #define CHECK_FPU_EXTRA_FLAGS 1 // Always enabled now // Sets D/I flags on FPU instructions -#define CHECK_FPU_FULL (EmuConfig.Cpu.Recompiler.fpuFullMode) +#define CHECK_FPU_FULL (EmuConfig.Cpu.Recompiler.fpuFullMode) // GameDB eeClampMode >= 3: the EE FPU's arithmetic is iFPUd's, computed in double over a relocated FPR file. Below it the single-precision fast path in iFPU-arm64.cpp runs. +#define CHECK_FPU_EXACT (EmuConfig.Cpu.Recompiler.fpuExactMode) // GameDB eeClampMode 4: mode 3 plus the rest of the EE multiplier's one-ULP deficit, at emitDefectiveFmul (iFPUd-arm64.cpp). #define CHECK_FPU_GUARDED (EmuConfig.Cpu.Recompiler.fpuGuardedAddSub) // If enabled (default), add/sub emulate the PS2 FPU's missing mantissa guard bits on the single-precision fast path. Disable only for EE-heavy titles confirmed not to need it. //------------ EE Recompiler defines - Comment to disable a recompiler --------------- diff --git a/pcsx2/DebugTools/DebugInterface.cpp b/pcsx2/DebugTools/DebugInterface.cpp index 8d6552f208..0f0d2d5bb8 100644 --- a/pcsx2/DebugTools/DebugInterface.cpp +++ b/pcsx2/DebugTools/DebugInterface.cpp @@ -18,6 +18,8 @@ #include "common/StringUtil.h" +#include + R5900DebugInterface r5900Debug; R3000DebugInterface r3000Debug; @@ -464,7 +466,7 @@ u128 R5900DebugInterface::getRegister(int cat, int num) result = u128::From32(cpuRegs.CP0.r[num]); break; case EECAT_FPR: - result = u128::From32(fpuRegs.fpr[num].UL); + result = u128::From32(fpuRegs.fpr[num].Word()); break; case EECAT_FCR: result = u128::From32(fpuRegs.fprc[num]); @@ -504,7 +506,12 @@ std::string R5900DebugInterface::getRegisterString(int cat, int num) case EECAT_VU0F: return StringUtil::U128ToString(getRegister(cat, num)); case EECAT_FPR: - return StringUtil::StdStringFromFormat("%f", fpuRegs.fpr[num].f); + { + const u32 word = fpuRegs.fpr[num].Word(); + float f; + std::memcpy(&f, &word, sizeof(f)); + return StringUtil::StdStringFromFormat("%f", f); + } default: return {}; } @@ -562,7 +569,7 @@ void R5900DebugInterface::setRegister(int cat, int num, u128 newValue) cpuRegs.CP0.r[num] = newValue._u32[0]; break; case EECAT_FPR: - fpuRegs.fpr[num].UL = newValue._u32[0]; + fpuRegs.fpr[num].SetWord(newValue._u32[0]); break; case EECAT_FCR: fpuRegs.fprc[num] = newValue._u32[0]; diff --git a/pcsx2/Docs/GameIndex.md b/pcsx2/Docs/GameIndex.md index 283d826bc6..db69eb2eea 100644 --- a/pcsx2/Docs/GameIndex.md +++ b/pcsx2/Docs/GameIndex.md @@ -26,8 +26,6 @@ SERIAL-12345: # !required! Serial number for the game, this is how games are loo # If you'd like to temporarily disable it, either comment out the line, or remove it! gameFixes: - VuAddSubHack - - FpuMulHack - - FpuNegDivHack - XGKickHack - EETimingHack - SkipMPEGHack @@ -123,6 +121,8 @@ The clamp modes are also numerically based. * `1` = Clamp **Normally** (only clamp results) * `2` = Clamp **Extra+Preserve Sign** (clamp results as well as operands) * `3` = **Full Clamping** for FPU +* `4` = **Exact**: mode 3 plus the rest of the EE multiplier's one-ULP deficit. + Multiplies it cannot decide from ft's mantissa call out of line. ### vuClampMode @@ -205,9 +205,6 @@ These values are case-sensitive, so take care. If you incorrectly specify a Gam ### Game Fixes Options -* `FpuMulHack` - * For Tales of Destiny: This fix addresses hanging issues. - * `SoftwareRendererFMVHack` * Used for complex FMV rendering in certain games. diff --git a/pcsx2/Docs/gamedb-schema.json b/pcsx2/Docs/gamedb-schema.json index d40b8630a9..79a2fa222e 100644 --- a/pcsx2/Docs/gamedb-schema.json +++ b/pcsx2/Docs/gamedb-schema.json @@ -69,9 +69,9 @@ "properties": { "eeClampMode": { "type": "integer", - "description": "0 (Disables), 1 (Normally), 2 (Extra+Preserve Sign), 3 (Full Clamping)", + "description": "0 (Disables), 1 (Normally), 2 (Extra+Preserve Sign), 3 (Full Clamping), 4 (Exact)", "minimum": 0, - "maximum": 3 + "maximum": 4 }, "vuClampMode": { "type": "integer", @@ -101,7 +101,6 @@ "BlitInternalFPSHack", "DMABusyHack", "EETimingHack", - "FpuMulHack", "GIFFIFOHack", "GoemonTlbHack", "IbitHack", diff --git a/pcsx2/EeFpuFormat.h b/pcsx2/EeFpuFormat.h new file mode 100644 index 0000000000..6e41107fc4 --- /dev/null +++ b/pcsx2/EeFpuFormat.h @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2026 ARMSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include "common/Pcsx2Defs.h" + +#include + +/* An EE FPR word held in a host double: + + stored = sign << 63 | (word & 0x7FFFFFFF) << 29 + + Uniformly stored == eeValue(word) / 2^896. The scale puts 2^-126 on + 0x1p-1022, so FPCR.FZ flushes at the EE's boundary; it keeps the double + exponent field under 256, so the EE's top binade is an ordinary number + here; and it is bijective, so a raw int32 parked in an FPR survives. +*/ + +static constexpr int kEeFprScaleExp = 896; + +__fi static constexpr u64 eeFprWidenBits(u32 word) +{ + return (static_cast(word & 0x80000000u) << 32) | (static_cast(word & 0x7FFFFFFFu) << 29); +} + +__fi static constexpr u32 eeFprNarrowBits(u64 stored) +{ + return static_cast((stored >> 32) & 0x80000000u) | static_cast((stored >> 29) & 0x7FFFFFFFu); +} + +// Bounds and scale factor for code computing in this domain. 0x7FFFFFFF is the +// largest number the FPU has. +static constexpr u64 kEeFprMaxBits = eeFprWidenBits(0x7FFFFFFFu); +static constexpr u64 kEeFprMinBits = eeFprWidenBits(0xFFFFFFFFu); +static constexpr u64 kEeFprUnscaleBits = UINT64_C(0x77F0000000000000); +static constexpr u64 kEeFprSignBit = UINT64_C(0x8000000000000000); +static constexpr u64 kEeFprExpMask = UINT64_C(0x7FF0000000000000); + +static_assert(kEeFprMaxBits == UINT64_C(0x0FFFFFFFE0000000)); +static_assert(kEeFprMinBits == UINT64_C(0x8FFFFFFFE0000000)); +static_assert((kEeFprUnscaleBits >> 52) == static_cast(1023 + kEeFprScaleExp), + "kEeFprUnscaleBits must be 2^kEeFprScaleExp"); + +__fi static double eeFprWiden(u32 word) +{ + const u64 bits = eeFprWidenBits(word); + double d; + std::memcpy(&d, &bits, sizeof(d)); + return d; +} + +__fi static u32 eeFprNarrow(double stored) +{ + u64 bits; + std::memcpy(&bits, &stored, sizeof(bits)); + return eeFprNarrowBits(bits); +} + +/* Whether the FPR file is in this domain right now. It follows the EE FPU's + clamp mode: only iFPUd computes here, and modes 0 to 2 leave the + architectural word in the slot's low half. Read it through + FPRreg::Word()/SetWord(). +*/ +extern bool g_eeFprSlotsRelocated; + +/* Put the file in the format the current clamp mode calls for. Emitted code + assumes one format, so call this with the code-cache reset a mode change + already forces. +*/ +void eeFprSyncSlotFormat(); diff --git a/pcsx2/FPU.cpp b/pcsx2/FPU.cpp index 6445207a13..d5aceb0014 100644 --- a/pcsx2/FPU.cpp +++ b/pcsx2/FPU.cpp @@ -8,6 +8,7 @@ #include #include +#include // Helper Macros //**************************************************************** @@ -30,23 +31,23 @@ #define _Fs_ ( ( cpuRegs.code >> 11 ) & 0x1F ) #define _Fd_ ( ( cpuRegs.code >> 6 ) & 0x1F ) -// Floats -#define _FtValf_ fpuRegs.fpr[ _Ft_ ].f -#define _FsValf_ fpuRegs.fpr[ _Fs_ ].f -#define _FdValf_ fpuRegs.fpr[ _Fd_ ].f -#define _FAValf_ fpuRegs.ACC.f +/* The word is never an lvalue here: a slot is wider than the word it holds + (R5900.h), so reads go through the accessor and writes through a setter. +*/ // U32's -#define _FtValUl_ fpuRegs.fpr[ _Ft_ ].UL -#define _FsValUl_ fpuRegs.fpr[ _Fs_ ].UL -#define _FdValUl_ fpuRegs.fpr[ _Fd_ ].UL -#define _FAValUl_ fpuRegs.ACC.UL +#define _FtValUl_ fpuRegs.fpr[ _Ft_ ].Word() +#define _FsValUl_ fpuRegs.fpr[ _Fs_ ].Word() +#define _FdValUl_ fpuRegs.fpr[ _Fd_ ].Word() +#define _FAValUl_ fpuRegs.ACC.Word() -// S32's - useful for ensuring sign extension when needed. -#define _FtValSl_ fpuRegs.fpr[ _Ft_ ].SL -#define _FsValSl_ fpuRegs.fpr[ _Fs_ ].SL -#define _FdValSl_ fpuRegs.fpr[ _Fd_ ].SL -#define _FAValSl_ fpuRegs.ACC.SL +// S32 - useful for ensuring sign extension when needed. +#define _FsValSl_ static_cast( _FsValUl_ ) + +// Destination writes +#define _SetFsVal_( w ) fpuRegs.fpr[ _Fs_ ].SetWord( w ) +#define _SetFdVal_( w ) fpuRegs.fpr[ _Fd_ ].SetWord( w ) +#define _SetFAVal_( w ) fpuRegs.ACC.SetWord( w ) // FPU Control Reg (FCR31) #define _ContVal_ fpuRegs.fprc[ 31 ] @@ -64,6 +65,42 @@ //**************************************************************** +bool g_eeFprSlotsRelocated = false; + +void eeFprSyncSlotFormat() +{ + const bool want = CHECK_FPU_FULL; + if (want == g_eeFprSlotsRelocated) + return; + + // Read every word in the outgoing format before changing it, since the + // accessors are what the format means. + u32 words[std::size(fpuRegs.fpr)]; + for (size_t i = 0; i < std::size(words); i++) + words[i] = fpuRegs.fpr[i].Word(); + const u32 acc = fpuRegs.ACC.Word(); + + g_eeFprSlotsRelocated = want; + + for (size_t i = 0; i < std::size(words); i++) + fpuRegs.fpr[i].SetWord(words[i]); + fpuRegs.ACC.SetWord(acc); +} + +static u32 floatToBits(float f) +{ + u32 bits; + std::memcpy(&bits, &f, sizeof(bits)); + return bits; +} + +static float bitsToFloat(u32 bits) +{ + float f; + std::memcpy(&f, &bits, sizeof(f)); + return f; +} + /* The EE value of a raw FPR word, exactly, as a double. The only way an operand enters this file: every arithmetic op, every flag @@ -121,9 +158,36 @@ static void clampToEeRange(u32& xReg) xReg &= 0x80000000; } +/* Whether a result saturates, decided after the rounding and not on the exact + value. The unit normalises and rounds to 24 bits before anything looks at + the exponent field, so a result can exceed kEeFpuMax and still come back on + it: 0x7FFFFFFF + 2^104 is 2^129 - 2^104, which needs 25 significant bits and + chops to 0x7FFFFFFF. One exponent higher the sum is 2^129, which does not + fit however it is rounded. + + Above 2^129 no rounding brings a value back, which also keeps the scaled + cast below in float range. + + The rounding is the (float) cast's, so the ambient FPCR decides it here as + it does everywhere else in this file -- under round-to-nearest the same + 2^129 - 2^104 is a tie that goes to 2^129 and does saturate. +*/ +static bool eeRoundsOutOfRange(double exact) +{ + const double mag = std::fabs(exact); + if (mag <= kEeFpuMax) + return false; + if (mag >= 0x1p129) + return true; + const u32 w = floatToBits(static_cast(exact * 0x1p-4)); + return ((w >> 23) & 0xFFu) + 4u > 255u; +} + /* One rounding step's worth of FCR31 O/U maintenance, from the magnitude of - the exact result. `exact` is the step's result recomputed through - eeToDouble(), where nothing was clamped, rounded away or flushed. + the value the step produced -- the sum or product as a double, before the + narrowing to an EE single but after everything the unit itself did to the + operands. For the adder that means after the guard mask (eeGuardedSum): the + flags report what was added, not what was asked for. checkOverflow()/checkUnderflow() used to ask instead whether xReg had come back as a host infinity or a host denormal. Neither ever appears in the FP @@ -142,21 +206,22 @@ static void clampToEeRange(u32& xReg) static void raiseOrClearOU(double exact) { _ContVal_ &= ~(FPUflagO | FPUflagU); - if (std::fabs(exact) > kEeFpuMax) + if (eeRoundsOutOfRange(exact)) _ContVal_ |= FPUflagO | FPUflagSO; else if (exact != 0.0 && std::fabs(exact) < kEeMinNormal) _ContVal_ |= FPUflagU | FPUflagSU; } /* The multiply-accumulates round twice, so they raise twice: once on the - intermediate product, once on the accumulate. These two predicates are what - the product hands on to the second step. + intermediate product, once on the accumulate. This predicate is what the + product hands on to the second step. An underflowing product is flushed to signed zero before the accumulate, so the accumulate sees ACC and clears the cause U again, leaving the sticky SU up. 68 cases in the capture come back with SU set and U clear; all are multiply-accumulates and no plain MUL/MULA ever does, which is what says two - steps rather than one. + steps rather than one. The flush needs no helper: the product reaches the + adder through eeMulRound(), which returns a signed zero for it. An overflowing product ends the instruction. Silicon saturates there and the accumulate cannot bring it back: MADD of 2^128 by 2.0 onto an ACC of -2^128 @@ -170,13 +235,6 @@ static bool madAccumulandOverflowed(double product) return std::fabs(product) > kEeFpuMax; } -static double madFlushedProduct(double product) -{ - if (product != 0.0 && std::fabs(product) < kEeMinNormal) - return std::copysign(0.0, product); - return product; -} - __fi u32 fp_max(u32 a, u32 b) { return ((s32)a < 0 && (s32)b < 0) ? std::min(a, b) : std::max(a, b); @@ -219,10 +277,9 @@ bool checkDivideByZero(u32& xReg, u32 yDivisorReg, u32 zDividendReg, u32 cFlagsT #ifdef comparePrecision // This compare discards the least-significant bit(s) in order to solve some rounding issues. #define C_cond_S(cond) { \ - FPRreg tempA, tempB; \ - tempA.UL = _FsValUl_ & comparePrecision; \ - tempB.UL = _FtValUl_ & comparePrecision; \ - _ContVal_ = ( ( tempA.f ) cond ( tempB.f ) ) ? \ + const float tempA = bitsToFloat( _FsValUl_ & comparePrecision ); \ + const float tempB = bitsToFloat( _FtValUl_ & comparePrecision ); \ + _ContVal_ = ( ( tempA ) cond ( tempB ) ) ? \ ( _ContVal_ | FPUflagC ) : \ ( _ContVal_ & ~FPUflagC ); \ } @@ -326,7 +383,7 @@ static u32 eeRoundToSingle(double exact, bool addsub = false) { const double mag = std::fabs(exact); - if (mag > kEeFpuMax) + if (eeRoundsOutOfRange(exact)) return (std::signbit(exact) ? 0x80000000u : 0u) | 0x7FFFFFFFu; if (mag < kEeMinNormal) @@ -344,20 +401,16 @@ static u32 eeRoundToSingle(double exact, bool addsub = false) return sign | static_cast((bits >> 29) & 0x7FFFFFu); } - FPRreg r; if (mag >= 0x1p126) { /* Scale down by 2^4, round there, then add the 4 exponents back. The scaled exponent field is at most 251, so the +4 cannot carry into the sign, and the >= 2^126 floor keeps the scaled value normal, so nothing is flushed on the way through. */ - r.f = static_cast(exact * 0x1p-4); - r.UL += 4u << 23; - return r.UL; + return floatToBits(static_cast(exact * 0x1p-4)) + (4u << 23); } - r.f = static_cast(exact); - return r.UL; + return floatToBits(static_cast(exact)); } /* The EE FPU's adder carries no guard bits to the right of the mantissa. A @@ -374,7 +427,7 @@ static u32 eeRoundToSingle(double exact, bool addsub = false) nothing left but its sign. |diff| <= 1 masks nothing. Ported from x86 FPU_ADD_SUB (x86/iFPU.cpp) and, on arm64, fpuEmitGuardedAddSub - (iFPU-arm64.cpp, the single-precision fast path) and FPU_ADD_SUB + (iFPU-arm64.cpp, the single-precision fast path) and FPU_ADD_SUB_D (iFPUd-arm64.cpp, the Full-clamp DOUBLE path). Both recompilers gate the masking on CHECK_FPU_GUARDED, the fpuGuardedAddSub @@ -409,13 +462,20 @@ static void fpuGuardMask(u32& a, u32& b) on the hardware. Subtraction is addition of the negated operand, as IEEE defines it: that gets - the zero signs right, including for a masked +-0. */ -static u32 eeGuardedAddSub(u32 a, u32 b, bool issub) + the zero signs right, including for a masked +-0. + + This returns the sum rather than the rounded word because the flags come off + it too: what the adder produced is what FCR31 reports, so its callers round + it for the destination and hand this same value to raiseOrClearOU() instead + of recomputing a second sum from the unmasked operands. Where the mask + erases an operand the two differ, and the console follows this one -- see + ee_fpu_ou_rounding_console_tests.cpp. */ +static double eeGuardedSum(u32 a, u32 b, bool issub) { fpuGuardMask(a, b); if (issub) b ^= 0x80000000; - return eeRoundToSingle(eeToDouble(a) + eeToDouble(b), true); + return eeToDouble(a) + eeToDouble(b); } /* The EE's divide/square-root unit, digit by digit. @@ -600,7 +660,7 @@ static u32 eeDivideSignificand(u32 sma, u32 smb) return (quotient << 1) + eeSrtDigitValue(digit); } -static u32 eeDivide(u32 a, u32 b) +u32 eeDivide(u32 a, u32 b) { const s32 ea = (s32)((a >> 23) & 0xFF); const s32 eb = (s32)((b >> 23) & 0xFF); @@ -696,7 +756,7 @@ static u32 eeSqrtSignificand(u32 m) return (root >> 2) & 0xFFFFFFu; } -static u32 eeSqrtBits(u32 t) +u32 eeSqrtBits(u32 t) { const u32 E = (t >> 23) & 0xFFu; if (E == 0) @@ -712,32 +772,37 @@ static u32 eeSqrtBits(u32 t) any more: DIV.S, SQRT.S and RSQRT.S are integer arithmetic now, and no rounding mode reaches a digit recurrence. That register is PCSX2's surrogate for the divide unit rounding to nearest while the rest of the FPU chops, and - both recompilers still swap it in, because they run these ops on host singles - (arm64 recDIV_S_xmm / recSQRT_S_xmm / recRSQRT_S_xmm in iFPU-arm64.cpp and - the DOUBLE:: twins in iFPUd-arm64.cpp; x86 iFPU.cpp / iFPUd.cpp with - xLDMXCSR): 1.0 rsqrt 1.5 is 0x3F5105EB on the console and 0x3F5105EC without - the swap. So the two engines part company on every operand silicon is not + the recompiler tiers that still run these ops on host floats swap it in: + arm64's fast path (recDIV_S_xmm / recSQRT_S_xmm / recRSQRT_S_xmm in + iFPU-arm64.cpp) and every x86 tier (iFPU.cpp / iFPUd.cpp with xLDMXCSR). + 1.0 rsqrt 1.5 is 0x3F5105EB on the console and 0x3F5105EC without the swap, + so those tiers part company with this one on every operand silicon is not correctly rounded on, which EeRecFpuDivUnitRounding and EeRecFpuRsqrt pin. + + arm64's eeClampMode 4 calls eeDivide and eeSqrtBits out of line instead -- + emitDivideUnitIsland in iFPUd-arm64.cpp -- so it has no rounding mode to + swap and nothing to diverge over. */ void ABS_S() { - _FdValUl_ = _FsValUl_ & 0x7fffffff; + _SetFdVal_( _FsValUl_ & 0x7fffffff ); clearFPUFlags( FPUflagO | FPUflagU ); } -/* Every op below computes `exact` before writing its destination: fd may alias - fs or ft, and the accumulator forms read the ACC they are about to write. +/* Every op below computes its result before writing its destination: fd may + alias fs or ft, and the accumulator forms read the ACC they are about to + write. */ void ADD_S() { - const double exact = eeToDouble( _FsValUl_ ) + eeToDouble( _FtValUl_ ); - _FdValUl_ = eeGuardedAddSub( _FsValUl_, _FtValUl_, false ); - raiseOrClearOU( exact ); + const double sum = eeGuardedSum( _FsValUl_, _FtValUl_, false ); + _SetFdVal_( eeRoundToSingle( sum, true ) ); + raiseOrClearOU( sum ); } void ADDA_S() { - const double exact = eeToDouble( _FsValUl_ ) + eeToDouble( _FtValUl_ ); - _FAValUl_ = eeGuardedAddSub( _FsValUl_, _FtValUl_, false ); - raiseOrClearOU( exact ); + const double sum = eeGuardedSum( _FsValUl_, _FtValUl_, false ); + _SetFAVal_( eeRoundToSingle( sum, true ) ); + raiseOrClearOU( sum ); } void BC1F() { @@ -790,13 +855,13 @@ void CTC1() { } void CVT_S() { - _FdValf_ = (float)_FsValSl_; + _SetFdVal_( floatToBits( (float)_FsValSl_ ) ); } void CVT_W() { - if ( ( _FsValUl_ & 0x7F800000 ) <= 0x4E800000 ) { _FdValSl_ = (s32)_FsValf_; } - else if ( ( _FsValUl_ & 0x80000000 ) == 0 ) { _FdValUl_ = 0x7fffffff; } - else { _FdValUl_ = 0x80000000; } + if ( ( _FsValUl_ & 0x7F800000 ) <= 0x4E800000 ) { _SetFdVal_( (u32)(s32)bitsToFloat( _FsValUl_ ) ); } + else if ( ( _FsValUl_ & 0x80000000 ) == 0 ) { _SetFdVal_( 0x7fffffff ); } + else { _SetFdVal_( 0x80000000 ); } } void DIV_S() { @@ -804,8 +869,13 @@ void DIV_S() { // and RSQRT.S. clearFPUFlags(FPUflagI | FPUflagD); - if (checkDivideByZero( _FdValUl_, _FtValUl_, _FsValUl_, FPUflagD | FPUflagSD, FPUflagI | FPUflagSI)) return; - _FdValUl_ = eeDivide( _FsValUl_, _FtValUl_ ); + u32 saturated; + if (checkDivideByZero( saturated, _FtValUl_, _FsValUl_, FPUflagD | FPUflagSD, FPUflagI | FPUflagSI)) + { + _SetFdVal_( saturated ); + return; + } + _SetFdVal_( eeDivide( _FsValUl_, _FtValUl_ ) ); } /* The EE multiplier's one-ULP deficit. @@ -943,7 +1013,7 @@ static u64 eeMulArray(u32 a, u32 b) return full - (((lo + hi) ^ full) & 0x8000); } -static bool eeMulOneUlpLow(u32 fs, u32 ft) +bool eeMulOneUlpLow(u32 fs, u32 ft) { if ((fs & 0x7F800000) == 0 || (ft & 0x7F800000) == 0) return false; // a zero operand (denormals are zero): the product is zero @@ -979,11 +1049,11 @@ static u32 eeMulRound(u32 fs, u32 ft, double exact) */ /* The product is its own named double, so -ffp-contract cannot fuse away the two roundings the PS2 ISA mandates -- and so the two flag steps below have - something separate to look at. See raiseOrClearOU/madFlushedProduct. + something separate to look at. See raiseOrClearOU/madAccumulandOverflowed. The A-forms name their single-precision product too, because the guarded adder needs its bits, and that takes the accumulate out of the compiler's reach as - well: `_FAValf_ += fs * ft` is one expression a contracting compiler can turn + well: `acc += fs * ft` is one expression a contracting compiler can turn into a single-rounded FMA, with only the -ffp-contract=off line in pcsx2/CMakeLists.txt between it and that. On corpus cases 567 and 1130 the fused value is the console's, so a contracting build hid the guard-bit defect @@ -998,35 +1068,36 @@ static u32 eeMulRound(u32 fs, u32 ft, double exact) madAccumulandOverflowed() has always made for the flag; the value follows it now too. */ -static u32 eeMulAccumulate(u32 fs, u32 ft, u32 accbits, bool issub) +static u32 eeMulAccumulate(u32 fs, u32 ft, u32 accbits, bool issub, double* sum) { const double product = eeToDouble( fs ) * eeToDouble( ft ); const u32 rounded = eeMulRound( fs, ft, product ) ^ (issub ? 0x80000000u : 0u); if (madAccumulandOverflowed( product )) return rounded; - return eeGuardedAddSub( accbits, rounded, false ); + *sum = eeGuardedSum( accbits, rounded, false ); + return eeRoundToSingle( *sum, true ); } void MADD_S() { const double product = eeToDouble( _FsValUl_ ) * eeToDouble( _FtValUl_ ); - const double acc = eeToDouble( _FAValUl_ ); - _FdValUl_ = eeMulAccumulate( _FsValUl_, _FtValUl_, _FAValUl_, false ); + double sum = 0.0; + _SetFdVal_( eeMulAccumulate( _FsValUl_, _FtValUl_, _FAValUl_, false, &sum ) ); raiseOrClearOU( product ); if (madAccumulandOverflowed( product )) return; - raiseOrClearOU( acc + madFlushedProduct( product ) ); + raiseOrClearOU( sum ); } void MADDA_S() { const double product = eeToDouble( _FsValUl_ ) * eeToDouble( _FtValUl_ ); - const double acc = eeToDouble( _FAValUl_ ); - _FAValUl_ = eeMulAccumulate( _FsValUl_, _FtValUl_, _FAValUl_, false ); + double sum = 0.0; + _SetFAVal_( eeMulAccumulate( _FsValUl_, _FtValUl_, _FAValUl_, false, &sum ) ); raiseOrClearOU( product ); if (madAccumulandOverflowed( product )) return; - raiseOrClearOU( acc + madFlushedProduct( product ) ); + raiseOrClearOU( sum ); } void MAX_S() { - _FdValUl_ = fp_max( _FsValUl_, _FtValUl_ ); + _SetFdVal_( fp_max( _FsValUl_, _FtValUl_ ) ); clearFPUFlags( FPUflagO | FPUflagU ); } @@ -1036,34 +1107,34 @@ void MFC1() { } void MIN_S() { - _FdValUl_ = fp_min(_FsValUl_, _FtValUl_); + _SetFdVal_( fp_min( _FsValUl_, _FtValUl_ ) ); clearFPUFlags( FPUflagO | FPUflagU ); } void MOV_S() { - _FdValUl_ = _FsValUl_; + _SetFdVal_( _FsValUl_ ); } void MSUB_S() { const double product = eeToDouble( _FsValUl_ ) * eeToDouble( _FtValUl_ ); - const double acc = eeToDouble( _FAValUl_ ); - _FdValUl_ = eeMulAccumulate( _FsValUl_, _FtValUl_, _FAValUl_, true ); + double sum = 0.0; + _SetFdVal_( eeMulAccumulate( _FsValUl_, _FtValUl_, _FAValUl_, true, &sum ) ); raiseOrClearOU( product ); if (madAccumulandOverflowed( product )) return; - raiseOrClearOU( acc - madFlushedProduct( product ) ); + raiseOrClearOU( sum ); } void MSUBA_S() { const double product = eeToDouble( _FsValUl_ ) * eeToDouble( _FtValUl_ ); - const double acc = eeToDouble( _FAValUl_ ); - _FAValUl_ = eeMulAccumulate( _FsValUl_, _FtValUl_, _FAValUl_, true ); + double sum = 0.0; + _SetFAVal_( eeMulAccumulate( _FsValUl_, _FtValUl_, _FAValUl_, true, &sum ) ); raiseOrClearOU( product ); if (madAccumulandOverflowed( product )) return; - raiseOrClearOU( acc - madFlushedProduct( product ) ); + raiseOrClearOU( sum ); } void MTC1() { - _FsValUl_ = cpuRegs.GPR.r[_Rt_].UL[0]; + _SetFsVal_( cpuRegs.GPR.r[_Rt_].UL[0] ); } /* The product of two EE singles is 48 significand bits, so a double holds it @@ -1076,18 +1147,18 @@ void MTC1() { */ void MUL_S() { const double exact = eeToDouble( _FsValUl_ ) * eeToDouble( _FtValUl_ ); - _FdValUl_ = eeMulRound( _FsValUl_, _FtValUl_, exact ); + _SetFdVal_( eeMulRound( _FsValUl_, _FtValUl_, exact ) ); raiseOrClearOU( exact ); } void MULA_S() { const double exact = eeToDouble( _FsValUl_ ) * eeToDouble( _FtValUl_ ); - _FAValUl_ = eeMulRound( _FsValUl_, _FtValUl_, exact ); + _SetFAVal_( eeMulRound( _FsValUl_, _FtValUl_, exact ) ); raiseOrClearOU( exact ); } void NEG_S() { - _FdValUl_ = (_FsValUl_ ^ 0x80000000); + _SetFdVal_( _FsValUl_ ^ 0x80000000 ); clearFPUFlags( FPUflagO | FPUflagU ); } @@ -1110,7 +1181,7 @@ void RSQRT_S() { // time the division happens. Console rows 59 and 63: rsqrt(+0, -0) is // +0x7FFFFFFF and rsqrt(-0, -0) is -0x7FFFFFFF, and an xor rule flips // both. - _FdValUl_ = ( _FsValUl_ & 0x80000000 ) | 0x7FFFFFFF; + _SetFdVal_( ( _FsValUl_ & 0x80000000 ) | 0x7FFFFFFF ); return; } @@ -1131,7 +1202,7 @@ void RSQRT_S() { // operand pairs over two probes, each pair run as sqrt.s, rsqrt.s and // div.s, and rsqrt.s equals div.s(Fs, sqrt.s(Ft)) on every row, with a // plain 24-bit single in between. See ee_fpu_divunit_console_tests.cpp. - _FdValUl_ = eeDivide( _FsValUl_, eeSqrtBits( _FtValUl_ ) ); + _SetFdVal_( eeDivide( _FsValUl_, eeSqrtBits( _FtValUl_ ) ) ); } void SQRT_S() { @@ -1148,19 +1219,19 @@ void SQRT_S() { if ( _FtValUl_ & 0x80000000 ) _ContVal_ |= FPUflagI | FPUflagSI; - _FdValUl_ = eeSqrtBits( _FtValUl_ ); + _SetFdVal_( eeSqrtBits( _FtValUl_ ) ); } void SUB_S() { - const double exact = eeToDouble( _FsValUl_ ) - eeToDouble( _FtValUl_ ); - _FdValUl_ = eeGuardedAddSub( _FsValUl_, _FtValUl_, true ); - raiseOrClearOU( exact ); + const double sum = eeGuardedSum( _FsValUl_, _FtValUl_, true ); + _SetFdVal_( eeRoundToSingle( sum, true ) ); + raiseOrClearOU( sum ); } void SUBA_S() { - const double exact = eeToDouble( _FsValUl_ ) - eeToDouble( _FtValUl_ ); - _FAValUl_ = eeGuardedAddSub( _FsValUl_, _FtValUl_, true ); - raiseOrClearOU( exact ); + const double sum = eeGuardedSum( _FsValUl_, _FtValUl_, true ); + _SetFAVal_( eeRoundToSingle( sum, true ) ); + raiseOrClearOU( sum ); } } // End Namespace COP1 @@ -1175,14 +1246,14 @@ void LWC1() { u32 addr; addr = cpuRegs.GPR.r[_Rs_].UL[0] + (s16)(cpuRegs.code & 0xffff); // force sign extension to 32bit if (addr & 0x00000003) { Console.Error( "FPU (LWC1 Opcode): Invalid Unaligned Memory Address" ); return; } // Should signal an exception? - fpuRegs.fpr[_Rt_].UL = memRead32(addr); + fpuRegs.fpr[_Rt_].SetWord( memRead32(addr) ); } void SWC1() { u32 addr; addr = cpuRegs.GPR.r[_Rs_].UL[0] + (s16)(cpuRegs.code & 0xffff); // force sign extension to 32bit if (addr & 0x00000003) { Console.Error( "FPU (SWC1 Opcode): Invalid Unaligned Memory Address" ); return; } // Should signal an exception? - memWrite32(addr, fpuRegs.fpr[_Rt_].UL); + memWrite32(addr, fpuRegs.fpr[_Rt_].Word()); } } } } diff --git a/pcsx2/GameDatabase.cpp b/pcsx2/GameDatabase.cpp index 30705543eb..ebd8ae5543 100644 --- a/pcsx2/GameDatabase.cpp +++ b/pcsx2/GameDatabase.cpp @@ -606,6 +606,7 @@ void GameDatabaseSchema::GameEntry::applyGameFixes(Pcsx2Config& config, bool app config.Cpu.Recompiler.fpuOverflow = (clampMode >= 1); config.Cpu.Recompiler.fpuExtraOverflow = (clampMode >= 2); config.Cpu.Recompiler.fpuFullMode = (clampMode >= 3); + config.Cpu.Recompiler.fpuExactMode = (clampMode >= 4); } else Console.Warning("GameDB: Skipping changing EE/FPU clamp mode [mode=%d]", clampMode); diff --git a/pcsx2/GameDatabase.h b/pcsx2/GameDatabase.h index 3c80e1178d..b1038e27b2 100644 --- a/pcsx2/GameDatabase.h +++ b/pcsx2/GameDatabase.h @@ -38,6 +38,7 @@ namespace GameDatabaseSchema Normal, Extra, Full, + Exact, // eeClampMode only; the VUs stop at Extra+Preserve Sign. Count }; diff --git a/pcsx2/ImGui/FullscreenUI_Settings.cpp b/pcsx2/ImGui/FullscreenUI_Settings.cpp index c92c1710ad..9993ceef3d 100644 --- a/pcsx2/ImGui/FullscreenUI_Settings.cpp +++ b/pcsx2/ImGui/FullscreenUI_Settings.cpp @@ -2678,6 +2678,10 @@ void FullscreenUI::DrawClampingModeSetting(SettingsInterface* bsi, const char* t std::optional default_false = IsEditingGameSettings(bsi) ? std::nullopt : std::optional(false); std::optional default_true = IsEditingGameSettings(bsi) ? std::nullopt : std::optional(true); + // eeClampMode 4 has no VU counterpart. + std::optional fourth; + if (vunum < 0) + fourth = bsi->GetOptionalBoolValue("EmuCore/CPU/Recompiler", "fpuExactMode", default_false); std::optional third = bsi->GetOptionalBoolValue( "EmuCore/CPU/Recompiler", (vunum >= 0 ? ((vunum == 0) ? "vu0SignOverflow" : "vu1SignOverflow") : "fpuFullMode"), default_false); std::optional second = bsi->GetOptionalBoolValue("EmuCore/CPU/Recompiler", @@ -2686,7 +2690,9 @@ void FullscreenUI::DrawClampingModeSetting(SettingsInterface* bsi, const char* t "EmuCore/CPU/Recompiler", (vunum >= 0 ? ((vunum == 0) ? "vu0Overflow" : "vu1Overflow") : "fpuOverflow"), default_true); int index; - if (third.has_value() && third.value()) + if (fourth.has_value() && fourth.value()) + index = base + 4; + else if (third.has_value() && third.value()) index = base + 3; else if (second.has_value() && second.value()) index = base + 2; @@ -2703,6 +2709,7 @@ void FullscreenUI::DrawClampingModeSetting(SettingsInterface* bsi, const char* t FSUI_NSTR("Normal (Default)"), FSUI_NSTR("Extra + Preserve Sign"), FSUI_NSTR("Full"), + FSUI_NSTR("Exact"), }; static constexpr const char* vu_clamping_mode_settings[] = { FSUI_NSTR("Use Global Setting"), @@ -2712,13 +2719,14 @@ void FullscreenUI::DrawClampingModeSetting(SettingsInterface* bsi, const char* t FSUI_NSTR("Extra + Preserve Sign"), }; const char* const* options = (vunum >= 0) ? vu_clamping_mode_settings : ee_clamping_mode_settings; + const int option_count = static_cast((vunum >= 0) ? std::size(vu_clamping_mode_settings) : std::size(ee_clamping_mode_settings)); const int setting_offset = IsEditingGameSettings(bsi) ? 0 : 1; if (MenuButtonWithValue(title, summary, Host::TranslateToCString(TR_CONTEXT, options[index + setting_offset]))) { ImGuiFullscreen::ChoiceDialogOptions cd_options; - cd_options.reserve(std::size(ee_clamping_mode_settings)); - for (int i = setting_offset; i < static_cast(std::size(ee_clamping_mode_settings)); i++) + cd_options.reserve(option_count); + for (int i = setting_offset; i < option_count; i++) cd_options.emplace_back(Host::TranslateToString(TR_CONTEXT, options[i]), (i == (index + setting_offset))); OpenChoiceDialog(title, false, std::move(cd_options), [game_settings = IsEditingGameSettings(bsi), vunum](s32 index, const std::string& title, bool checked) { @@ -2726,11 +2734,12 @@ void FullscreenUI::DrawClampingModeSetting(SettingsInterface* bsi, const char* t { auto lock = Host::GetSettingsLock(); - std::optional first, second, third; + std::optional first, second, third, fourth; if (!game_settings || index > 0) { const bool base = game_settings ? 1 : 0; + fourth = (index >= (base + 4)); third = (index >= (base + 3)); second = (index >= (base + 2)); first = (index >= (base + 1)); @@ -2743,6 +2752,9 @@ void FullscreenUI::DrawClampingModeSetting(SettingsInterface* bsi, const char* t (vunum >= 0 ? ((vunum == 0) ? "vu0ExtraOverflow" : "vu1ExtraOverflow") : "fpuExtraOverflow"), second); bsi->SetOptionalBoolValue( "EmuCore/CPU/Recompiler", (vunum >= 0 ? ((vunum == 0) ? "vu0Overflow" : "vu1Overflow") : "fpuOverflow"), first); + // Same write as AdvancedSettingsWidget::setClampingMode. + if (vunum < 0) + bsi->SetOptionalBoolValue("EmuCore/CPU/Recompiler", "fpuExactMode", fourth); SetSettingsChanged(bsi); } @@ -5876,7 +5888,6 @@ void FullscreenUI::DrawGameFixesSettingsPage() FSUI_CSTR("Game fixes should not be modified unless you are aware of what each option does and the implications of doing so."), false, false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); - DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_WRENCH, "FPU Multiply Hack"), FSUI_CSTR("For Tales of Destiny."), "EmuCore/Gamefixes", "FpuMulHack", false); DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_MICROCHIP, "Use Software Renderer For FMVs"), FSUI_CSTR("Needed for some games with complex FMV rendering."), "EmuCore/Gamefixes", "SoftwareRendererFMVHack", false); DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_FORWARD_FAST, "Skip MPEG Hack"), FSUI_CSTR("Skips videos/FMVs in games to avoid game hanging/freezes."), @@ -6330,7 +6341,6 @@ TRANSLATE_NOOP("FullscreenUI", "Activating game patches can cause unpredictable TRANSLATE_NOOP("FullscreenUI", "Use patches at your own risk, the ARMSX2 team will provide no support for users who have enabled game patches."); TRANSLATE_NOOP("FullscreenUI", "Game Fixes"); TRANSLATE_NOOP("FullscreenUI", "Game fixes should not be modified unless you are aware of what each option does and the implications of doing so."); -TRANSLATE_NOOP("FullscreenUI", "For Tales of Destiny."); TRANSLATE_NOOP("FullscreenUI", "Needed for some games with complex FMV rendering."); TRANSLATE_NOOP("FullscreenUI", "Skips videos/FMVs in games to avoid game hanging/freezes."); TRANSLATE_NOOP("FullscreenUI", "To avoid TLB miss on Goemon."); @@ -6453,6 +6463,7 @@ TRANSLATE_NOOP("FullscreenUI", "2 Frames"); TRANSLATE_NOOP("FullscreenUI", "3 Frames"); TRANSLATE_NOOP("FullscreenUI", "Extra + Preserve Sign"); TRANSLATE_NOOP("FullscreenUI", "Full"); +TRANSLATE_NOOP("FullscreenUI", "Exact"); TRANSLATE_NOOP("FullscreenUI", "Extra"); TRANSLATE_NOOP("FullscreenUI", "Automatic (Default)"); TRANSLATE_NOOP("FullscreenUI", "Direct3D 11 (Legacy)"); @@ -6945,7 +6956,6 @@ TRANSLATE_NOOP("FullscreenUI", "Enable PINE"); TRANSLATE_NOOP("FullscreenUI", "PINE Slot"); TRANSLATE_NOOP("FullscreenUI", "Show Cheats For All CRCs"); TRANSLATE_NOOP("FullscreenUI", "Show Patches For All CRCs"); -TRANSLATE_NOOP("FullscreenUI", "FPU Multiply Hack"); TRANSLATE_NOOP("FullscreenUI", "Use Software Renderer For FMVs"); TRANSLATE_NOOP("FullscreenUI", "Skip MPEG Hack"); TRANSLATE_NOOP("FullscreenUI", "Preload TLB Hack"); diff --git a/pcsx2/Pcsx2Config.cpp b/pcsx2/Pcsx2Config.cpp index bb10a0b5c3..2a415298fc 100644 --- a/pcsx2/Pcsx2Config.cpp +++ b/pcsx2/Pcsx2Config.cpp @@ -473,6 +473,7 @@ Pcsx2Config::RecompilerOptions::RecompilerOptions() fpuOverflow = true; //fpuExtraOverflow = false; //fpuFullMode = false; + //fpuExactMode = false; fpuGuardedAddSub = true; // PS2-accurate add/sub guard-bit emulation; opt-out for perf on titles verified not to need it. } @@ -486,12 +487,16 @@ void Pcsx2Config::RecompilerOptions::ApplySanityCheck() if (fpuFullMode) fpuIsRight = fpuOverflow && fpuExtraOverflow; + if (fpuExactMode) + fpuIsRight = fpuOverflow && fpuExtraOverflow && fpuFullMode; + if (!fpuIsRight) { // Values are wonky; assume the defaults. fpuOverflow = RecompilerOptions().fpuOverflow; fpuExtraOverflow = RecompilerOptions().fpuExtraOverflow; fpuFullMode = RecompilerOptions().fpuFullMode; + fpuExactMode = RecompilerOptions().fpuExactMode; } bool vuIsOk = true; @@ -552,12 +557,13 @@ void Pcsx2Config::RecompilerOptions::LoadSave(SettingsWrapper& wrap) SettingsWrapBitBool(fpuOverflow); SettingsWrapBitBool(fpuExtraOverflow); SettingsWrapBitBool(fpuFullMode); + SettingsWrapBitBool(fpuExactMode); SettingsWrapBitBool(fpuGuardedAddSub); } u32 Pcsx2Config::RecompilerOptions::GetEEClampMode() const { - return fpuFullMode ? 3 : (fpuExtraOverflow ? 2 : (fpuOverflow ? 1 : 0)); + return fpuExactMode ? 4 : (fpuFullMode ? 3 : (fpuExtraOverflow ? 2 : (fpuOverflow ? 1 : 0))); } void Pcsx2Config::RecompilerOptions::SetEEClampMode(u32 value) @@ -565,6 +571,7 @@ void Pcsx2Config::RecompilerOptions::SetEEClampMode(u32 value) fpuOverflow = (value >= 1); fpuExtraOverflow = (value >= 2); fpuFullMode = (value >= 3); + fpuExactMode = (value >= 4); } u32 Pcsx2Config::RecompilerOptions::GetVUClampMode() const diff --git a/pcsx2/R5900.cpp b/pcsx2/R5900.cpp index b05488655b..2d71b07141 100644 --- a/pcsx2/R5900.cpp +++ b/pcsx2/R5900.cpp @@ -70,6 +70,7 @@ void cpuReset() cpuRegs.CP0.n.PRid = 0x00002e20; // PRevID = Revision ID, same as R5900 fpuRegs.fprc[0] = 0x00002e30; // fpu Revision.. fpuRegs.fprc[31] = 0x01000001; // fpu Status/Control + eeFprSyncSlotFormat(); cpuRegs.nextEventCycle = cpuRegs.cycle + 4; EEsCycle = 0; diff --git a/pcsx2/R5900.h b/pcsx2/R5900.h index 721e750369..3e45753f95 100644 --- a/pcsx2/R5900.h +++ b/pcsx2/R5900.h @@ -3,6 +3,8 @@ #pragma once +#include "EeFpuFormat.h" + #include "common/Pcsx2Defs.h" #include @@ -149,19 +151,37 @@ union GPR_reg64 { s8 SC[8]; }; +/* One EE FPR. The architectural register is 32 bits and the slot is 64: read + the word through Word(), write it through SetWord(). From eeClampMode 3 up a + slot holds the relocation of EeFpuFormat.h, below that the word in its low + half. +*/ union FPRreg { - float f; - u32 UL; - s32 SL; // signed 32bit used for sign extension in interpreters. + double d; + u64 UD; + + u32 Word() const { return g_eeFprSlotsRelocated ? eeFprNarrowBits(UD) : static_cast(UD); } + void SetWord(u32 word) { UD = g_eeFprSlotsRelocated ? eeFprWidenBits(word) : word; } }; struct fpuRegisters { - FPRreg fpr[32]; // 32bit floating point registers + FPRreg fpr[32]; // 32 floating point registers u32 fprc[32]; // 32bit floating point control registers - FPRreg ACC; // 32 bit accumulator + FPRreg ACC; // accumulator u32 ACCflag; // an internal accumulator overflow flag }; +/* The FPU block as savestates carry it. The format is shared with upstream + and does not move. +*/ +struct fpuRegistersWire { + u32 fpr[32]; + u32 fprc[32]; + u32 ACC; + u32 ACCflag; +}; +static_assert(sizeof(fpuRegistersWire) == 264, "the savestate FPU block is 264 bytes"); + union PageMask_t { struct @@ -316,6 +336,29 @@ extern cachedTlbs_t cachedTlbs; static cpuRegisters& cpuRegs = _cpuRegistersPack.cpuRegs; static fpuRegisters& fpuRegs = _cpuRegistersPack.fpuRegs; +static __fi void fpuRegsToWire(fpuRegistersWire& wire) +{ + for (int i = 0; i < 32; i++) + { + wire.fpr[i] = fpuRegs.fpr[i].Word(); + wire.fprc[i] = fpuRegs.fprc[i]; + } + wire.ACC = fpuRegs.ACC.Word(); + wire.ACCflag = fpuRegs.ACCflag; +} + +static __fi void fpuRegsFromWire(const fpuRegistersWire& wire) +{ + for (int i = 0; i < 32; i++) + { + // Whole slot, so no upper half survives from the previous state. + fpuRegs.fpr[i].SetWord(wire.fpr[i]); + fpuRegs.fprc[i] = wire.fprc[i]; + } + fpuRegs.ACC.SetWord(wire.ACC); + fpuRegs.ACCflag = wire.ACCflag; +} + extern bool eeEventTestIsActive; void intUpdateCPUCycles(); diff --git a/pcsx2/R5900OpcodeTables.h b/pcsx2/R5900OpcodeTables.h index 7816a3fc0f..968cd0abc1 100644 --- a/pcsx2/R5900OpcodeTables.h +++ b/pcsx2/R5900OpcodeTables.h @@ -786,6 +786,23 @@ namespace R5900 void C_LT(); void C_LE(); void CVT_S(); + + /* The EE multiplier's one-ULP deficit where the exact product has a + tail smaller than the 2^15 the array loses: the borrow still + crosses, and only reconstructing the truncated low columns says + whether it did. Mode 4's multiply guards for that band and calls + this out of line; the interpreter reaches the same code inline. + FPU.cpp, next to the array it consults. */ + bool eeMulOneUlpLow(u32 fs, u32 ft); + + /* The EE's divide/square-root unit: a digit recurrence that no + rounding mode makes an Fdiv or an Fsqrt agree with. Mode 4's DIV.S, + SQRT.S and RSQRT.S call these out of line; the interpreter reaches + the same code inline. RSQRT.S is the composition of the two, with an + ordinary single in between. FPU.cpp, under the block that states the + model. */ + u32 eeDivide(u32 fs, u32 ft); + u32 eeSqrtBits(u32 ft); } } } } // End namespace R5900 diff --git a/pcsx2/RDebug/deci2_dbgp.cpp b/pcsx2/RDebug/deci2_dbgp.cpp index 2e1a662465..0965c0a7df 100644 --- a/pcsx2/RDebug/deci2_dbgp.cpp +++ b/pcsx2/RDebug/deci2_dbgp.cpp @@ -151,7 +151,7 @@ void D2_DBGP(const u8 *inbuffer, u8 *outbuffer, char *message, char *eepc, char break; case 3:break;//performance counter 32x3 case 4:break;//hw debug reg 32x8 - case 5:memcpy(eregs[i].value, &fpuRegs.fpr[eregs[i].number], 4);break; + case 5:{const u32 w=fpuRegs.fpr[eregs[i].number].Word();memcpy(eregs[i].value, &w, 4);}break; case 6:memcpy(eregs[i].value, &fpuRegs.fprc[eregs[i].number], 4);break; case 7:memcpy(eregs[i].value, &VU0.VF[eregs[i].number], 16);break; case 8:memcpy(eregs[i].value, &VU0.VI[eregs[i].number], 4);break; @@ -201,7 +201,7 @@ void D2_DBGP(const u8 *inbuffer, u8 *outbuffer, char *message, char *eepc, char break; case 3:break;//performance counter 32x3 case 4:break;//hw debug reg 32x8 - case 5:memcpy(&fpuRegs.fpr[eregs[i].number], eregs[i].value, 4);break; + case 5:{u32 w;memcpy(&w, eregs[i].value, 4);fpuRegs.fpr[eregs[i].number].SetWord(w);}break; case 6:memcpy(&fpuRegs.fprc[eregs[i].number], eregs[i].value, 4);break; case 7:memcpy(&VU0.VF[eregs[i].number], eregs[i].value, 16);break; case 8:memcpy(&VU0.VI[eregs[i].number], eregs[i].value, 4);break; diff --git a/pcsx2/SaveState.cpp b/pcsx2/SaveState.cpp index 8698306de8..3e60774f51 100644 --- a/pcsx2/SaveState.cpp +++ b/pcsx2/SaveState.cpp @@ -187,7 +187,14 @@ bool SaveStateBase::FreezeInternals(Error* error) Freeze(cpuRegs); // cpu regs + COP0 Freeze(psxRegs); // iop regs - Freeze(fpuRegs); + + fpuRegistersWire fpu_wire; + if (IsSaving()) + fpuRegsToWire(fpu_wire); + Freeze(fpu_wire); + if (IsLoading()) + fpuRegsFromWire(fpu_wire); + Freeze(tlb); // tlbs Freeze(cachedTlbs); // cached tlbs Freeze(AllowParams1); //OSDConfig written (Fast Boot) diff --git a/pcsx2/SaveStateLegacy.cpp b/pcsx2/SaveStateLegacy.cpp index 8b2c6e9194..504e2350b5 100644 --- a/pcsx2/SaveStateLegacy.cpp +++ b/pcsx2/SaveStateLegacy.cpp @@ -445,7 +445,6 @@ namespace OldState // function shared with the current format. They are era-invariant today; // if one ever changes upstream, the legacy blob desyncs mid-stream, so // pin them here to fail the build instead of the load. - static_assert(sizeof(fpuRegisters) == 264, "fpuRegs block is 264 bytes in every era"); static_assert(sizeof(VECTOR) == 16 && sizeof(REG_VI) == 16, "VU register views are 128-bit in every era"); static_assert(sizeof(ipu_fifo) == 288 && sizeof(g_BP) == 48 && sizeof(decoder) == 3028 && sizeof(ipu_cmd) == 32, "IPU block payload is era-invariant"); @@ -473,7 +472,7 @@ namespace // normalized (version-independent) view of the legacy data OldState::cpuRegisters_9A34 cpu; // 9A2C loads leave the tail fields unset OldState::psxRegisters_9A34 psx; - fpuRegisters fpu; + fpuRegistersWire fpu; OldState::tlbs_9A2C tlb[48]; bool allowParams1, allowParams2; u32 elfCRC; @@ -652,7 +651,7 @@ bool SaveStateBase::FreezeInternalsLegacy(Error* error) psxRegs.sCycle[i] = widen_iop(st.psx.sCycle[i]); std::memcpy(psxRegs.eCycle, st.psx.eCycle, sizeof(psxRegs.eCycle)); - fpuRegs = st.fpu; + fpuRegsFromWire(st.fpu); for (int i = 0; i < 48; i++) { diff --git a/pcsx2/VMManager.cpp b/pcsx2/VMManager.cpp index ca5e25b338..b8458524ce 100644 --- a/pcsx2/VMManager.cpp +++ b/pcsx2/VMManager.cpp @@ -3200,6 +3200,10 @@ void VMManager::CheckForCPUConfigChanges(const Pcsx2Config& old_config) Console.WriteLn("Updating CPU configuration..."); FPControlRegister::SetCurrent(EmuConfig.Cpu.FPUFPCR); + // Before the code cache is thrown away below, so no emitted code and the + // file it works on can disagree about what a slot holds. + eeFprSyncSlotFormat(); + // The VU program cache toggle (EnableVUProgramCache) is picked up by the // mVUreset that ClearCPUExecutionCaches triggers below — recording and the // disk cache are re-synced there from the live config, so no explicit sync diff --git a/pcsx2/arm64/AsmHelpers.h b/pcsx2/arm64/AsmHelpers.h index cf4045e096..cba28964ea 100644 --- a/pcsx2/arm64/AsmHelpers.h +++ b/pcsx2/arm64/AsmHelpers.h @@ -233,6 +233,83 @@ __fi static void armEmitPackSignZeroBits(const vixl::aarch64::Register& dst, armAsm->Fmov(dst, vixl::aarch64::VRegister(vZero.GetCode(), 32)); } +// The EE FPR word <-> stored double relocation of EeFpuFormat.h, emitted. +// +// Sign-extending the word puts its sign bit at 63 but also fills 62..60; this +// clears those three. 61 contiguous ones under rotation, so a logical immediate. +static constexpr u64 kEeFprWidenMask = 0x8FFFFFFFFFFFFFFFULL; + +// dst = widen(word). `tmp` is an X scratch and may be `word`'s X form. Only +// the low 32 bits of `word` are read. +__fi static void armEmitEeFprWiden(const vixl::aarch64::VRegister& dst, + const vixl::aarch64::Register& word, const vixl::aarch64::Register& tmp) +{ + armAsm->Sbfiz(tmp.X(), word.X(), 29, 32); + armAsm->And(tmp.X(), tmp.X(), static_cast(kEeFprWidenMask)); + armAsm->Fmov(dst.D(), tmp.X()); +} + +// dst = widen(*src). Ldrsw does the sign extension the widen needs anyway. +__fi static void armEmitEeFprWidenFromMem(const vixl::aarch64::VRegister& dst, + const vixl::aarch64::MemOperand& src, const vixl::aarch64::Register& tmp) +{ + armAsm->Ldrsw(tmp.X(), src); + armAsm->Lsl(tmp.X(), tmp.X(), 29); + armAsm->And(tmp.X(), tmp.X(), static_cast(kEeFprWidenMask)); + armAsm->Fmov(dst.D(), tmp.X()); +} + +// dst = narrow(src), zero-extended: dst.W() is the word and dst.X() is that +// word with the top half clear. `tmp` is an X scratch and must not be `dst`. +__fi static void armEmitEeFprNarrow(const vixl::aarch64::Register& dst, + const vixl::aarch64::VRegister& src, const vixl::aarch64::Register& tmp) +{ + armAsm->Fmov(tmp.X(), src.D()); + armAsm->Lsr(dst.X(), tmp.X(), 32); + armAsm->Bfxil(dst.X(), tmp.X(), 29, 31); +} + +// *dst = narrow(src). `word` receives the narrowed word; neither scratch may +// be the other. +__fi static void armEmitEeFprNarrowToMem(const vixl::aarch64::MemOperand& dst, + const vixl::aarch64::VRegister& src, const vixl::aarch64::Register& word, + const vixl::aarch64::Register& tmp) +{ + armEmitEeFprNarrow(word, src, tmp); + armAsm->Str(word.W(), dst); +} + +// The same relocation against an fpuRegs slot: memory holds the stored double +// and a register holds the architectural word. + +// dst = the word in *slot, zero-extended. `tmp` is an X scratch, not `dst`. +__fi static void armEmitEeFprLoadSlotWord(const vixl::aarch64::Register& dst, + const vixl::aarch64::MemOperand& slot, const vixl::aarch64::Register& tmp) +{ + armAsm->Ldr(tmp.X(), slot); + armAsm->Lsr(dst.X(), tmp.X(), 32); + armAsm->Bfxil(dst.X(), tmp.X(), 29, 31); +} + +// *slot = `word`. `tmp` is an X scratch and may be `word`'s X form. +__fi static void armEmitEeFprStoreSlotWord(const vixl::aarch64::MemOperand& slot, + const vixl::aarch64::Register& word, const vixl::aarch64::Register& tmp) +{ + armAsm->Sbfiz(tmp.X(), word.X(), 29, 32); + armAsm->And(tmp.X(), tmp.X(), static_cast(kEeFprWidenMask)); + armAsm->Str(tmp.X(), slot); +} + +// Bridge for emitters holding a result as the architectural single in an S +// register while the slot they store it to holds the stored double: iFPUd's +// bodies, the fast path's SQRT, and LWC1's fastmem load. Clobbers `tmp`. +__fi static void armEmitEeFprFromS(const vixl::aarch64::VRegister& slot, + const vixl::aarch64::VRegister& src, const vixl::aarch64::Register& tmp) +{ + armAsm->Fmov(tmp.W(), src.S()); + armEmitEeFprWiden(slot, tmp, tmp); +} + // may clobber RSCRATCH/RSCRATCH2. they shouldn't be inputs. void armEmitVTBL(const vixl::aarch64::VRegister& dst, const vixl::aarch64::VRegister& src1, const vixl::aarch64::VRegister& src2, const vixl::aarch64::VRegister& tbl); diff --git a/pcsx2/arm64/iCOP2-arm64.cpp b/pcsx2/arm64/iCOP2-arm64.cpp index 2f3ec44673..4177bfef14 100644 --- a/pcsx2/arm64/iCOP2-arm64.cpp +++ b/pcsx2/arm64/iCOP2-arm64.cpp @@ -1911,6 +1911,7 @@ void recCOP2_VMADD() endMacroOp_arm64(0x110); } +// mVU_MSUB's clamp set is cFs when isCOP2. void recCOP2_VMSUB() { if (_Fd_cop2 == 0 && _XYZW_cop2 == 0) return; @@ -1918,8 +1919,9 @@ void recCOP2_VMSUB() const a64::VRegister fs = cop2GetVF(_Fs_cop2); const a64::VRegister ft = cop2GetVF(_Ft_cop2); + cop2ClampInto(RQSCRATCH2, fs); const a64::VRegister rd = cop2ResultReg(_Fd_cop2, _XYZW_cop2); - armAsm->Fmul(rd.V4S(), fs.V4S(), ft.V4S()); + armAsm->Fmul(rd.V4S(), RQSCRATCH2.V4S(), ft.V4S()); const a64::VRegister acc = cop2GetACC(); armAsm->Fsub(rd.V4S(), acc.V4S(), rd.V4S()); cop2ClampResultReg(rd); @@ -1936,8 +1938,8 @@ void recCOP2_VMSUB() // a zero broadcast Ft must become FLT_MAX*0 = 0 rather than Inf*0 = NaN folded // to +/-FLT_MAX by the result clamp. MSUBx/y/z/w use mVU_FMACd (clampType=0, // no cFs) — that Fs divergence is shared/by-design, so MSUB keeps clampFs=false. -// The MADDw extras (cACC|cFt) are a separate concern. -#define COP2_MADD_BC(name, addOp, bc, clampFs) \ +// MADDw's set is cACC|cFt|cFs when isCOP2; the extra two ride the scratch pair. +#define COP2_MADD_BC(name, addOp, bc, clampFs, clampFtAcc) \ void recCOP2_V##name() \ { \ if (_Fd_cop2 == 0 && _XYZW_cop2 == 0) return; \ @@ -1950,25 +1952,33 @@ void recCOP2_VMSUB() mulA = RQSCRATCH; \ } \ cop2LoadBroadcast(RQSCRATCH2, _Ft_cop2, bc); \ + if (clampFtAcc) \ + cop2ClampResultReg(RQSCRATCH2); /* in-place operand clamp */ \ const a64::VRegister rd = cop2ResultReg(_Fd_cop2, _XYZW_cop2); \ armAsm->Fmul(rd.V4S(), mulA.V4S(), RQSCRATCH2.V4S()); \ const a64::VRegister acc = cop2GetACC(); \ - armAsm->addOp(rd.V4S(), acc.V4S(), rd.V4S()); \ + if (clampFtAcc) \ + { \ + cop2ClampInto(RQSCRATCH2, acc); \ + armAsm->addOp(rd.V4S(), RQSCRATCH2.V4S(), rd.V4S()); \ + } \ + else \ + armAsm->addOp(rd.V4S(), acc.V4S(), rd.V4S()); \ cop2ClampResultReg(rd); \ cop2EmitFlagUpdate(_XYZW_cop2, rd); \ cop2ApplyDestMaskExplicit(_Fd_cop2, _XYZW_cop2, rd); \ endMacroOp_arm64(0x110); \ } -COP2_MADD_BC(MADDx, Fadd, 0, true) -COP2_MADD_BC(MADDy, Fadd, 1, true) -COP2_MADD_BC(MADDz, Fadd, 2, true) -COP2_MADD_BC(MADDw, Fadd, 3, true) +COP2_MADD_BC(MADDx, Fadd, 0, true, false) +COP2_MADD_BC(MADDy, Fadd, 1, true, false) +COP2_MADD_BC(MADDz, Fadd, 2, true, false) +COP2_MADD_BC(MADDw, Fadd, 3, true, true) -COP2_MADD_BC(MSUBx, Fsub, 0, false) -COP2_MADD_BC(MSUBy, Fsub, 1, false) -COP2_MADD_BC(MSUBz, Fsub, 2, false) -COP2_MADD_BC(MSUBw, Fsub, 3, false) +COP2_MADD_BC(MSUBx, Fsub, 0, false, false) +COP2_MADD_BC(MSUBy, Fsub, 1, false, false) +COP2_MADD_BC(MSUBz, Fsub, 2, false, false) +COP2_MADD_BC(MSUBw, Fsub, 3, false, false) // MADDq/MSUBq — broadcast Q #define COP2_MADD_Q(name, addOp) \ diff --git a/pcsx2/arm64/iCore-arm64.cpp b/pcsx2/arm64/iCore-arm64.cpp index aa584dfb5b..4d8384e1a6 100644 --- a/pcsx2/arm64/iCore-arm64.cpp +++ b/pcsx2/arm64/iCore-arm64.cpp @@ -711,13 +711,13 @@ static constexpr u32 NEON_RESERVED_FPU_MIN = 9; // (The callee-saved allocator range q10-q15 is declared in iCore-arm64.h — // NEON_CALLEE_SAVED_START/END; indices 8/9 reserved above. SL-13 reserves // q25/q26 the same way for the COP2 clamp-constant broadcasts — -// NEON_RESERVED_COP2_CLAMPMAX/MIN in iCore-arm64.h. q10 is reserved the same -// way again for the mode-3 multiplier-defect mask — -// NEON_RESERVED_FPU_MULMASK, also iCore-arm64.h, which carries the contract.) +// NEON_RESERVED_COP2_CLAMPMAX/MIN in iCore-arm64.h. q11 is reserved the same +// way again for the EE FPU's unscale constant — NEON_RESERVED_EEFPU_UNSCALE, +// also iCore-arm64.h, which carries the contract.) static bool _isReservedNEONreg(u32 i) { return i == NEON_RESERVED_FPU_MAX || i == NEON_RESERVED_FPU_MIN || - i == NEON_RESERVED_FPU_MULMASK || + i == NEON_RESERVED_EEFPU_UNSCALE || i == NEON_RESERVED_COP2_CLAMPMAX || i == NEON_RESERVED_COP2_CLAMPMIN; } @@ -906,7 +906,7 @@ int _allocFPtoNEONreg(int fpreg, int mode) if (mode & MODE_READ) { - armLoadEERegPtrRaw(armSRegister(neonreg), &fpuRegs.fpr[fpreg].f); + armAsm->Ldr(armEeFprSlotReg(neonreg), armCpuRegMem(&fpuRegs.fpr[fpreg])); } return neonreg; @@ -1062,7 +1062,7 @@ int _allocFPACCtoNEONreg(int mode) if (mode & MODE_READ) { - armLoadEERegPtrRaw(armSRegister(neonreg), &fpuRegs.ACC.f); + armAsm->Ldr(armEeFprSlotReg(neonreg), armCpuRegMem(&fpuRegs.ACC)); } return neonreg; @@ -1123,13 +1123,13 @@ void _writebackNEONreg(int neonreg) case NEONTYPE_FPREG: { - armStoreEERegPtrRaw(armSRegister(neonreg), &fpuRegs.fpr[arm64neon[neonreg].reg].f); + armAsm->Str(armEeFprSlotReg(neonreg), armCpuRegMem(&fpuRegs.fpr[arm64neon[neonreg].reg])); } break; case NEONTYPE_FPACC: { - armStoreEERegPtrRaw(armSRegister(neonreg), &fpuRegs.ACC.f); + armAsm->Str(armEeFprSlotReg(neonreg), armCpuRegMem(&fpuRegs.ACC)); } break; @@ -1214,8 +1214,54 @@ void _addNeededGPRtoNEONreg(int gprreg) } } +#ifdef PCSX2_RECOMPILER_TESTS +// High-water marks of the needed set, sampled per op at the clear below, where +// an op's allocations are all made and none released. The GPRREG count is the +// demand on the callee-saved range, which _allocGPRtoNEONreg draws from alone. +// Read by EeFuzz.CalleeSavedNeonBudget. +static u32 s_neonGprNeededPeak = 0; +static u32 s_neonRangeNeededPeak = 0; + +static void _sampleNeonNeededPeak() +{ + u32 gpr = 0, range = 0; + for (u32 i = 0; i < static_cast(NUM_ARM_NEON_REGS); i++) + { + if (!arm64neon[i].inuse || !arm64neon[i].needed) + continue; + if (arm64neon[i].type == NEONTYPE_GPRREG) + gpr++; + if (i >= NEON_CALLEE_SAVED_START && i < NEON_CALLEE_SAVED_END && !_isReservedNEONreg(i)) + range++; + } + if (gpr > s_neonGprNeededPeak) + s_neonGprNeededPeak = gpr; + if (range > s_neonRangeNeededPeak) + s_neonRangeNeededPeak = range; +} + +void eeTestResetNeonNeededPeak() +{ + s_neonGprNeededPeak = 0; + s_neonRangeNeededPeak = 0; +} +u32 eeTestNeonGprNeededPeak() { return s_neonGprNeededPeak; } +u32 eeTestNeonRangeNeededPeak() { return s_neonRangeNeededPeak; } + +u32 eeTestNeonCalleeSavedSlots() +{ + u32 n = 0; + for (u32 i = NEON_CALLEE_SAVED_START; i < NEON_CALLEE_SAVED_END; i++) + n += _isReservedNEONreg(i) ? 0 : 1; + return n; +} +#endif + void _clearNeededNEONregs() { +#ifdef PCSX2_RECOMPILER_TESTS + _sampleNeonNeededPeak(); +#endif for (int i = 0; i < NUM_ARM_NEON_REGS; i++) { if (arm64neon[i].needed && arm64neon[i].type == NEONTYPE_TEMP) diff --git a/pcsx2/arm64/iCore-arm64.h b/pcsx2/arm64/iCore-arm64.h index 71451dea5e..82f4a05f99 100644 --- a/pcsx2/arm64/iCore-arm64.h +++ b/pcsx2/arm64/iCore-arm64.h @@ -124,8 +124,8 @@ struct _arm64gprregs // Callee-saved NEON range available to the allocator: q10-q15 (q8/q9 hold // the pinned FPU clamp constants and are excluded from the pool entirely; -// q10 likewise holds the pinned multiplier-defect mask, below, so the range -// yields q11-q15 in practice). +// q11 likewise holds the EE FPU's unscale constant, below, so the range yields +// q10 and q12-q15 in practice). // AAPCS64 preserves only the LOWER 64 bits of v8-v15 across C calls, so // full-128-bit classes (NEONTYPE_GPRREG quads, VFREG) can never be retained // across a seam — but 32-bit FPR-class slots (FPREG/FPACC, lane 0 only) can @@ -133,27 +133,26 @@ struct _arm64gprregs static constexpr u32 NEON_CALLEE_SAVED_START = 10; static constexpr u32 NEON_CALLEE_SAVED_END = 16; // exclusive -// d10 = 0x2AA, the EE multiplier's Booth-digit predicate mask, parked for the +// d11 = 2^kEeFprScaleExp (kEeFprUnscaleBits, EeFpuFormat.h), parked for the // whole JIT session by _DynGen_EnterRecompiledCode alongside s8/s9 and read by -// emitDefectiveFmul (iFPUd-arm64.cpp) on every mode-3 multiply. Same contract -// as NEON_RESERVED_FPU_MAX/MIN and for the same reason: the lower 64 bits of -// d8-d15 are callee-saved, so a parked constant needs no compile-time liveness -// tracking, no invalidation at C-call seams, and no re-materialization on any -// path — which is exactly what a predicate must not get wrong (a stale mask is -// a silent one-ULP error on a fraction of multiplies, and the 1147-case -// hardware corpus cannot see the class it decides). +// iFPUd-arm64.cpp wherever a slot becomes the value it denotes. Emitted in +// every clamp mode: the dispatcher outlives a clamp-mode change. // -// It costs one slot out of the callee-saved allocator range above. That is the -// whole price: the multiply sequence drops 6 instructions -> 4 unconditionally, -// including the first multiply in a block, where a compile-time liveness flag -// in a caller-saved register would still have paid the full 6. +// Same contract as NEON_RESERVED_FPU_MAX/MIN: the lower 64 bits of d8-d15 are +// callee-saved, so a parked constant needs no compile-time liveness tracking, +// no invalidation at C-call seams and no re-materialization. +// +// It costs one slot out of the callee-saved allocator range above, which +// _allocGPRtoNEONreg draws from alone. Three is the floor, what a 3-operand MMI +// op needs at once; the fourth keeps an FPU multiply-accumulate's Fd, Fs, Ft and +// ACC all in call-surviving homes. EeFuzz.CalleeSavedNeonBudget holds both. // // Two reasons it needs no microVU pool gate (unlike SL-13's q25/q26, and for // the same reasons q8/q9 need none): micro-mode mVU runs under mVUdispatcherAB, // whose prologue Stp/Ldp-saves d8-d15; and COP2 macro-mode mVU emits inline in // EE blocks but is structurally bounded to NEON slots 0-3 by // kMacroVFEvictHighWater, which mVUmacroEmitEpilogue asserts on every macro op. -static constexpr u32 NEON_RESERVED_FPU_MULMASK = 10; +static constexpr u32 NEON_RESERVED_EEFPU_UNSCALE = 11; // SL-13: q25/q26 are dedicated to the COP2 macro clamp-constant broadcasts // (q25 = maxFloat.4S = +FLT_MAX, q26 = minFloat.4S = -FLT_MAX) and excluded diff --git a/pcsx2/arm64/iFPU-arm64.cpp b/pcsx2/arm64/iFPU-arm64.cpp index 680d66fd00..80792433df 100644 --- a/pcsx2/arm64/iFPU-arm64.cpp +++ b/pcsx2/arm64/iFPU-arm64.cpp @@ -149,11 +149,11 @@ void recMFC1() const int fsreg = _checkNEONreg(NEONTYPE_FPREG, _Fs_, MODE_READ); if (fsreg >= 0) { - armAsm->Fmov(RWSCRATCH, armSRegister(fsreg)); + armEmitEeFprWordFromSlot(RWSCRATCH, armDRegister(fsreg), a64::x9); } else { - armLoadEERegPtr(RWSCRATCH, &fpuRegs.fpr[_Fs_].UL); + armEmitEeFprWordFromSlotMem(RWSCRATCH, armCpuRegMem(&fpuRegs.fpr[_Fs_]), a64::x9); } armAsm->Sxtw(RXSCRATCH, RWSCRATCH); // Deposit last, after the FPR-slot probe above (see recCFC1). @@ -176,9 +176,9 @@ void recMTC1() // If fpr[fs] is already resident in NEON, write the new bits straight into // the host reg and mark it dirty (MODE_WRITE), keeping it hot for a - // following FPU op; the block epilogue flushes the S-reg to fpr[fs].f. + // following FPU op; the block epilogue flushes the host reg to fpr[fs]. // MTC1 overwrites fpr[fs] wholesale, so any prior MODE_WRITE-only value - // in the slot is dead and correctly discarded by overwriting lane 0. + // in the slot is dead and correctly discarded by overwriting it. // GE-11: when fs is NOT resident but the backprop analysis says it is // used later in the block, ALLOCATE the destination slot (write-only, no // memory load) — this is the previously-dead _allocIfUsedFPUtoNEON, the @@ -189,11 +189,11 @@ void recMTC1() fsreg = _allocIfUsedFPUtoNEON(_Fs_, MODE_WRITE); if (fsreg >= 0) { - armAsm->Fmov(armSRegister(fsreg), rt); + armEmitEeFprSlotFromWord(armDRegister(fsreg), rt, RXSCRATCH); } else { - armStoreEERegPtr(rt, &fpuRegs.fpr[_Fs_].UL); + armEmitEeFprSlotMemFromWord(armCpuRegMem(&fpuRegs.fpr[_Fs_]), rt, RXSCRATCH); } } @@ -469,9 +469,9 @@ static a64::VRegister fpuClampInput(const a64::VRegister& src, const a64::VRegis // FPU_SUB, iFPU.cpp) — ON by default (games like True Crime NYC and Jak 3 // misrender without it, and per-game flagging proved impractical) but opt-out // globally for EE-heavy titles that don't need it; see the early-out below. It -// reproduces the masking already present in the DOUBLE path's FPU_ADD_SUB -// (iFPUd-arm64.cpp:200); the CHECK_FPU_FULL (double) config dispatches to that -// path instead and never reaches here (Full mode guards unconditionally). +// reproduces the masking already present in the DOUBLE path's FPU_ADD_SUB_D +// (iFPUd-arm64.cpp:200); eeClampMode 3 and up dispatch to that path instead and +// never reach here, and it guards unconditionally. // // When |expd - expt| <= 1 the mask clears zero bits, so that (common) case skips // straight to the plain op. Only |diff| >= 2 masks the smaller-exponent operand; @@ -619,49 +619,13 @@ static void fpuEmitGuardedAddSub(const a64::VRegister& dst, _freeNEONreg(tmp); } -// FpuMulHack (Tales of Destiny Remake gamefix, EmuConfig.Gamefixes.FpuMulHack). -// x86 routes every FPU multiply (MUL/MULA/MADD/MSUB) through FPU_MUL, which — -// when the gamefix is on — patches the single specific product 0.25 * π -// (0x3e800000 * 0x40490fdb) from the correctly-rounded 0x3f490fdb to 0x3f490fda -// so the game stops hanging in one late-game room. Emit -// `dst = (hit) ? 0x3f490fda : s*t`; callers clamp/accumulate dst as they normally -// would (the magic value is an ordinary small float, so a following -// fpuClampResult is a no-op). In the default config (gamefix off) this is a bare -// Fmul — zero added cost. -// -// The patched value is not arbitrary: 0x3f490fda is π/4 one ULP low, which is -// what the EE's multiplier actually returns. Its Booth recoding drops one ULP -// when ft's significand has an odd digit pair (ft & 0x2AA) and the exact product -// has no tail below the single ULP — here fs = 2^-2, so the product is exact and -// the deficit reaches the result. The general model reproduces this pair (and -// leaves the swapped operand order alone, exactly as the check below does). -// It is NOT generalized here: in this fast path it costs ~9 instructions on every -// multiply in every game, against 1 today. Its home is iFPUd-arm64.cpp, where the -// double product already exists and it costs 4 — extending it to this path needs -// its own measured case. +// The EE multiplier's one-ULP deficit is not modelled here: it needs the exact +// product's tail below the single's ULP, which a single-precision multiply has +// already discarded. It lives at emitDefectiveFmul (iFPUd-arm64.cpp), where the +// product is a double and the tail is 29 bits of it. static void emitFpuMul(const a64::VRegister& dst, const a64::VRegister& s, const a64::VRegister& t) { - if (!CHECK_FPUMULHACK) - { - armAsm->Fmul(dst, s, t); - return; - } - - a64::Label noHack, done; - armAsm->Fmov(RWARG1, s); - armAsm->Fmov(RWARG2, t); - armAsm->Mov(RWSCRATCH, 0x3e800000); - armAsm->Cmp(RWARG1, RWSCRATCH); - armAsm->B(&noHack, a64::ne); - armAsm->Mov(RWSCRATCH, 0x40490fdb); - armAsm->Cmp(RWARG2, RWSCRATCH); - armAsm->B(&noHack, a64::ne); - armAsm->Mov(RWSCRATCH, 0x3f490fda); - armAsm->Fmov(dst, RWSCRATCH); - armAsm->B(&done); - armAsm->Bind(&noHack); armAsm->Fmul(dst, s, t); - armAsm->Bind(&done); } //------------------------------------------------------------------ @@ -669,7 +633,8 @@ static void emitFpuMul(const a64::VRegister& dst, const a64::VRegister& s, const //------------------------------------------------------------------ // "Full" / DOUBLE-precision emitters (iFPUd-arm64.cpp), selected per-op when -// CHECK_FPU_FULL (GameDB eeClampMode:3). Default config uses the fast paths. +// CHECK_FPU_FULL (GameDB eeClampMode 3 and up). Default config uses the fast +// paths. // MOV.S is a raw bit-copy in BOTH modes (x86 DOUBLE::recMOV_S_xmm == the fast // body), so it has no DOUBLE selection. namespace DOUBLE { @@ -703,7 +668,7 @@ static void recMOV_S_xmm(int info) // fs==fd): the allocator hands back EEREC_D==EEREC_S and the Fmov would be // an identity self-move. if (EEREC_D != EEREC_S) - armAsm->Fmov(armSRegister(EEREC_D), armSRegister(EEREC_S)); + armAsm->Fmov(armEeFprSlotReg(EEREC_D), armEeFprSlotReg(EEREC_S)); } void recMOV_S() @@ -796,12 +761,12 @@ static void recCompareFPRs(a64::Condition cond) if (fsreg >= 0) armAsm->Fmov(RSSCRATCH, armSRegister(fsreg)); else - armLoadEERegPtr(RSSCRATCH, &fpuRegs.fpr[_Fs_].f); + armLoadEERegPtr(RSSCRATCH, &fpuRegs.fpr[_Fs_]); const int ftreg = (_Ft_ == _Fs_) ? fsreg : _checkNEONreg(NEONTYPE_FPREG, _Ft_, 0); if (ftreg >= 0) armAsm->Fmov(RSSCRATCH2, armSRegister(ftreg)); else - armLoadEERegPtr(RSSCRATCH2, &fpuRegs.fpr[_Ft_].f); + armLoadEERegPtr(RSSCRATCH2, &fpuRegs.fpr[_Ft_]); fpuClampCompareOperand(RSSCRATCH); fpuClampCompareOperand(RSSCRATCH2); armAsm->Fcmp(RSSCRATCH, RSSCRATCH2); @@ -1495,9 +1460,18 @@ void recMSUBA_S() // CVT.S: fd = (float)int_bits_of(fpr[fs]) // Single NEON-scalar SCVTF Sd,Sn — the int32 bits are already in the V file; // the old Fmov-to-GPR bounce cost an extra insn + cross-file hazard (GE-02). +// A relocated slot puts them back out of reach, so the double tier pays the +// bounce. static void recCVT_S_xmm(int info) { - armAsm->Scvtf(armSRegister(EEREC_D), armSRegister(EEREC_S)); + if (!CHECK_FPU_FULL) + { + armAsm->Scvtf(armSRegister(EEREC_D), armSRegister(EEREC_S)); + return; + } + armEmitEeFprNarrow(RWSCRATCH, armDRegister(EEREC_S), a64::x9); + armAsm->Scvtf(RSSCRATCH, RWSCRATCH); + armEmitEeFprFromS(armDRegister(EEREC_D), RSSCRATCH, RXSCRATCH); } void recCVT_S() @@ -1512,8 +1486,20 @@ void recCVT_S() // is NaN: ARM Fcvtzs yields 0, but the PS2 (interp CVT_W, FPU.cpp) saturates // NaN by sign — positive NaN → 0x7fffffff, negative NaN → 0x80000000. Fix up // the NaN case only (cold branch over the source-sign select). +// +// A relocated slot holds no NaN, so the double tier unscales into the value and +// converts, and Fcvtzs's own saturation covers it. static void recCVT_W_xmm(int info) { + if (CHECK_FPU_FULL) + { + armAsm->Fmul(RDSCRATCH, armDRegister(EEREC_S), + a64::VRegister(NEON_RESERVED_EEFPU_UNSCALE, 64)); + armAsm->Fcvtzs(RWSCRATCH, RDSCRATCH); + armEmitEeFprWiden(armDRegister(EEREC_D), RWSCRATCH, RXSCRATCH); + return; + } + const a64::VRegister fs = armSRegister(EEREC_S); armAsm->Fcvtzs(RWSCRATCH, fs); a64::Label done; diff --git a/pcsx2/arm64/iFPUd-arm64.cpp b/pcsx2/arm64/iFPUd-arm64.cpp index 3836b7b136..f75190a0f8 100644 --- a/pcsx2/arm64/iFPUd-arm64.cpp +++ b/pcsx2/arm64/iFPUd-arm64.cpp @@ -6,10 +6,13 @@ // This is the arm64 port of pcsx2/x86/iFPUd.cpp: the PS2-accurate FPU that // widens each single to IEEE double, performs the op in double, then narrows // back to a PS2 single with the hardware's overflow/underflow/clamp semantics. -// It is selected only when CHECK_FPU_FULL (EmuConfig.Cpu.Recompiler.fpuFullMode, -// the GameDB `eeClampMode:3` path — FFX, Max Payne, Dark Cloud 2, Klonoa 2 …). +// It is selected when CHECK_FPU_FULL (EmuConfig.Cpu.Recompiler.fpuFullMode, the +// GameDB eeClampMode 3 and up — FFX, Max Payne, Dark Cloud 2, Klonoa 2 …). // Default config runs the single-precision fast path in iFPU-arm64.cpp. // +// It serves eeClampMode 3 and 4, which differ at emitDefectiveFmul and at +// emitDivideUnitIsland below. +// // The algorithm is translated from the x86 semantics; the codegen follows the // iFPU-arm64.cpp idioms (scalar Fcvt, GPR bit-twiddle via Fmov, the // armLoadEERegPtr fprc[31]/ACCflag accessors). The shared interpreter @@ -41,54 +44,23 @@ namespace DOUBLE { #define FPUflagSI 0x00000040 #define FPUflagSD 0x00000020 -// ---- PS2 single -> IEEE double -------------------------------------------- +// ---- The guest FPR file ----------------------------------------------------- // -// A PS2 single with exponent field 0xff is a *normal* large number (1.m * 2^128), -// but IEEE reads exp 0xff as Inf/NaN — so a plain cvtss2sd would corrupt it. -// For those (and only those) lower the exponent by one in the single domain, -// widen exactly, then raise the exponent by one in the double domain. Mirrors -// x86 ToDouble (xPSUB.D one_exp / xCVTSS2SD / xPADD.Q dbl_one_exp). -// -// Reads `srcidx`'s S lane, writes `dstidx`'s D lane, and never writes the -// source. The two may be the same register (that is ToDouble below). -// -// The source is allowed to be an allocator-resident guest FPR or the ACC, which -// is the point: the complex arm needs somewhere to put the exponent-lowered -// single before Fcvt, and it uses the destination's S lane — a temp the caller -// owns — instead of scribbling on the source. Every widening site used to pay a -// `copySrc` Fmov purely to make that scribble legal. -static void ToDoubleFrom(int dstidx, int srcidx) +// The file holds each word relocated into double position and scaled by +// 2^-kEeFprScaleExp (EeFpuFormat.h); this file works in words and bridges at +// the edges. Widening is one exact multiply against the pinned scale, and +// FPCR.FZ takes an EE denormal to a zero of the same sign there. `dstidx` may +// be `srcidx`. +static void SlotToDouble(int dstidx, int srcidx) { - const a64::VRegister ss = armSRegister(srcidx); - const a64::VRegister sd = armSRegister(dstidx); - const a64::VRegister dd = armDRegister(dstidx); - - a64::Label simple, done; - armAsm->Fmov(RWSCRATCH, ss); - armAsm->And(RWARG1, RWSCRATCH, 0x7f800000); - armAsm->Cmp(RWARG1, 0x7f800000); - armAsm->B(&simple, a64::ne); - - // Complex: exp field == 0xff (Inf/NaN to IEEE, finite to PS2). - armAsm->Sub(RWSCRATCH, RWSCRATCH, 0x00800000); // lower exponent by one (single) - armAsm->Fmov(sd, RWSCRATCH); - armAsm->Fcvt(dd, sd); // cvtss2sd (now finite) - armAsm->Fmov(RXSCRATCH, dd); - armAsm->Mov(RXARG1, static_cast(1) << 52); // dbl_one_exp - armAsm->Add(RXSCRATCH, RXSCRATCH, RXARG1); // raise exponent by one (double) - armAsm->Fmov(dd, RXSCRATCH); - armAsm->B(&done); - - armAsm->Bind(&simple); - armAsm->Fcvt(dd, ss); - - armAsm->Bind(&done); + armAsm->Fmul(armDRegister(dstidx), armDRegister(srcidx), + a64::VRegister(NEON_RESERVED_EEFPU_UNSCALE, 64)); } -// In-place form: widen temp NEON reg `idx` from its own S lane. -static void ToDouble(int idx) +// The other half of the bridge: an architectural single in an S lane as a slot. +static void SingleToSlot(int dstidx, int srcidx) { - ToDoubleFrom(idx, idx); + armEmitEeFprFromS(armDRegister(dstidx), armSRegister(srcidx), RXSCRATCH); } // ---- IEEE double -> PS2 single (full overflow/underflow/flag handling) ----- @@ -150,14 +122,25 @@ static void ToPS2FPU_Full(int idx, bool flags, int /*absidx*/, bool acc, bool ad // // `hi`, not `hs`: kEeFpuMax itself is representable and belongs to the // halving arm, which handles it exactly (halved it is +FLT_MAX, and - // 0x7f7fffff + 0x00800000 == 0x7fffffff). This is the same bound the - // interpreter's eeRoundToSingle saturates at (FPU.cpp). + // 0x7f7fffff + 0x00800000 == 0x7fffffff). + // + // The test is on the rounded magnitude: the adder normalises and truncates + // before anything looks at the exponent field, so a sum above kEeFpuMax can + // chop back onto it and did not saturate. kEeFpuMax + 2^104 is 2^129 - 2^104, + // which needs 25 significant bits and chops to kEeFpuMax; one exponent + // higher the sum is 2^129 and no rounding brings it back. + // + // Chopping the low 29 bits is the rounding only under round-toward-zero, + // the arithmetic FPCR. The divide unit's callers run under round-to-nearest + // and pass flags=false, which is the same split. + if (flags) + armAsm->And(RXARG1, RXARG1, UINT64_C(0xFFFFFFFFE0000000)); armAsm->Mov(RXARG2, UINT64_C(0x47FFFFFFE0000000)); // (2 - 2^-23) * 2^128 armAsm->Cmp(RXARG1, RXARG2); armAsm->B(&toOverflow, a64::hi); // Large but PS2-representable (exp-0xff range): lower double exp, narrow, - // raise single exp — the inverse of ToDouble's complex path. + // raise single exp — the inverse of the widening, in the single domain. armAsm->Mov(RXARG2, static_cast(1) << 52); armAsm->Sub(RXSCRATCH, RXSCRATCH, RXARG2); armAsm->Fmov(d, RXSCRATCH); @@ -276,6 +259,9 @@ static void ToPS2FPU_Wide(int idx) armAsm->B(&end); armAsm->Bind(&toComplex); + // Rounded magnitude, as in ToPS2FPU_Full: a product that chops back onto + // kEeFpuMax did not saturate and must not raise O. + armAsm->And(RXARG1, RXARG1, UINT64_C(0xFFFFFFFFE0000000)); armAsm->Mov(RXARG2, UINT64_C(0x47FFFFFFE0000000)); // (2 - 2^-23) * 2^128 armAsm->Cmp(RXARG1, RXARG2); armAsm->B(&chop, a64::ls); // in [2^128, kEeFpuMax]: an ordinary chop @@ -308,73 +294,13 @@ static void ToPS2FPU_Wide(int idx) // The EE FPU has no guard bits to the right of the mantissa; subtraction (and // add of mixed signs) can shift the mantissa left and expose what would have // been guard bits. This masks the low mantissa bits of the smaller operand by -// the exponent difference so they read as zero. Port of x86 FPU_ADD_SUB; both -// operands (single, in temp NEON regs `idxd`/`idxt`) are mutated in place. -static void FPU_ADD_SUB(int idxd, int idxt) -{ - const a64::VRegister sd = armSRegister(idxd); - const a64::VRegister st = armSRegister(idxt); - - armAsm->Fmov(RWARG1, sd); // d bits - armAsm->Fmov(RWARG2, st); // t bits - // GE-M2: the exponent-diff and mask temps use the reserved load/store scratch - // x9/x10, not the RWARG3/RWARG4 (w2/w3) pool hosts they replaced — w2/w3 are - // EE-allocatable, so under the residency flip they can hold a live guest GPR, - // and this hand-emitted path never flushes the allocator. This span has no - // load/store or C-call, so x9/x10 are free scratch here. (x86 uses GPR temps - // too: pcsx2/x86/iFPU.cpp FPU_ADD_SUB; only the register choice is our - // scratch-discipline constraint.) - armAsm->Ubfx(a64::w9, RWARG1, 23, 8); // expd - armAsm->Ubfx(RWSCRATCH, RWARG2, 23, 8); // expt - armAsm->Sub(a64::w9, a64::w9, RWSCRATCH); // diff = expd - expt (signed) - - a64::Label caseD25, casePos, caseEq, caseDn25, done; - armAsm->Cmp(a64::w9, 25); - armAsm->B(&caseD25, a64::ge); - armAsm->Cmp(a64::w9, 0); - armAsm->B(&casePos, a64::gt); - armAsm->B(&caseEq, a64::eq); - armAsm->Cmn(a64::w9, 25); // cmp diff, -25 - armAsm->B(&caseDn25, a64::le); - - // diff in -24..-1 (expd < expt): mask tempd's low (-diff-1) bits. - armAsm->Neg(RWSCRATCH, a64::w9); - armAsm->Sub(RWSCRATCH, RWSCRATCH, 1); - armAsm->Mov(a64::w10, 0xffffffff); - armAsm->Lsl(a64::w10, a64::w10, RWSCRATCH); - armAsm->And(RWARG1, RWARG1, a64::w10); - armAsm->Fmov(sd, RWARG1); - armAsm->B(&done); - - armAsm->Bind(&caseD25); - // diff >= 25 (expt much smaller): tempt keeps only its sign. - armAsm->And(RWARG2, RWARG2, 0x80000000); - armAsm->Fmov(st, RWARG2); - armAsm->B(&done); - - armAsm->Bind(&casePos); - // diff in 1..24 (expt smaller): mask tempt's low (diff-1) bits. - armAsm->Sub(RWSCRATCH, a64::w9, 1); - armAsm->Mov(a64::w10, 0xffffffff); - armAsm->Lsl(a64::w10, a64::w10, RWSCRATCH); - armAsm->And(RWARG2, RWARG2, a64::w10); - armAsm->Fmov(st, RWARG2); - armAsm->B(&done); - - armAsm->Bind(&caseDn25); - // diff <= -25 (expd much smaller): tempd keeps only its sign. - armAsm->And(RWARG1, RWARG1, 0x80000000); - armAsm->Fmov(sd, RWARG1); - - armAsm->Bind(&caseEq); // diff == 0: nothing - armAsm->Bind(&done); -} - -// ---- PS2 add/sub guard-bit emulation, wide form --------------------------- +// the exponent difference so they read as zero. Port of x86 FPU_ADD_SUB +// (pcsx2/x86/iFPUd.cpp), which states the law over the architectural single and +// runs before the widening. // -// Same law as FPU_ADD_SUB, for operands that are already doubles holding EE -// singles exactly (low 29 mantissa bits zero, |x| <= kEeFpuMax). Two changes, -// neither of which costs an instruction: +// Here both operands are already doubles holding EE singles exactly (low 29 +// mantissa bits zero, |x| <= kEeFpuMax) in temp NEON regs `idxd`/`idxt`, and +// are mutated in place. Two changes from the single-domain form: // // * The exponent field is bits 52..62 instead of 23..30, and the bias is 896 // higher. The bias cancels in the difference, so the case split is unchanged @@ -386,8 +312,8 @@ static void FPU_ADD_SUB(int idxd, int idxt) // domains still agree. Verified: 0 disagreements over 1,572,864 pairs // covering every (expd, expt) combination, 12,240 of them in exactly that // class, against an off-by-one liveness control that moves 5,588 of 65,025. -// (A PS2 denormal cannot reach here: ToDouble runs under FZ, which flushes -// it to a zero of the same sign -- measured on this host, not assumed.) +// (A PS2 denormal cannot reach here: SlotToDouble runs under FZ, which +// flushes it to a zero of the same sign.) // * A single's mantissa bit k is double bit k+29, so masking the single's low // (diff-1) bits is masking the double's low (diff-1)+29. The extra 29 are // already zero, so only the shift amount changes: `diff - 1` -> `diff + 28`. @@ -398,8 +324,12 @@ static void FPU_ADD_SUB_D(int idxd, int idxt) armAsm->Fmov(RXARG1, dd); // d bits armAsm->Fmov(RXARG2, dt); // t bits - // GE-M2: x9/x10 for the diff and mask temps, not the w2/w3 pool hosts -- see - // the note in FPU_ADD_SUB. This span has no load/store or C-call either. + // GE-M2: the exponent-diff and mask temps use the reserved load/store scratch + // x9/x10, not the RWARG3/RWARG4 (w2/w3) pool hosts they replaced — w2/w3 are + // EE-allocatable, so under the residency flip they can hold a live guest GPR, + // and this hand-emitted path never flushes the allocator. This span has no + // load/store or C-call, so x9/x10 are free scratch here. (x86 uses GPR temps + // too; only the register choice is our scratch-discipline constraint.) armAsm->Ubfx(a64::x9, RXARG1, 52, 11); // expd armAsm->Ubfx(RXSCRATCH, RXARG2, 52, 11); // expt armAsm->Sub(a64::w9, a64::w9, RWSCRATCH); // diff = expd - expt (signed) @@ -448,28 +378,15 @@ static void FPU_ADD_SUB_D(int idxd, int idxt) // ---- Op cores -------------------------------------------------------------- -// Copy an allocator-resident FP source (EEREC_S/EEREC_T) into a fresh temp so -// the emitter can mutate it without corrupting the guest fpr slot. -// -// Only the paths that mutate the operand in the single domain still need this: -// recFPUOp's FPU_ADD_SUB guard mask, and the Fabs in SQRT/RSQRT. A site that -// only widens uses ToDoubleFrom(temp, EEREC_x) instead and pays no copy. -static int copySrc(int eerec) -{ - const int idx = _allocTempNEONreg(); - armAsm->Fmov(armSRegister(idx), armSRegister(eerec)); - return idx; -} - -// ADD/SUB/ADDA/SUBA: FPU_ADD_SUB guard mask -> widen -> op in double -> narrow. +// ADD/SUB/ADDA/SUBA: widen both slots -> guard mask -> op in double -> narrow. static void recFPUOp(int info, int eeRecDst, int op /*0=add,1=sub*/, bool acc) { - const int sreg = copySrc(EEREC_S); - const int treg = copySrc(EEREC_T); + const int sreg = _allocTempNEONreg(); + const int treg = _allocTempNEONreg(); - FPU_ADD_SUB(sreg, treg); - ToDouble(sreg); - ToDouble(treg); + SlotToDouble(sreg, EEREC_S); + SlotToDouble(treg, EEREC_T); + FPU_ADD_SUB_D(sreg, treg); if (op == 0) armAsm->Fadd(armDRegister(sreg), armDRegister(sreg), armDRegister(treg)); @@ -477,12 +394,85 @@ static void recFPUOp(int info, int eeRecDst, int op /*0=add,1=sub*/, bool acc) armAsm->Fsub(armDRegister(sreg), armDRegister(sreg), armDRegister(treg)); ToPS2FPU_Full(sreg, true, treg, acc, true); - armAsm->Fmov(armSRegister(eeRecDst), armSRegister(sreg)); + SingleToSlot(eeRecDst, sreg); _freeNEONreg(sreg); _freeNEONreg(treg); } +// ---- Out-of-line calls into the interpreter's models ----------------------- +// +// The multiply array's one-ULP deficit and the divide/square-root digit +// recurrence are not host arithmetic under any rounding mode, so mode 4 calls +// the models FPU.cpp states. The callees are plain AAPCS: every caller-saved +// home the allocator is using is spilled across the call, and the EE pin +// mirrors go through their flush/reload pair. They are pure arithmetic on their +// arguments, so unlike the vtlb slow paths they need no pc/code flush and no +// cycle spill. x8 carries the result back out, being neither allocatable nor a +// pin. +struct IslandFrame +{ + u8 gprs[8]; + u8 fprs[NUM_ARM_NEON_REGS]; + u32 ngpr, nfpr, frame, spare; +}; + +// `spare` bytes above the saved registers, addressed through IslandSpare, for +// an island that has to carry a value across a call of its own. +static void emitIslandEnter(IslandFrame& f, u32 spare = 0) +{ + f.ngpr = 0; + f.nfpr = 0; + for (int i = 0; i < NUM_ARM_GPR_REGS; i++) + { + // Leaves x4-x7 and x14/x15, the caller-saved half of the EE pool. x0-x3 + // and x8-x10 are scratch, x11-x13 are pins flushed below, x16+ are + // reserved or callee-saved. + if (i >= 16 || (i >= 8 && i <= 13) || i <= 3) + continue; + if (arm64gprs[i].inuse) + f.gprs[f.ngpr++] = static_cast(i); + } + for (int i = 0; i < NUM_ARM_NEON_REGS; i++) + { + // AAPCS64 preserves only the low 64 bits of q8-q15, and the allocator + // keeps 128-bit classes there, so every live one is saved in full. + if (arm64neon[i].inuse) + f.fprs[f.nfpr++] = static_cast(i); + } + + f.spare = spare; + f.frame = (f.ngpr * 8 + f.nfpr * 16 + spare + 15u) & ~15u; + if (f.frame) + armAsm->Sub(a64::sp, a64::sp, f.frame); + u32 off = 0; + for (u32 i = 0; i < f.ngpr; i++, off += 8) + armAsm->Str(a64::XRegister(f.gprs[i]), a64::MemOperand(a64::sp, off)); + for (u32 i = 0; i < f.nfpr; i++, off += 16) + armAsm->Str(a64::QRegister(f.fprs[i]), a64::MemOperand(a64::sp, off)); + // Flush before, reload after: the pin mirrors are lazily dirty, so a reload + // on its own would lose the writes the block has made to them. Both halves + // address RSTATE, so neither disturbs the argument or result registers. + armFlushEEClobberedPins(); +} + +static a64::MemOperand IslandSpare(const IslandFrame& f) +{ + return a64::MemOperand(a64::sp, f.ngpr * 8 + f.nfpr * 16); +} + +static void emitIslandLeave(const IslandFrame& f) +{ + u32 off = 0; + for (u32 i = 0; i < f.ngpr; i++, off += 8) + armAsm->Ldr(a64::XRegister(f.gprs[i]), a64::MemOperand(a64::sp, off)); + for (u32 i = 0; i < f.nfpr; i++, off += 16) + armAsm->Ldr(a64::QRegister(f.fprs[i]), a64::MemOperand(a64::sp, off)); + if (f.frame) + armAsm->Add(a64::sp, a64::sp, f.frame); + armReloadEEClobberedPins(); +} + // ---- The EE multiplier's one-ULP deficit ----------------------------------- // // The console's multiply array does not round correctly: it comes back exactly @@ -500,42 +490,38 @@ static void recFPUOp(int info, int eeRecDst, int op /*0=add,1=sub*/, bool acc) // The interpreter models a superset (FPU.cpp eeMulRound / eeMulOneUlpLow / // eeMulArray): it reconstructs the array's truncated low half, so it also // catches the rows where the tail is non-zero but smaller than the borrow. -// This is the mode-3 codegen for the zero-tail law. FpuMulHack is a one-point -// sample of the same rule and this subsumes it, including the asymmetry -- it -// is not folded in here because iFPUd never had it. +// This is the double tier's codegen for the zero-tail law. FpuMulHack is a +// one-point sample of the same rule and this subsumes it, asymmetry included. // // The product is computed in double, where a 24x24 significand multiply is -// exact, so condition 1 needs no integer multiply and no tail extraction: -// one integer ULP of the double is strictly below one ULP of the single, so -// decrementing the double's raw bit pattern drops an exactly-representable -// product one single-step and leaves everything else inside the same single -// bucket. The rounding downstream performs the tail test -- and it is the same -// test under either of the two narrowings this file uses: ToPS2FPU_Wide's -// mask-off-the-low-29 is truncation by construction, and ToPS2FPU_Full's Fcvt -// runs under FPUFPCR, whose mode is round-toward-zero (the argument is spelled -// out at ToPS2FPU_Wide). That equivalence is structural. The FPCR is the -// dependency that actually matters: under round-to-nearest the decremented -// double would round straight back up to the product it came from and this -// would emit nothing at all. +// exact, so neither condition needs an integer multiply: the tail is the 29 +// bits below the single's ULP, and the predicate is a function of ft alone. // -// A NEON compare result is all-ones, which as a 64-bit lane is -1, so `Add` of -// the mask is the conditional decrement. Two traps, both already paid for once: +// eeClampMode 3 emits the Booth term alone; 4 adds the boundary term and the +// array call below. // -// * the Add is a 64-bit lane, so the mask must be all-ones across the full 64 -// bits. A 32-bit compare adds +0xFFFFFFFF instead of -1; that scores 98.28% -// and reads like a predicate bug when it is a mask bug. -// * the Bic is `.8b` because there is no scalar BIC. AdvSIMD BIC has only the -// .8B/.16B register forms and the .4H/.8H/.2S/.4S immediate form; `bic -// d30, d30, d31` is rejected outright by an assembler. vixl guards it with -// a VIXL_ASSERT, which this tree compiles out so it emits 0x5eff1fde, an -// undefined encoding, and the JIT takes SIGILL the first time the block -// runs. -// * a zero product must be excluded, which is the Fcmeq/Bic pair. Under FZ a -// zero or denormal operand widens to +/-0 and the product is exactly +/-0, -// whose pattern decrements to 0xFFFFFFFFFFFFFFFF -- a NaN. Testing the -// product covers both of the interpreter's guards at once: a product is -// exactly +/-0 only when an operand was zero or denormal, because the -// smallest product of two EE normals is ~2^-252, an ordinary double. +// ft is read out of the allocator-resident guest register, which holds the word +// relocated into double position: the single's mantissa bit k is bit k+29 +// there. The predicate has two terms: +// +// * `mant & 0x2AA` -- bits 1,3,5,7,9, the sign bits of the five lowest +// radix-4 Booth digits, at slot bits 30-38. 0x2AA << 29 is not an aarch64 +// logical immediate and neither is the pair of masks' intersection, but +// 0x5555555555555555 and 0x7fc0000000 both are, so two Ands do it. +// * a boundary term at the truncation column, +// `bit11 != (8 <= (mant >> 12 & 0xF) <= 13)`. The right-hand side is +// `b15 & ~(b14 & b13)`, so the term is three shifted-register ops landing +// on slot bit 44 and a mask to isolate it. +// +// The decrement is a whole EE ULP: a zero-tail product has its low 29 bits +// clear, so subtracting 1 << 29 lands on another exactly-representable single +// that no narrowing can round back. +// +// A zero product is excluded by its exponent field. Under FZ a zero or denormal +// operand widens to +/-0 and the product is exactly +/-0, whose pattern would +// decrement to a NaN. That covers both of the interpreter's guards, since a +// product is exactly +/-0 only when an operand was zero or denormal -- the +// smallest product of two EE normals is ~2^-252, an ordinary double. // // The interpreter's two remaining guards need no codegen. A saturating result // is unreachable-by-one-ULP: products are multiples of 2^81 at that exponent @@ -545,76 +531,87 @@ static void recFPUOp(int info, int eeRecDst, int op /*0=add,1=sub*/, bool acc) // (w == 0x00800000) needs ma*mb == 2^46 with both in [2^23, 2^24), forcing // ma == mb == 2^23 -- ft mantissa 0, predicate off. // -// The predicate is the cheap half of the measured one. `mant & 0x2AA` -- bits -// 1,3,5,7,9, the sign bits of the five lowest radix-4 Booth digits -- is the -// whole rule except for a boundary term at the truncation column, -// `bit11 != (8 <= (mant >> 12 & 0xF) <= 13)`, which is dropped. It fires on -// exactly 1/64 of significands, and only a zero tail lets it change anything: -// counted over all 2^46 significand pairs, 76,236,820 have a zero tail -// -- 1 pair in 923,028 -- so the term decides 1 pair in 59,073,813. When fs is -// a power of two the tail is always zero, and that class scores 98.4375% -// instead of 100%. Tested against hardware, this predicate gives -// 98.4375 / 99.8421 / 99.8482 / 99.8091 per fs significand against the full -// one's 100 / 99.8421 / 99.8482 / 99.8091. -// The other three rows are not evidence that the term buys nothing elsewhere: -// their fs values have a zero tail on 1 or 2 rows out of 2^23, so the term -// cannot show in them at all. The fpmul3 sweeps, whose fs values have trailing -// zeros, do populate that cell: at fs = 1.5 there are 2,796,203 zero-tail rows -// and the term decides 65,536 of them (2.34%). Pooled over fpmul3's 7,196,506 -// zero-tail rows the full predicate is exactly hardware -- 0 missed, 0 wrong -- -// while this one misses 229,142 and is likewise never wrong. So the gap is a -// real 3.3% of the reachable class; what makes it acceptable is that it is -// one-directional (it can only miss a deficit, never invent one) and rare in -// general operand space, per the count above. The term needs a bitfield extract -// NEON has no equivalent for, so it has to go through GPRs and come back -- -// sketched at ten instructions against this predicate's one. -// The resulting interpreter divergence is pinned by -// EeRecFpuFull.MulDefectDropsTheBoundaryTermTheInterpreterModels. +// A tail below the array's 2^15 borrow goes out of line to eeMulOneUlpLow, +// which reconstructs the truncated columns. The guard is one-directional: bits +// 28..21 of the product pattern are clear on every row in the band and on some +// rows outside it, and eeMulOneUlpLow re-tests the tail itself, so a false +// entry costs a call and returns false. A tighter mask spelled on the tail +// alone would miss rows. // -// ft is read narrow, out of the allocator-resident guest register: Cmtst on the -// 64-bit lane only looks at bits 0..9 -- the single's mantissa bits 0..9, -// whatever the register's upper half happens to hold -- so the mask is the -// single-domain 0x2AA and not the double-domain 0x2AA << 29. -// -// The mask is not materialized here: it is parked in d10 for the whole JIT -// session by _DynGen_EnterRecompiledCode, next to the s8/s9 clamp constants and -// under the same AAPCS64 argument: the low 64 bits of d8-d15 are callee-saved, -// so the constant survives every C call without compile-time liveness tracking. -// That is what took this from six instructions to four, and it applies to the -// first multiply in a block as much as the tenth -- a liveness flag in a -// caller-saved register would still have paid mov+fmov to open every span, and -// would have had to be invalidated at C-call seams, branch forks, superblock -// side-exit bodies (recEmitColdSideExits emits several per emission session, -// each reachable only through its own island) and backpatched fastmem thunks, -// where a single missed seam is a silent one-ULP error the corpus cannot see. -// The register costs one slot out of the callee-saved allocator range; the full -// contract, including why microVU needs no pool gate for it, is on -// NEON_RESERVED_FPU_MULMASK in iCore-arm64.h. -// -// `dstidx` holds ToDouble(fs) on entry and the product on exit, `tidx` holds -// ToDouble(ft), `ftnarrowidx` is the untouched guest ft. RQSCRATCH/RQSCRATCH2 -// (q30/q31) are outside the allocator pool, so no temp aliases them. -// -// What comes out, decoded from the code buffer (the Fmul was already there, so -// four of these five are the cost): -// -// cmtst d30, d11, d10 ; d11 == EEREC_T (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 -static void emitDefectiveFmul(int dstidx, int tidx, int ftnarrowidx) +static void emitMulArrayIsland(const a64::VRegister& prod, int fsslotidx, int ftslotidx) +{ + IslandFrame f; + emitIslandEnter(f); + + // The stub takes the architectural words; the slots hold them relocated. + armEmitEeFprNarrow(RXARG1, armDRegister(fsslotidx), RXSCRATCH); + armEmitEeFprNarrow(RXARG2, armDRegister(ftslotidx), RXSCRATCH); + armEmitCall(reinterpret_cast( + &R5900::Interpreter::OpcodeImpl::COP1::eeMulOneUlpLow)); + // AAPCS64 leaves everything above a bool return's one byte unspecified. + armAsm->And(RXSCRATCH, RXARG1, 1); + + emitIslandLeave(f); + + armAsm->Fmov(RXARG2, prod); + armAsm->Sub(RXARG2, RXARG2, a64::Operand(RXSCRATCH, a64::LSL, 29)); + armAsm->Fmov(prod, RXARG2); +} + +// `dstidx` holds the widened fs on entry and the product on exit, `tidx` holds +// the widened ft, `fsslotidx` and `ftslotidx` are the untouched guest operands. +// x0/x1/x8 are the scratch this file uses everywhere, ToPS2FPU_Wide included. +static void emitDefectiveFmul(int dstidx, int tidx, int fsslotidx, int ftslotidx) { const a64::VRegister prod = armDRegister(dstidx); // Hoisted above the Fmul: the predicate is not on its dependency chain. - armAsm->Cmtst(RDSCRATCH, armDRegister(ftnarrowidx), a64::VRegister(NEON_RESERVED_FPU_MULMASK, 64)); + armAsm->Fmov(RXSCRATCH, armDRegister(ftslotidx)); + if (CHECK_FPU_EXACT) + { + armAsm->And(RXARG1, RXSCRATCH, a64::Operand(RXSCRATCH, a64::LSL, 1)); // bit43 = b14 & b13 + armAsm->Bic(RXARG1, RXSCRATCH, a64::Operand(RXARG1, a64::LSL, 1)); // bit44 = b15 & ~(b14 & b13) + armAsm->Eor(RXARG1, RXARG1, a64::Operand(RXSCRATCH, a64::LSL, 4)); // bit44 ^= b11 + armAsm->And(RXARG1, RXARG1, UINT64_C(0x100000000000)); + armAsm->And(RXSCRATCH, RXSCRATCH, UINT64_C(0x5555555555555555)); + armAsm->And(RXSCRATCH, RXSCRATCH, UINT64_C(0x7fc0000000)); + armAsm->Orr(RXARG1, RXARG1, RXSCRATCH); + } + else + { + armAsm->And(RXARG1, RXSCRATCH, UINT64_C(0x5555555555555555)); + armAsm->And(RXARG1, RXARG1, UINT64_C(0x7fc0000000)); + } armAsm->Fmul(prod, prod, armDRegister(tidx)); - armAsm->Fcmeq(RDSCRATCH2, prod, 0.0); - armAsm->Bic(RQSCRATCH.V8B(), RQSCRATCH.V8B(), RQSCRATCH2.V8B()); - armAsm->Add(prod, prod, RDSCRATCH); + // One flag chain: the predicate fired, the tail is empty, the product is not + // zero. Each stage's false arm sets the flags so the next condition cannot + // hold, leaving the final ne false. + armAsm->Fmov(RXARG2, prod); + armAsm->And(RXSCRATCH, RXARG2, UINT64_C(0x1fffffff)); + armAsm->Cmp(RXARG1, 0); + armAsm->Ccmp(RXSCRATCH, 0, a64::NoFlag, a64::ne); + armAsm->And(RXSCRATCH, RXARG2, UINT64_C(0x7ff0000000000000)); + armAsm->Ccmp(RXSCRATCH, 0, a64::ZFlag, a64::eq); + armAsm->Mov(RXARG1, UINT64_C(1) << 29); + armAsm->Csel(RXARG1, RXARG1, a64::xzr, a64::ne); + armAsm->Sub(RXARG2, RXARG2, RXARG1); + armAsm->Fmov(prod, RXARG2); + + if (!CHECK_FPU_EXACT) + return; + + // The rest of the law is the array's. The decrement above cannot have + // changed the tail read here: it only fires on a zero tail, and 1 << 29 + // leaves the low 29 bits alone. + a64::Label done; + armAsm->And(RXSCRATCH, RXARG2, UINT64_C(0x1fffffff)); + armAsm->Cbz(RXSCRATCH, &done); + armAsm->Tst(RXSCRATCH, UINT64_C(0x1fe00000)); + armAsm->B(&done, a64::ne); + emitMulArrayIsland(prod, fsslotidx, ftslotidx); + armAsm->Bind(&done); } // MUL/MULA: widen -> multiply in double (with the multiplier deficit) -> narrow. @@ -625,12 +622,12 @@ static void recMULop(int info, int eeRecDst, bool acc) const int sreg = _allocTempNEONreg(); const int treg = _allocTempNEONreg(); - ToDoubleFrom(sreg, EEREC_S); - ToDoubleFrom(treg, EEREC_T); - emitDefectiveFmul(sreg, treg, EEREC_T); + SlotToDouble(sreg, EEREC_S); + SlotToDouble(treg, EEREC_T); + emitDefectiveFmul(sreg, treg, EEREC_S, EEREC_T); ToPS2FPU_Full(sreg, true, treg, acc, false); - armAsm->Fmov(armSRegister(eeRecDst), armSRegister(sreg)); + SingleToSlot(eeRecDst, sreg); _freeNEONreg(sreg); _freeNEONreg(treg); @@ -647,21 +644,19 @@ static void recMULop(int info, int eeRecDst, bool acc) // result is just +/-max with the dominant sign — skip the double add entirely. // Only when both are finite is the accumulation performed in double. // -// Representation: unlike the x86 port and unlike recFPUOp/recMULop, everything -// between the two roundings stays wide. The invariant from the multiply stage -// to the final ToPS2FPU_Full is "this double is exactly a PS2 single" — low 29 -// mantissa bits zero, |x| <= kEeFpuMax, no denormals — which the guard mask -// preserves (it only clears low bits or reduces an operand to its sign) and -// which is what makes the accumulate exact: two 24-bit significands at an -// exponent distance of at most 24 sum in 48 bits, well inside a double's 53. -// The one arm that leaves the wide domain early is accovf, because kEeFpuMax -// has no single a narrowing could reach. +// Everything between the two roundings stays wide. The invariant from the +// multiply stage to the final ToPS2FPU_Full is that the double is exactly a PS2 +// single — low 29 mantissa bits zero, |x| <= kEeFpuMax, no denormals — which +// the guard mask preserves and which makes the accumulate exact: two 24-bit +// significands at an exponent distance of at most 24 sum in 48 bits, inside a +// double's 53. The accovf arm leaves the wide domain early, kEeFpuMax having no +// single a narrowing could reach. static void recMaddsub(int info, int eeRecDst, int op /*0=add,1=sub*/, bool acc) { const int sreg = _allocTempNEONreg(); const int treg = _allocTempNEONreg(); - // --- multiply stage: sreg = ToPS2FPU(ToDouble(s) * ToDouble(t)). Sets O on + // --- multiply stage: sreg = ToPS2FPU(widen(s) * widen(t)). Sets O on // product overflow; never touches ACCflag here. --- // // The product is rounded but not narrowed: ToPS2FPU_Wide leaves it as a @@ -669,14 +664,14 @@ static void recMaddsub(int info, int eeRecDst, int op /*0=add,1=sub*/, bool acc) // emitter -- the guard mask, the SUB sign flip, the accumulate -- wants the // wide form back, and narrowing here only to re-widen 13 instructions later // was the round trip this shape exists to remove. - ToDoubleFrom(sreg, EEREC_S); - ToDoubleFrom(treg, EEREC_T); - emitDefectiveFmul(sreg, treg, EEREC_T); + SlotToDouble(sreg, EEREC_S); + SlotToDouble(treg, EEREC_T); + emitDefectiveFmul(sreg, treg, EEREC_S, EEREC_T); ToPS2FPU_Wide(sreg); - // --- widen the (allocator-resident, still narrow) ACC straight into treg, - // then guard-mask it against the product in the wide domain. --- - ToDoubleFrom(treg, EEREC_ACC); + // --- widen the (allocator-resident) ACC slot straight into treg, then + // guard-mask it against the product in the wide domain. --- + SlotToDouble(treg, EEREC_ACC); FPU_ADD_SUB_D(treg, sreg); a64::Label mulovf, accovf, operation, skipall; @@ -734,7 +729,7 @@ static void recMaddsub(int info, int eeRecDst, int op /*0=add,1=sub*/, bool acc) ToPS2FPU_Full(treg, true, sreg, acc, true); armAsm->Bind(&skipall); - armAsm->Fmov(armSRegister(eeRecDst), armSRegister(treg)); + SingleToSlot(eeRecDst, treg); _freeNEONreg(sreg); _freeNEONreg(treg); @@ -773,69 +768,58 @@ static void ClearOUFlags() void recABS_S_xmm(int info) { ClearOUFlags(); - armAsm->Fabs(armSRegister(EEREC_D), armSRegister(EEREC_S)); + armAsm->Fabs(armDRegister(EEREC_D), armDRegister(EEREC_S)); } void recNEG_S_xmm(int info) { ClearOUFlags(); - armAsm->Fneg(armSRegister(EEREC_D), armSRegister(EEREC_S)); + armAsm->Fneg(armDRegister(EEREC_D), armDRegister(EEREC_S)); } // MAX/MIN: PS2 semantics on ALL values (incl. denormals — no FTZ, no clamp). -// Port of x86 recMINMAX's integer-ordering trick: for each operand build the -// 64-bit double pattern {lo32 = raw float bits, hi32 = sign | 0x40000000} and -// compare as doubles. The fixed 0x400-exponent upper word makes IEEE-double -// ordering equal PS2 total (sign, magnitude) ordering over the raw bits, and -// no constructed input can be NaN/Inf (the double exponent field is constant), -// so Fmin/Fmax's NaN propagation can never trigger. Result = lower 32 bits of -// the selected pattern. +// Order the two words by (sign, magnitude) and write the winner's word through +// unchanged (iFPU-arm64.cpp recMINMAX derives the ordering key). +// +// The relocation is order-preserving — sign to 63, magnitude to 59..29, 62..60 +// left clear — so the key is the same expression a register width up and Csel +// picks between untouched slots. +// +// Same GPR scratch contract as FPU_ADD_SUB_D: x0/x1/x8 and the non-allocatable +// x9. ClearOUFlags() runs first, so any eviction it emits lands before the raw +// scratch goes live. static void recMINMAX(int info, bool ismin) { - // Temps FIRST: the alloc's eviction stores must not land between the GPR - // pattern builds and their consuming Fmovs (alloc-before-emit rule). - const int sreg = _allocTempNEONreg(); - const int treg = _allocTempNEONreg(); - ClearOUFlags(); - armAsm->Fmov(RWSCRATCH, armSRegister(EEREC_S)); // x8 = zext(s bits) - armAsm->And(RWARG1, RWSCRATCH, 0x80000000); - armAsm->Orr(RWARG1, RWARG1, 0x40000000); - armAsm->Orr(RXSCRATCH, RXSCRATCH, a64::Operand(RXARG1, a64::LSL, 32)); + const a64::Register sbits = RXARG1, tbits = RXARG2; + const a64::Register skey = RXSCRATCH, tkey = a64::x9; - armAsm->Fmov(RWARG2, armSRegister(EEREC_T)); // x1 = zext(t bits) - // GE-M2: exp/sign pattern temp in reserved scratch x9 (was RWARG3/w2, an - // EE-allocatable pool host — see FPU_ADD_SUB). No load/store or C-call spans it. - armAsm->And(a64::w9, RWARG2, 0x80000000); - armAsm->Orr(a64::w9, a64::w9, 0x40000000); - armAsm->Orr(RXARG2, RXARG2, a64::Operand(a64::x9, a64::LSL, 32)); - - armAsm->Fmov(armDRegister(sreg), RXSCRATCH); - armAsm->Fmov(armDRegister(treg), RXARG2); - if (ismin) - armAsm->Fmin(armDRegister(sreg), armDRegister(sreg), armDRegister(treg)); - else - armAsm->Fmax(armDRegister(sreg), armDRegister(sreg), armDRegister(treg)); - armAsm->Fmov(RXSCRATCH, armDRegister(sreg)); - armAsm->Fmov(armSRegister(EEREC_D), RWSCRATCH); // lower 32 = winner's raw bits - _freeNEONreg(sreg); - _freeNEONreg(treg); + armAsm->Fmov(sbits, armDRegister(EEREC_S)); + armAsm->Fmov(tbits, armDRegister(EEREC_T)); + armAsm->Asr(skey, sbits, 63); + armAsm->Eor(skey, sbits, a64::Operand(skey, a64::LSR, 1)); + armAsm->Asr(tkey, tbits, 63); + armAsm->Eor(tkey, tbits, a64::Operand(tkey, a64::LSR, 1)); + armAsm->Cmp(skey, tkey); + // Equal keys mean identical slots, so either arm is correct there. + armAsm->Csel(sbits, sbits, tbits, ismin ? a64::le : a64::ge); + armAsm->Fmov(armDRegister(EEREC_D), sbits); } void recMAX_S_xmm(int info) { recMINMAX(info, false); } void recMIN_S_xmm(int info) { recMINMAX(info, true); } -// C.cond: widen both operands with ToDouble and compare as doubles — a PS2 +// C.cond: widen both operands with SlotToDouble and compare as doubles — a PS2 // pseudo-inf compares as the finite 2^128-scale number it is, with no operand -// clamping (x86 recCMP + recC_*_xmm). ToDouble never yields NaN, so the +// clamping (x86 recCMP + recC_*_xmm). The widening never yields NaN, so the // compare is always ordered and the lt/le/eq condition reads are exact. static void recCMP(int info) { const int sreg = _allocTempNEONreg(); const int treg = _allocTempNEONreg(); - ToDoubleFrom(sreg, EEREC_S); - ToDoubleFrom(treg, EEREC_T); + SlotToDouble(sreg, EEREC_S); + SlotToDouble(treg, EEREC_T); armAsm->Fcmp(armDRegister(sreg), armDRegister(treg)); _freeNEONreg(sreg); _freeNEONreg(treg); @@ -897,20 +881,81 @@ static void ClearIDFlags() // otherwise consistent. The result carries exponent field 0xff — on the EE // that is an ordinary large finite float (the EE has no NaN/Inf), but guest // softfloat routines do classify exp==0xff separately, so the one-ULP-band -// difference from +FLT_MAX is game-visible. -static void SetMaxValueS(int idx) +// difference from +FLT_MAX is game-visible. kEeFprMaxBits is that word's slot, +// so this is the same two masks a register width up. +static void SetMaxValueSlot(int dstidx, int srcidx) { - armAsm->Fmov(RWSCRATCH, armSRegister(idx)); - armAsm->And(RWSCRATCH, RWSCRATCH, 0x80000000); - armAsm->Orr(RWSCRATCH, RWSCRATCH, 0x7fffffff); - armAsm->Fmov(armSRegister(idx), RWSCRATCH); + armAsm->Fmov(RXSCRATCH, armDRegister(srcidx)); + armAsm->And(RXSCRATCH, RXSCRATCH, kEeFprSignBit); + armAsm->Orr(RXSCRATCH, RXSCRATCH, kEeFprMaxBits); + armAsm->Fmov(armDRegister(dstidx), RXSCRATCH); +} + +// ---- The EE's divide/square-root unit --------------------------------------- +// +// It is a radix-2 SRT digit recurrence with no rounding step in it, so an Fdiv +// or an Fsqrt does not reproduce it under any rounding mode. FPU.cpp states the +// model above eeSrtDigit; this is the call into it. +// +// Only mode 4 pays for it. Modes 1 to 3 keep the host instruction and the +// FPUDivFPCR swap, which is right on most operands and one ULP out on the rest. +// +// Silicon composes RSQRT.S out of the other two with an ordinary single in +// between, so this does as well; the intermediate crosses the sqrt's call +// through the island's own scratch, x0 being the only register it could +// otherwise live in. +enum class DivUnitOp +{ + Divide, // eeDivide(fs, ft) + Sqrt, // eeSqrtBits(ft) + RecipSqrt, // eeDivide(fs, eeSqrtBits(ft)) +}; + +static void emitDivideUnitIsland(DivUnitOp op, int dstidx, int fsslotidx, int ftslotidx) +{ + namespace Interp = R5900::Interpreter::OpcodeImpl::COP1; + + IslandFrame f; + emitIslandEnter(f, op == DivUnitOp::RecipSqrt ? 16 : 0); + + // The models take the architectural words; the slots hold them relocated. + // eeSqrtBits drops the operand's sign itself, so the host path's |Ft| has no + // counterpart here. + if (op == DivUnitOp::Sqrt) + { + armEmitEeFprNarrow(RXARG1, armDRegister(ftslotidx), RXSCRATCH); + } + else + { + armEmitEeFprNarrow(RXARG1, armDRegister(fsslotidx), RXSCRATCH); + armEmitEeFprNarrow(RXARG2, armDRegister(ftslotidx), RXSCRATCH); + } + + if (op == DivUnitOp::RecipSqrt) + { + armAsm->Str(RXARG1.W(), IslandSpare(f)); + armAsm->Mov(RXARG1.W(), RXARG2.W()); + } + if (op != DivUnitOp::Divide) + armEmitCall(reinterpret_cast(&Interp::eeSqrtBits)); + if (op == DivUnitOp::RecipSqrt) + { + armAsm->Mov(RXARG2.W(), RXARG1.W()); + armAsm->Ldr(RXARG1.W(), IslandSpare(f)); + } + if (op != DivUnitOp::Sqrt) + armEmitCall(reinterpret_cast(&Interp::eeDivide)); + + armAsm->Mov(RWSCRATCH, RXARG1.W()); + emitIslandLeave(f); + armEmitEeFprWiden(armDRegister(dstidx), RWSCRATCH, RXSCRATCH); } // x86 recDIVhelper1 (FPU_FLAGS_ID == 1 unconditionally): divide-by-zero -// flag/result shape in the single domain, otherwise divide in double. -// sreg/treg are write-only temps and srcS/srcT the allocator-resident operands, -// which are only ever read; the result lands in sreg (S lane on the zero-divisor -// arm, S lane after ToPS2FPU_Full on the normal one). +// flag/result shape, otherwise the quotient -- from the recurrence under mode 4 +// and in double below it. sreg/treg are write-only temps, srcS/srcT the guest +// slots; the result lands in sreg as a slot on both arms. treg is -1 under +// mode 4, which has no double to hold. // The Fcmp-with-zero runs under the EE FPCR whose FZ bit flushes denormal // inputs — same divisor-is-zero net as x86's DAZ'd CMPEQ.SS. The double // quotient of two in-range PS2 values is always finite (max magnitude @@ -920,13 +965,13 @@ static void recDIVhelper1(int sreg, int treg, int srcS, int srcT) ClearIDFlags(); a64::Label normal, xOverZero, setDone, done; - armAsm->Fcmp(armSRegister(srcT), 0.0); + armAsm->Fcmp(armDRegister(srcT), 0.0); armAsm->B(&normal, a64::ne); // Divisor is ±0: pick the flag pair, then result = (fs ^ ft) | 0x7fffffff // (x86 SetMaxValue under FPU_RESULT — see SetMaxValueS above; masking the // XOR down to its sign bit first is equivalent, the OR sets bits 0..30). - armAsm->Fcmp(armSRegister(srcS), 0.0); + armAsm->Fcmp(armDRegister(srcS), 0.0); armAsm->B(&xOverZero, a64::ne); SetFprcOr(FPUflagI | FPUflagSI); // 0/0 armAsm->B(&setDone); @@ -934,19 +979,27 @@ static void recDIVhelper1(int sreg, int treg, int srcS, int srcT) SetFprcOr(FPUflagD | FPUflagSD); // x/0 armAsm->Bind(&setDone); - armAsm->Fmov(RWSCRATCH, armSRegister(srcS)); - armAsm->Fmov(RWARG1, armSRegister(srcT)); - armAsm->Eor(RWSCRATCH, RWSCRATCH, RWARG1); - armAsm->And(RWSCRATCH, RWSCRATCH, 0x80000000); - armAsm->Orr(RWSCRATCH, RWSCRATCH, 0x7fffffff); - armAsm->Fmov(armSRegister(sreg), RWSCRATCH); + armAsm->Fmov(RXSCRATCH, armDRegister(srcS)); + armAsm->Fmov(RXARG1, armDRegister(srcT)); + armAsm->Eor(RXSCRATCH, RXSCRATCH, RXARG1); + armAsm->And(RXSCRATCH, RXSCRATCH, kEeFprSignBit); + armAsm->Orr(RXSCRATCH, RXSCRATCH, kEeFprMaxBits); + armAsm->Fmov(armDRegister(sreg), RXSCRATCH); armAsm->B(&done); armAsm->Bind(&normal); - ToDoubleFrom(sreg, srcS); - ToDoubleFrom(treg, srcT); - armAsm->Fdiv(armDRegister(sreg), armDRegister(sreg), armDRegister(treg)); - ToPS2FPU_Full(sreg, false, treg, false, false); + if (CHECK_FPU_EXACT) + { + emitDivideUnitIsland(DivUnitOp::Divide, sreg, srcS, srcT); + } + else + { + SlotToDouble(sreg, srcS); + SlotToDouble(treg, srcT); + armAsm->Fdiv(armDRegister(sreg), armDRegister(sreg), armDRegister(treg)); + ToPS2FPU_Full(sreg, false, treg, false, false); + SingleToSlot(sreg, sreg); + } armAsm->Bind(&done); } @@ -958,12 +1011,15 @@ void recDIV_S_xmm(int info) if (swapFpcr) emitLoadFPCRImm(EmuConfig.Cpu.FPUDivFPCR.bitmask); + // EEREC_D may be either operand and the normal arm writes before it has read + // both, so the result is built in a temp. const int sreg = _allocTempNEONreg(); - const int treg = _allocTempNEONreg(); + const int treg = CHECK_FPU_EXACT ? -1 : _allocTempNEONreg(); recDIVhelper1(sreg, treg, EEREC_S, EEREC_T); - armAsm->Fmov(armSRegister(EEREC_D), armSRegister(sreg)); + armAsm->Fmov(armDRegister(EEREC_D), armDRegister(sreg)); _freeNEONreg(sreg); - _freeNEONreg(treg); + if (!CHECK_FPU_EXACT) + _freeNEONreg(treg); if (swapFpcr) emitLoadFPCRImm(EmuConfig.Cpu.FPUFPCR.bitmask); @@ -971,82 +1027,115 @@ void recDIV_S_xmm(int info) void recSQRT_S_xmm(int info) { - // Round-to-nearest for the double Fsqrt + the ToPS2FPU narrowing, like - // x86's roundmode_nearest swap (FPUDivFPCR is the nearest-mode FPCR). + // Round-to-nearest for the double Fsqrt and the narrowing Fcvt, like x86's + // roundmode_nearest swap (FPUDivFPCR is the nearest-mode FPCR). const bool swapFpcr = EmuConfig.Cpu.FPUFPCR.bitmask != EmuConfig.Cpu.FPUDivFPCR.bitmask; if (swapFpcr) emitLoadFPCRImm(EmuConfig.Cpu.FPUDivFPCR.bitmask); - const int treg = copySrc(EEREC_T); // SQRT.S reads FT + // SQRT.S reads FT. The recurrence takes it as a word, so only the host arm + // needs it widened. + const int treg = CHECK_FPU_EXACT ? -1 : _allocTempNEONreg(); + if (!CHECK_FPU_EXACT) + SlotToDouble(treg, EEREC_T); ClearIDFlags(); - // x86 DOUBLE tests the raw SIGN BIT (unlike the fast body's exp-field + // x86 DOUBLE tests the raw sign bit (unlike the fast body's exp-field // gate): sqrt(-0) sets I|SI too, then |t| makes the operand positive. - // x86-JIT is the FULL-mode oracle for this corner. - armAsm->Fmov(RWARG1, armSRegister(treg)); + // x86-JIT is the FULL-mode oracle for this corner. The slot carries that + // bit at 63, so the test needs no word. + armAsm->Fmov(RXARG1, armDRegister(EEREC_T)); a64::Label tPositive; - armAsm->Tbz(RWARG1, 31, &tPositive); + armAsm->Tbz(RXARG1, 63, &tPositive); SetFprcOr(FPUflagI | FPUflagSI); - armAsm->Fabs(armSRegister(treg), armSRegister(treg)); + if (!CHECK_FPU_EXACT) + armAsm->Fabs(armDRegister(treg), armDRegister(treg)); armAsm->Bind(&tPositive); - ToDouble(treg); - armAsm->Fsqrt(armDRegister(treg), armDRegister(treg)); - ToPS2FPU_Full(treg, false, treg, false, false); - armAsm->Fmov(armSRegister(EEREC_D), armSRegister(treg)); - _freeNEONreg(treg); + if (CHECK_FPU_EXACT) + { + emitDivideUnitIsland(DivUnitOp::Sqrt, EEREC_D, EEREC_T, EEREC_T); + } + else + { + armAsm->Fsqrt(armDRegister(treg), armDRegister(treg)); + // A root cannot leave the in-range band, so the narrowing is the plain + // Fcvt with none of ToPS2FPU_Full's arms around it: the largest operand + // is a shade under 2^129 and roots to under 2^65, the smallest one FZ + // does not flush is 2^-126 and roots to 2^-63, and both sit inside + // [2^-126, 2^128). The one result outside it is a zero, which the + // underflow arm would have flushed to the same zero. + armAsm->Fcvt(armSRegister(treg), armDRegister(treg)); + SingleToSlot(EEREC_D, treg); + _freeNEONreg(treg); + } if (swapFpcr) emitLoadFPCRImm(EmuConfig.Cpu.FPUFPCR.bitmask); } // x86 recRSQRThelper1: negative-divisor I|SI + |t|, zero-divisor flag pair -// with SetMaxValue keyed off the DIVIDEND's sign, else fs / sqrt(ft) in -// double. (The interp keys the zero-divisor sign off the DIVISOR — x86-JIT -// wins that disagreement under FULL.) +// with SetMaxValue keyed off the DIVIDEND's sign, else fs / sqrt(ft) — through +// the recurrence under mode 4 and in double below it. (The interp keys the +// zero-divisor sign off the DIVISOR — x86-JIT wins that disagreement under +// FULL.) void recRSQRT_S_xmm(int info) { const bool swapFpcr = EmuConfig.Cpu.FPUFPCR.bitmask != EmuConfig.Cpu.FPUDivFPCR.bitmask; if (swapFpcr) emitLoadFPCRImm(EmuConfig.Cpu.FPUDivFPCR.bitmask); - const int sreg = copySrc(EEREC_S); - const int treg = copySrc(EEREC_T); + // As in recDIV_S_xmm, the result is built in a temp. + const int sreg = _allocTempNEONreg(); + const int treg = CHECK_FPU_EXACT ? -1 : _allocTempNEONreg(); ClearIDFlags(); - armAsm->Fmov(RWARG1, armSRegister(treg)); + armAsm->Fmov(RXARG1, armDRegister(EEREC_T)); a64::Label tPositive; - armAsm->Tbz(RWARG1, 31, &tPositive); + armAsm->Tbz(RXARG1, 63, &tPositive); SetFprcOr(FPUflagI | FPUflagSI); - armAsm->Fabs(armSRegister(treg), armSRegister(treg)); armAsm->Bind(&tPositive); + // Unconditional: |t| is a no-op on the positive arm and doubles as the copy + // that keeps ft's slot intact. eeSqrtBits drops the sign itself, so mode 4 + // tests ft where it lies -- Fcmp puts -0 equal to 0 either way. + if (!CHECK_FPU_EXACT) + armAsm->Fabs(armDRegister(treg), armDRegister(EEREC_T)); a64::Label normal, zeroOverZero, setDone, done; - armAsm->Fcmp(armSRegister(treg), 0.0); + armAsm->Fcmp(armDRegister(CHECK_FPU_EXACT ? EEREC_T : treg), 0.0); armAsm->B(&normal, a64::ne); - armAsm->Fcmp(armSRegister(sreg), 0.0); + armAsm->Fcmp(armDRegister(EEREC_S), 0.0); armAsm->B(&zeroOverZero, a64::eq); SetFprcOr(FPUflagD | FPUflagSD); // x/0 armAsm->B(&setDone); armAsm->Bind(&zeroOverZero); SetFprcOr(FPUflagI | FPUflagSI); // 0/0 armAsm->Bind(&setDone); - SetMaxValueS(sreg); + SetMaxValueSlot(sreg, EEREC_S); armAsm->B(&done); armAsm->Bind(&normal); - ToDouble(treg); - ToDouble(sreg); - armAsm->Fsqrt(armDRegister(treg), armDRegister(treg)); - armAsm->Fdiv(armDRegister(sreg), armDRegister(sreg), armDRegister(treg)); - ToPS2FPU_Full(sreg, false, treg, false, false); + if (CHECK_FPU_EXACT) + { + emitDivideUnitIsland(DivUnitOp::RecipSqrt, sreg, EEREC_S, EEREC_T); + } + else + { + SlotToDouble(treg, treg); + SlotToDouble(sreg, EEREC_S); + armAsm->Fsqrt(armDRegister(treg), armDRegister(treg)); + armAsm->Fdiv(armDRegister(sreg), armDRegister(sreg), armDRegister(treg)); + ToPS2FPU_Full(sreg, false, treg, false, false); + SingleToSlot(sreg, sreg); + } armAsm->Bind(&done); - armAsm->Fmov(armSRegister(EEREC_D), armSRegister(sreg)); + armAsm->Fmov(armDRegister(EEREC_D), armDRegister(sreg)); _freeNEONreg(sreg); - _freeNEONreg(treg); + if (!CHECK_FPU_EXACT) + _freeNEONreg(treg); if (swapFpcr) emitLoadFPCRImm(EmuConfig.Cpu.FPUFPCR.bitmask); diff --git a/pcsx2/arm64/iR5900-arm64.cpp b/pcsx2/arm64/iR5900-arm64.cpp index 535c275304..06ca5f2665 100644 --- a/pcsx2/arm64/iR5900-arm64.cpp +++ b/pcsx2/arm64/iR5900-arm64.cpp @@ -322,20 +322,15 @@ static const void* _DynGen_EnterRecompiledCode() armAsm->Ldr(a64::s8, FLT_MAX); armAsm->Ldr(a64::s9, -FLT_MAX); - // Same convention, one register along: d10 = 0x2AA, the Booth-digit mask - // of the EE multiplier's one-ULP deficit (NEON_RESERVED_FPU_MULMASK; the - // contract is on the constant, the consumer is emitDefectiveFmul in - // iFPUd-arm64.cpp). Parking it here is what makes the mode-3 multiply - // sequence 4 instructions instead of 6 — every consumer reads it, none - // materializes it, and because the low 64 bits of d8-d15 are callee-saved - // there is no C-call seam, branch fork, superblock side exit or - // backpatched fastmem thunk that can invalidate it. + // Same convention, three registers along: d11 = 2^kEeFprScaleExp, which + // turns a slot into the value it denotes. The contract is on + // NEON_RESERVED_EEFPU_UNSCALE; iFPUd-arm64.cpp is the consumer. // // Emitted unconditionally rather than under CHECK_FPU_FULL: two // instructions once per JIT entry are not worth a dispatcher that goes // stale if the clamp mode changes without a recompiler reset. - armAsm->Mov(RXSCRATCH, UINT64_C(0x2AA)); - armAsm->Fmov(a64::VRegister(NEON_RESERVED_FPU_MULMASK, 64), RXSCRATCH); + armAsm->Mov(RXSCRATCH, kEeFprUnscaleBits); + armAsm->Fmov(a64::VRegister(NEON_RESERVED_EEFPU_UNSCALE, 64), RXSCRATCH); // Load fastmem base into x19 if enabled if (CHECK_FASTMEM) @@ -530,11 +525,13 @@ void iFlushCall(int flushtype) } } - // GE-15: 32-bit FPR-class slots (NEONTYPE_FPREG/FPACC) in the - // callee-saved q10-q15 range survive plain C-helper seams — AAPCS64 - // preserves the LOWER 64 bits of v8-v15, and this class only ever - // reads/writes lane 0 (S register; _writebackNEONreg stores S-width, so - // post-call garbage in the upper lanes is never observed). Writeback if + // GE-15: FPR-class slots (NEONTYPE_FPREG/FPACC) in the callee-saved + // q10-q15 range survive plain C-helper seams — AAPCS64 preserves the + // LOWER 64 bits of v8-v15, and this class only ever reads/writes lane 0, + // at S width, or at D width where eeClampMode 3 and up put a relocated + // double in the slot. Both fit the preserved half, and _writebackNEONreg stores + // the same width it filled, so post-call garbage above it is never + // observed. Writeback if // dirty but KEEP mapped — 4248's writeback-dirty-but-keep passes, whose // type-mask 0x482 likewise excludes the 128-bit classes: GPRREG quads / // VFREG upper lanes are caller-saved and must still flush diff --git a/pcsx2/arm64/iR5900-arm64.h b/pcsx2/arm64/iR5900-arm64.h index 96a14ec674..4041007d77 100644 --- a/pcsx2/arm64/iR5900-arm64.h +++ b/pcsx2/arm64/iR5900-arm64.h @@ -186,6 +186,56 @@ static __fi vixl::aarch64::MemOperand armCpuRegMem(const void* field) return vixl::aarch64::MemOperand(RSTATE, static_cast(off)); } +/* An FPR slot as seen from outside the FPU's own arithmetic: MFC1/MTC1, + LWC1/SWC1, MOV.S and the allocator's fill and spill. What a slot holds + follows the clamp mode (EeFpuFormat.h), which a code-cache reset pins for + the life of a block, so these branch at emit time. +*/ +static __fi const vixl::aarch64::VRegister& armEeFprSlotReg(int neonreg) +{ + return CHECK_FPU_FULL ? armDRegister(neonreg) : armSRegister(neonreg); +} + +// `slot` = the word in `word`. `tmp` is an X scratch and may be `word`'s X form. +static __fi void armEmitEeFprSlotFromWord(const vixl::aarch64::VRegister& slot, + const vixl::aarch64::Register& word, const vixl::aarch64::Register& tmp) +{ + if (CHECK_FPU_FULL) + armEmitEeFprWiden(slot, word, tmp); + else + armAsm->Fmov(slot.S(), word.W()); +} + +// `word` = the word in `slot`, zero-extended. `tmp` is an X scratch, not `word`. +static __fi void armEmitEeFprWordFromSlot(const vixl::aarch64::Register& word, + const vixl::aarch64::VRegister& slot, const vixl::aarch64::Register& tmp) +{ + if (CHECK_FPU_FULL) + armEmitEeFprNarrow(word, slot, tmp); + else + armAsm->Fmov(word.W(), slot.S()); +} + +// *mem = the word in `word`. `tmp` is an X scratch and may be `word`'s X form. +static __fi void armEmitEeFprSlotMemFromWord(const vixl::aarch64::MemOperand& mem, + const vixl::aarch64::Register& word, const vixl::aarch64::Register& tmp) +{ + if (CHECK_FPU_FULL) + armEmitEeFprStoreSlotWord(mem, word, tmp); + else + armAsm->Str(word.W(), mem); +} + +// `word` = the word in *mem, zero-extended. `tmp` is an X scratch, not `word`. +static __fi void armEmitEeFprWordFromSlotMem(const vixl::aarch64::Register& word, + const vixl::aarch64::MemOperand& mem, const vixl::aarch64::Register& tmp) +{ + if (CHECK_FPU_FULL) + armEmitEeFprLoadSlotWord(word, mem, tmp); + else + armAsm->Ldr(word.W(), mem); +} + // Publish the ABSOLUTE cycle to cpuRegs.cycle before a C call that reads it: // abs = delta + nextEventCycle. RECCYCLE itself is preserved (still the // delta); `scratch` is clobbered. Pair with armReloadCycleDelta after any diff --git a/pcsx2/arm64/recVTLB-arm64.cpp b/pcsx2/arm64/recVTLB-arm64.cpp index efb67aa65c..932af6e2a1 100644 --- a/pcsx2/arm64/recVTLB-arm64.cpp +++ b/pcsx2/arm64/recVTLB-arm64.cpp @@ -1047,6 +1047,14 @@ void recLWC1() // is-load-dest skip excludes it from the save set). const int ftreg = _allocFPtoNEONreg(_Rt_, MODE_WRITE); vtlbFastmemReadFPR32(9, ftreg); + if (CHECK_FPU_FULL) + { + // The load lands the architectural word; on the double tier the + // slot holds it relocated. Emitted after the backpatch site so it + // runs on the thunk's return path too (the slow path ends in the + // same Fmov S, w0). + armEmitEeFprFromS(armDRegister(ftreg), armSRegister(ftreg), RXSCRATCH); + } } else { @@ -1057,7 +1065,7 @@ void recLWC1() // (if any) is now stale and must not flush back over the write. _deleteFPtoNEONreg(_Rt_, DELETE_REG_FREE_NO_WRITEBACK); // Store to fpuRegs.fpr[ft] - armStoreEERegPtr(a64::w0, &fpuRegs.fpr[_Rt_].UL); + armEmitEeFprSlotMemFromWord(armCpuRegMem(&fpuRegs.fpr[_Rt_]), a64::w0, RXSCRATCH); } } @@ -1072,9 +1080,9 @@ void recSWC1() // after w10 already holds the value). const int fslot = _checkNEONreg(NEONTYPE_FPREG, _Rt_, 0); if (fslot >= 0) - armAsm->Fmov(a64::w10, armSRegister(fslot)); + armEmitEeFprWordFromSlot(a64::w10, armDRegister(fslot), RXSCRATCH); else - armLoadEERegPtr(a64::w10, &fpuRegs.fpr[_Rt_].UL); + armEmitEeFprWordFromSlotMem(a64::w10, armCpuRegMem(&fpuRegs.fpr[_Rt_]), RXSCRATCH); // Inline STR off RFASTMEMBASE + backpatch on the fast path, softmem // fallback otherwise. diff --git a/pcsx2/ee_divtrace.cpp b/pcsx2/ee_divtrace.cpp index cd8fdd038e..bace779f18 100644 --- a/pcsx2/ee_divtrace.cpp +++ b/pcsx2/ee_divtrace.cpp @@ -82,8 +82,8 @@ namespace ee_divtrace if (!g_fp_exclude) { for (int i = 0; i < 32; ++i) - h = hash_mix(h, fpuRegs.fpr[i].UL); - h = hash_mix(h, fpuRegs.ACC.UL); + h = hash_mix(h, fpuRegs.fpr[i].Word()); + h = hash_mix(h, fpuRegs.ACC.Word()); } // VU0 macro-visible state — opt-in (EERUNNER_VU0FP): catches COP2 macro // divergence at the producing block instead of downstream in GPRs/memory. diff --git a/platforms/android/app/src/main/assets/i18n/ar.json b/platforms/android/app/src/main/assets/i18n/ar.json index ef2b0ec744..614b40c6ba 100644 --- a/platforms/android/app/src/main/assets/i18n/ar.json +++ b/platforms/android/app/src/main/assets/i18n/ar.json @@ -512,7 +512,6 @@ "perf.fix.eeTiming": "EE Timing", "perf.fix.extraXgkick": "Extra XGKICK", "perf.fix.fmvSoftware": "FMV Software", - "perf.fix.fpuMultiply": "FPU Multiply", "perf.fix.fullVu0Sync": "Full VU0 Sync", "perf.fix.gamedbFixes": "GameDB Fixes", "perf.fix.gifFifo": "GIF FIFO", diff --git a/platforms/android/app/src/main/assets/i18n/de.json b/platforms/android/app/src/main/assets/i18n/de.json index 884e191acb..0a88526886 100644 --- a/platforms/android/app/src/main/assets/i18n/de.json +++ b/platforms/android/app/src/main/assets/i18n/de.json @@ -564,7 +564,6 @@ "perf.fix.eeTiming": "EE Timing", "perf.fix.extraXgkick": "Extra XGKICK", "perf.fix.fmvSoftware": "FMV Software", - "perf.fix.fpuMultiply": "FPU Multiply", "perf.fix.fullVu0Sync": "Full VU0 Sync", "perf.fix.gamedbFixes": "GameDB-Fixes", "perf.fix.gifFifo": "GIF FIFO", diff --git a/platforms/android/app/src/main/assets/i18n/es.json b/platforms/android/app/src/main/assets/i18n/es.json index 387820ed35..e1a0ae6367 100644 --- a/platforms/android/app/src/main/assets/i18n/es.json +++ b/platforms/android/app/src/main/assets/i18n/es.json @@ -512,7 +512,6 @@ "perf.fix.eeTiming": "EE Timing", "perf.fix.extraXgkick": "Extra XGKICK", "perf.fix.fmvSoftware": "FMV Software", - "perf.fix.fpuMultiply": "FPU Multiply", "perf.fix.fullVu0Sync": "Full VU0 Sync", "perf.fix.gamedbFixes": "GameDB Fixes", "perf.fix.gifFifo": "GIF FIFO", diff --git a/platforms/android/app/src/main/assets/i18n/fa.json b/platforms/android/app/src/main/assets/i18n/fa.json index f2c52f4afa..4391776439 100644 --- a/platforms/android/app/src/main/assets/i18n/fa.json +++ b/platforms/android/app/src/main/assets/i18n/fa.json @@ -579,7 +579,6 @@ "perf.fix.eeTiming": "زمان بندی EE", "perf.fix.extraXgkick": "XGKICK اضافی", "perf.fix.fmvSoftware": "نرم افزار FMV", - "perf.fix.fpuMultiply": "ضرب کردن FPU", "perf.fix.fullVu0Sync": "همگام سازی کامل VU0", "perf.fix.gamedbFixes": "رفع GameDB", "perf.fix.gifFifo": "GIF FIFO", diff --git a/platforms/android/app/src/main/assets/i18n/fr.json b/platforms/android/app/src/main/assets/i18n/fr.json index f49295e2b9..081ecca695 100644 --- a/platforms/android/app/src/main/assets/i18n/fr.json +++ b/platforms/android/app/src/main/assets/i18n/fr.json @@ -512,7 +512,6 @@ "perf.fix.eeTiming": "EE Timing", "perf.fix.extraXgkick": "Extra XGKICK", "perf.fix.fmvSoftware": "FMV Software", - "perf.fix.fpuMultiply": "FPU Multiply", "perf.fix.fullVu0Sync": "Full VU0 Sync", "perf.fix.gamedbFixes": "GameDB Fixes", "perf.fix.gifFifo": "GIF FIFO", diff --git a/platforms/android/app/src/main/assets/i18n/id.json b/platforms/android/app/src/main/assets/i18n/id.json index 76715964bc..a59fc3cbb4 100644 --- a/platforms/android/app/src/main/assets/i18n/id.json +++ b/platforms/android/app/src/main/assets/i18n/id.json @@ -512,7 +512,6 @@ "perf.fix.eeTiming": "EE Timing", "perf.fix.extraXgkick": "Extra XGKICK", "perf.fix.fmvSoftware": "FMV Software", - "perf.fix.fpuMultiply": "FPU Multiply", "perf.fix.fullVu0Sync": "Full VU0 Sync", "perf.fix.gamedbFixes": "GameDB Fixes", "perf.fix.gifFifo": "GIF FIFO", diff --git a/platforms/android/app/src/main/assets/i18n/it.json b/platforms/android/app/src/main/assets/i18n/it.json index d0224fc991..982eb5eeaf 100644 --- a/platforms/android/app/src/main/assets/i18n/it.json +++ b/platforms/android/app/src/main/assets/i18n/it.json @@ -512,7 +512,6 @@ "perf.fix.eeTiming": "EE Timing", "perf.fix.extraXgkick": "Extra XGKICK", "perf.fix.fmvSoftware": "FMV Software", - "perf.fix.fpuMultiply": "FPU Multiply", "perf.fix.fullVu0Sync": "Full VU0 Sync", "perf.fix.gamedbFixes": "Correzioni GameDB", "perf.fix.gifFifo": "GIF FIFO", diff --git a/platforms/android/app/src/main/assets/i18n/ja.json b/platforms/android/app/src/main/assets/i18n/ja.json index 48fc6688c2..1806bbd138 100644 --- a/platforms/android/app/src/main/assets/i18n/ja.json +++ b/platforms/android/app/src/main/assets/i18n/ja.json @@ -512,7 +512,6 @@ "perf.fix.eeTiming": "EE Timing", "perf.fix.extraXgkick": "Extra XGKICK", "perf.fix.fmvSoftware": "FMV Software", - "perf.fix.fpuMultiply": "FPU Multiply", "perf.fix.fullVu0Sync": "Full VU0 Sync", "perf.fix.gamedbFixes": "GameDB Fixes", "perf.fix.gifFifo": "GIF FIFO", diff --git a/platforms/android/app/src/main/assets/i18n/ko.json b/platforms/android/app/src/main/assets/i18n/ko.json index b2926515b3..ba4ab88047 100644 --- a/platforms/android/app/src/main/assets/i18n/ko.json +++ b/platforms/android/app/src/main/assets/i18n/ko.json @@ -512,7 +512,6 @@ "perf.fix.eeTiming": "EE Timing", "perf.fix.extraXgkick": "Extra XGKICK", "perf.fix.fmvSoftware": "FMV Software", - "perf.fix.fpuMultiply": "FPU Multiply", "perf.fix.fullVu0Sync": "Full VU0 Sync", "perf.fix.gamedbFixes": "GameDB Fixes", "perf.fix.gifFifo": "GIF FIFO", diff --git a/platforms/android/app/src/main/assets/i18n/ku.json b/platforms/android/app/src/main/assets/i18n/ku.json index a89c9eb3aa..032060c850 100644 --- a/platforms/android/app/src/main/assets/i18n/ku.json +++ b/platforms/android/app/src/main/assets/i18n/ku.json @@ -565,7 +565,6 @@ "perf.fix.eeTiming": "EE Timeming", "perf.fix.extraXgkick": "XGKICK Zêdeyî", "perf.fix.fmvSoftware": "FMV Software", - "perf.fix.fpuMultiply": "FPU Multiply", "perf.fix.fullVu0Sync": "Tevahiya VU0 Sync", "perf.fix.gamedbFixes": "GameDB rast dike", "perf.fix.gifFifo": "GIF FIFO", diff --git a/platforms/android/app/src/main/assets/i18n/pl.json b/platforms/android/app/src/main/assets/i18n/pl.json index 11133ae82d..60e36ab54a 100644 --- a/platforms/android/app/src/main/assets/i18n/pl.json +++ b/platforms/android/app/src/main/assets/i18n/pl.json @@ -512,7 +512,6 @@ "perf.fix.eeTiming": "EE Timing", "perf.fix.extraXgkick": "Extra XGKICK", "perf.fix.fmvSoftware": "FMV Software", - "perf.fix.fpuMultiply": "FPU Multiply", "perf.fix.fullVu0Sync": "Full VU0 Sync", "perf.fix.gamedbFixes": "GameDB Fixes", "perf.fix.gifFifo": "GIF FIFO", diff --git a/platforms/android/app/src/main/assets/i18n/pt-BR.json b/platforms/android/app/src/main/assets/i18n/pt-BR.json index b516acf613..5089650e55 100644 --- a/platforms/android/app/src/main/assets/i18n/pt-BR.json +++ b/platforms/android/app/src/main/assets/i18n/pt-BR.json @@ -511,7 +511,6 @@ "perf.fix.eeTiming": "EE Timing", "perf.fix.extraXgkick": "Extra XGKICK", "perf.fix.fmvSoftware": "FMV Software", - "perf.fix.fpuMultiply": "FPU Multiply", "perf.fix.fullVu0Sync": "Full VU0 Sync", "perf.fix.gamedbFixes": "Correções GameDB", "perf.fix.gifFifo": "GIF FIFO", diff --git a/platforms/android/app/src/main/assets/i18n/ru.json b/platforms/android/app/src/main/assets/i18n/ru.json index 53c08b67e2..449b1b97da 100644 --- a/platforms/android/app/src/main/assets/i18n/ru.json +++ b/platforms/android/app/src/main/assets/i18n/ru.json @@ -565,7 +565,6 @@ "perf.fix.eeTiming": "ЭЭ Тайминг", "perf.fix.extraXgkick": "Дополнительный XGKICK", "perf.fix.fmvSoftware": "Программное обеспечение FMV", - "perf.fix.fpuMultiply": "Умножение ФПУ", "perf.fix.fullVu0Sync": "Полная синхронизация VU0", "perf.fix.gamedbFixes": "Исправления GameDB", "perf.fix.gifFifo": "Гиф ФИФО", diff --git a/platforms/android/app/src/main/assets/i18n/th.json b/platforms/android/app/src/main/assets/i18n/th.json index 0dd4e503f5..a7b89308af 100644 --- a/platforms/android/app/src/main/assets/i18n/th.json +++ b/platforms/android/app/src/main/assets/i18n/th.json @@ -512,7 +512,6 @@ "perf.fix.eeTiming": "EE Timing", "perf.fix.extraXgkick": "Extra XGKICK", "perf.fix.fmvSoftware": "FMV Software", - "perf.fix.fpuMultiply": "FPU Multiply", "perf.fix.fullVu0Sync": "Full VU0 Sync", "perf.fix.gamedbFixes": "GameDB Fixes", "perf.fix.gifFifo": "GIF FIFO", diff --git a/platforms/android/app/src/main/assets/i18n/tr.json b/platforms/android/app/src/main/assets/i18n/tr.json index 39eaf1c50f..404cd522aa 100644 --- a/platforms/android/app/src/main/assets/i18n/tr.json +++ b/platforms/android/app/src/main/assets/i18n/tr.json @@ -512,7 +512,6 @@ "perf.fix.eeTiming": "EE Timing", "perf.fix.extraXgkick": "Extra XGKICK", "perf.fix.fmvSoftware": "FMV Software", - "perf.fix.fpuMultiply": "FPU Multiply", "perf.fix.fullVu0Sync": "Full VU0 Sync", "perf.fix.gamedbFixes": "GameDB Fixes", "perf.fix.gifFifo": "GIF FIFO", diff --git a/platforms/android/app/src/main/assets/i18n/uk.json b/platforms/android/app/src/main/assets/i18n/uk.json index c6cd0b7797..305e0fb7ad 100644 --- a/platforms/android/app/src/main/assets/i18n/uk.json +++ b/platforms/android/app/src/main/assets/i18n/uk.json @@ -565,7 +565,6 @@ "perf.fix.eeTiming": "EE Таймінг", "perf.fix.extraXgkick": "Додатковий XGKICK", "perf.fix.fmvSoftware": "Програмне забезпечення FMV", - "perf.fix.fpuMultiply": "Множення FPU", "perf.fix.fullVu0Sync": "Повна синхронізація VU0", "perf.fix.gamedbFixes": "Виправлення GameDB", "perf.fix.gifFifo": "GIF FIFO", diff --git a/platforms/android/app/src/main/assets/i18n/vi.json b/platforms/android/app/src/main/assets/i18n/vi.json index 8747e63677..942d5b5ee5 100644 --- a/platforms/android/app/src/main/assets/i18n/vi.json +++ b/platforms/android/app/src/main/assets/i18n/vi.json @@ -512,7 +512,6 @@ "perf.fix.eeTiming": "EE Timing", "perf.fix.extraXgkick": "Extra XGKICK", "perf.fix.fmvSoftware": "FMV Software", - "perf.fix.fpuMultiply": "FPU Multiply", "perf.fix.fullVu0Sync": "Full VU0 Sync", "perf.fix.gamedbFixes": "GameDB Fixes", "perf.fix.gifFifo": "GIF FIFO", diff --git a/platforms/android/app/src/main/assets/i18n/zh-CN.json b/platforms/android/app/src/main/assets/i18n/zh-CN.json index 7f6aaa63f9..faf8e18dd9 100644 --- a/platforms/android/app/src/main/assets/i18n/zh-CN.json +++ b/platforms/android/app/src/main/assets/i18n/zh-CN.json @@ -514,7 +514,6 @@ "perf.fix.eeTiming": "EE Timing", "perf.fix.extraXgkick": "Extra XGKICK", "perf.fix.fmvSoftware": "FMV Software", - "perf.fix.fpuMultiply": "FPU Multiply", "perf.fix.fullVu0Sync": "Full VU0 Sync", "perf.fix.gamedbFixes": "GameDB Fixes", "perf.fix.gifFifo": "GIF FIFO", diff --git a/platforms/android/app/src/main/assets/i18n/zh-TW.json b/platforms/android/app/src/main/assets/i18n/zh-TW.json index 4e1855898b..342559199a 100644 --- a/platforms/android/app/src/main/assets/i18n/zh-TW.json +++ b/platforms/android/app/src/main/assets/i18n/zh-TW.json @@ -514,7 +514,6 @@ "perf.fix.eeTiming": "EE Timing", "perf.fix.extraXgkick": "Extra XGKICK", "perf.fix.fmvSoftware": "FMV Software", - "perf.fix.fpuMultiply": "FPU Multiply", "perf.fix.fullVu0Sync": "Full VU0 Sync", "perf.fix.gamedbFixes": "GameDB Fixes", "perf.fix.gifFifo": "GIF FIFO", diff --git a/platforms/android/app/src/main/java/com/armsx2/config/Settings.kt b/platforms/android/app/src/main/java/com/armsx2/config/Settings.kt index 0aac6e5a17..edfc7dee42 100644 --- a/platforms/android/app/src/main/java/com/armsx2/config/Settings.kt +++ b/platforms/android/app/src/main/java/com/armsx2/config/Settings.kt @@ -69,8 +69,15 @@ data class Settings( val eeCycleRate: Int = 0, /** EmuCore/Speedhacks/EECycleSkip — 0..3. 0 = no skip. */ val eeCycleSkip: Int = 0, - /** EE/FPU clamp mode — 0 None / 1 Normal / 2 Extra / 3 Full (PCSX2 default Normal). - * Unpacks to EmuCore/CPU/Recompiler fpuOverflow/fpuExtraOverflow/fpuFullMode. */ + /** EE/FPU clamp mode — 0 None / 1 Normal / 2 Extra / 3 Full / 4 Exact + * (PCSX2 default Normal). Unpacks to EmuCore/CPU/Recompiler + * fpuOverflow/fpuExtraOverflow/fpuFullMode/fpuExactMode. + * 4 is Full plus the rest of the EE multiplier's one-ULP deficit and a + * divide/sqrt/rsqrt that runs the unit's own recurrence out of line, so it + * costs a call per divide. ⚠️ The GameDB overwrites this whole tier for any + * title carrying an eeClampMode entry, and an entry below 4 CLEARS the + * exact bit — so on those titles the choice is inert unless game fixes are + * off. Keep any bound in the pickers in sync with this list. */ val eeClampMode: Int = 1, /** VU clamp mode — 0 None / 1 Normal / 2 Extra / 3 Extra+Sign (PCSX2 default Normal). * Unpacks to vu0/vu1 Overflow/ExtraOverflow/SignOverflow. */ @@ -207,8 +214,6 @@ data class Settings( val gamefixInstantDma: Boolean = false, /** EmuCore/Gamefixes/BlitInternalFPSHack. */ val gamefixBlitInternalFps: Boolean = false, - /** EmuCore/Gamefixes/FpuMulHack — Tales of Destiny. */ - val gamefixFpuMul: Boolean = false, /** EmuCore/Gamefixes/OPHFlagHack — Bleach Blade Battlers. */ val gamefixOphFlag: Boolean = false, /** EmuCore/Gamefixes/GIFFIFOHack — emulate the GIF FIFO (Test Drive Unlimited). */ @@ -756,6 +761,10 @@ data class Settings( put("EmuCore/CPU/Recompiler", "fpuOverflow", "bool", (eeClampMode >= 1).toString()) put("EmuCore/CPU/Recompiler", "fpuExtraOverflow", "bool", (eeClampMode >= 2).toString()) put("EmuCore/CPU/Recompiler", "fpuFullMode", "bool", (eeClampMode >= 3).toString()) + // The four are cumulative and emucore validates them as such: an + // inconsistent set is silently reset to defaults on load rather than + // rejected, so all four go out together or none of them mean anything. + put("EmuCore/CPU/Recompiler", "fpuExactMode", "bool", (eeClampMode >= 4).toString()) for (vu in arrayOf("vu0", "vu1")) { put("EmuCore/CPU/Recompiler", "${vu}Overflow", "bool", (vuClampMode >= 1).toString()) put("EmuCore/CPU/Recompiler", "${vu}ExtraOverflow", "bool", (vuClampMode >= 2).toString()) @@ -846,7 +855,6 @@ data class Settings( put("EmuCore/Gamefixes", "EETimingHack", "bool", gamefixEETiming.toString()) put("EmuCore/Gamefixes", "InstantDMAHack", "bool", gamefixInstantDma.toString()) put("EmuCore/Gamefixes", "BlitInternalFPSHack", "bool", gamefixBlitInternalFps.toString()) - put("EmuCore/Gamefixes", "FpuMulHack", "bool", gamefixFpuMul.toString()) put("EmuCore/Gamefixes", "OPHFlagHack", "bool", gamefixOphFlag.toString()) put("EmuCore/Gamefixes", "GIFFIFOHack", "bool", gamefixGifFifo.toString()) put("EmuCore/Gamefixes", "DMABusyHack", "bool", gamefixDmaBusy.toString()) @@ -988,14 +996,18 @@ data class Settings( fun floatAt(key: String): Float? = ini[key]?.toFloatOrNull() fun strAt(key: String): String? = ini[key] - // EE/FPU clamp (0 None / 1 Normal / 2 Extra / 3 Full) is packed by applyTo into - // three cumulative bool keys (fpuOverflow>=1, fpuExtraOverflow>=2, fpuFullMode>=3). + // EE/FPU clamp (0 None / 1 Normal / 2 Extra / 3 Full / 4 Exact) is packed by + // applyTo into four cumulative bool keys (fpuOverflow>=1, fpuExtraOverflow>=2, + // fpuFullMode>=3, fpuExactMode>=4). Read them back highest-first, and treat the + // exact key's absence as an older core rather than as mode 3 — a build without it + // never wrote the key, and inferring 3 there would silently demote the setting. val eeClamp = run { val fo = boolAt("EmuCore/CPU/Recompiler/fpuOverflow") val fe = boolAt("EmuCore/CPU/Recompiler/fpuExtraOverflow") val ff = boolAt("EmuCore/CPU/Recompiler/fpuFullMode") - if (fo == null && fe == null && ff == null) this.eeClampMode - else if (ff == true) 3 else if (fe == true) 2 else if (fo == true) 1 else 0 + val fx = boolAt("EmuCore/CPU/Recompiler/fpuExactMode") + if (fo == null && fe == null && ff == null && fx == null) this.eeClampMode + else if (fx == true) 4 else if (ff == true) 3 else if (fe == true) 2 else if (fo == true) 1 else 0 } // VU clamp (0 None / 1 Normal / 2 Extra / 3 Extra+Sign) — same packing on vu0* // (applyTo writes vu0 and vu1 identically, so reading vu0 recovers the mode). @@ -1064,7 +1076,6 @@ data class Settings( gamefixEETiming = boolAt("EmuCore/Gamefixes/EETimingHack") ?: this.gamefixEETiming, gamefixInstantDma = boolAt("EmuCore/Gamefixes/InstantDMAHack") ?: this.gamefixInstantDma, gamefixBlitInternalFps = boolAt("EmuCore/Gamefixes/BlitInternalFPSHack") ?: this.gamefixBlitInternalFps, - gamefixFpuMul = boolAt("EmuCore/Gamefixes/FpuMulHack") ?: this.gamefixFpuMul, gamefixOphFlag = boolAt("EmuCore/Gamefixes/OPHFlagHack") ?: this.gamefixOphFlag, gamefixGifFifo = boolAt("EmuCore/Gamefixes/GIFFIFOHack") ?: this.gamefixGifFifo, gamefixDmaBusy = boolAt("EmuCore/Gamefixes/DMABusyHack") ?: this.gamefixDmaBusy, @@ -1681,7 +1692,6 @@ data class Settings( put("gamefixEETiming", gamefixEETiming) put("gamefixInstantDma", gamefixInstantDma) put("gamefixBlitInternalFps", gamefixBlitInternalFps) - put("gamefixFpuMul", gamefixFpuMul) put("gamefixOphFlag", gamefixOphFlag) put("gamefixGifFifo", gamefixGifFifo) put("gamefixDmaBusy", gamefixDmaBusy) @@ -1960,7 +1970,6 @@ data class Settings( gamefixEETiming = json.optBoolean("gamefixEETiming", def.gamefixEETiming), gamefixInstantDma = json.optBoolean("gamefixInstantDma", def.gamefixInstantDma), gamefixBlitInternalFps = json.optBoolean("gamefixBlitInternalFps", def.gamefixBlitInternalFps), - gamefixFpuMul = json.optBoolean("gamefixFpuMul", def.gamefixFpuMul), gamefixOphFlag = json.optBoolean("gamefixOphFlag", def.gamefixOphFlag), gamefixGifFifo = json.optBoolean("gamefixGifFifo", def.gamefixGifFifo), gamefixDmaBusy = json.optBoolean("gamefixDmaBusy", def.gamefixDmaBusy), @@ -2214,7 +2223,6 @@ data class Settings( if (current.gamefixEETiming != base.gamefixEETiming) j.put("gamefixEETiming", current.gamefixEETiming) if (current.gamefixInstantDma != base.gamefixInstantDma) j.put("gamefixInstantDma", current.gamefixInstantDma) if (current.gamefixBlitInternalFps != base.gamefixBlitInternalFps) j.put("gamefixBlitInternalFps", current.gamefixBlitInternalFps) - if (current.gamefixFpuMul != base.gamefixFpuMul) j.put("gamefixFpuMul", current.gamefixFpuMul) if (current.gamefixOphFlag != base.gamefixOphFlag) j.put("gamefixOphFlag", current.gamefixOphFlag) if (current.gamefixGifFifo != base.gamefixGifFifo) j.put("gamefixGifFifo", current.gamefixGifFifo) if (current.gamefixDmaBusy != base.gamefixDmaBusy) j.put("gamefixDmaBusy", current.gamefixDmaBusy) @@ -2456,7 +2464,6 @@ data class Settings( gamefixEETiming = if (overrides.has("gamefixEETiming")) overrides.getBoolean("gamefixEETiming") else base.gamefixEETiming, gamefixInstantDma = if (overrides.has("gamefixInstantDma")) overrides.getBoolean("gamefixInstantDma") else base.gamefixInstantDma, gamefixBlitInternalFps = if (overrides.has("gamefixBlitInternalFps")) overrides.getBoolean("gamefixBlitInternalFps") else base.gamefixBlitInternalFps, - gamefixFpuMul = if (overrides.has("gamefixFpuMul")) overrides.getBoolean("gamefixFpuMul") else base.gamefixFpuMul, gamefixOphFlag = if (overrides.has("gamefixOphFlag")) overrides.getBoolean("gamefixOphFlag") else base.gamefixOphFlag, gamefixGifFifo = if (overrides.has("gamefixGifFifo")) overrides.getBoolean("gamefixGifFifo") else base.gamefixGifFifo, gamefixDmaBusy = if (overrides.has("gamefixDmaBusy")) overrides.getBoolean("gamefixDmaBusy") else base.gamefixDmaBusy, diff --git a/platforms/android/app/src/main/java/com/armsx2/i18n/I18n.kt b/platforms/android/app/src/main/java/com/armsx2/i18n/I18n.kt index 05fd373d20..e53062fce9 100644 --- a/platforms/android/app/src/main/java/com/armsx2/i18n/I18n.kt +++ b/platforms/android/app/src/main/java/com/armsx2/i18n/I18n.kt @@ -1019,6 +1019,7 @@ val EN: Map = mapOf( "patches.widescreen.description" to "Automatically applies a widescreen patch to EVERY game that has one — you don't pick them per game. Games that weren't built for 16:9 can come out stretched, cropped, or missing on-screen text, so turn this off if a game looks wrong. Applies at boot — restart the game after changing this.", "perf.advancedSpeedhacks.legend" to "MTVU - runs VU1 on its own thread (faster on multi-core); can break games needing tight EE/VU1 sync.\n", "perf.advancedSpeedhacks.title" to "Advanced Speedhacks", + "perf.clamp.exact" to "Exact", "perf.clamp.extra" to "Extra", "perf.clamp.extraSign" to "Extra+Sign", "perf.clamp.full" to "Full", @@ -1039,7 +1040,7 @@ val EN: Map = mapOf( "perf.eeCycleRate.label" to "EE Cycle Rate", "perf.eeCycleSkip.description" to "Skips EE cycles for speed. Can cause stutter, physics bugs, or crashes.", "perf.eeCycleSkip.label" to "EE Cycle Skip", - "perf.eeFpuClamping.description" to "FPU overflow/rounding accuracy. Restart the game to apply.", + "perf.eeFpuClamping.description" to "FPU overflow/rounding accuracy. Exact reproduces the EE's multiplier and divider bit-for-bit, for the few games that check results exactly — it is the slowest setting by some way. Games that carry their own clamping entry in the game database keep that entry instead of this. Restart the game to apply.", "perf.eeFpuClamping.label" to "EE/FPU Clamping", "perf.eeFpuRoundMode.description" to "EE FPU float rounding. Chop (toward zero) is the PS2 default — change only if a game needs it. Restart the game to apply.", "perf.eeFpuRoundMode.label" to "EE FPU Round Mode", @@ -1048,7 +1049,6 @@ val EN: Map = mapOf( "perf.fix.eeTiming" to "EE Timing", "perf.fix.extraXgkick" to "Extra XGKICK", "perf.fix.fmvSoftware" to "FMV Software", - "perf.fix.fpuMultiply" to "FPU Multiply", "perf.fix.fullVu0Sync" to "Full VU0 Sync", "perf.fix.compatPatches" to "Compatibility Patches", "perf.fix.compatPatches.desc" to "Applies the per-game compatibility patches bundled with ARMSX2 — the fixes some games need to boot or render correctly. Same idea as GameDB Fixes above, and safe to leave on. Does NOT enable widescreen or cheats; those live in the Patches screen. Takes effect on the next boot.", @@ -1073,7 +1073,6 @@ val EN: Map = mapOf( "perf.fix.eeTiming.desc" to "Tweaks EE timing for the handful of games sensitive to it (e.g. Digital Devil Saga, SSX On Tour). Off by default.", "perf.fix.instantDma.desc" to "Completes certain DMA transfers instantly, fixing missing text or graphics in games like Fire Pro Wrestling Returns.", "perf.fix.blitFps.desc" to "Corrects the internal FPS reading so in-game and emulated frame counters are accurate in games that mis-report it.", - "perf.fix.fpuMultiply.desc" to "Uses a more accurate FPU multiply, fixing games that rely on exact float math (e.g. Tales of Destiny).", "perf.fix.ophFlag.desc" to "Emulates the VU0 OPH flag, fixing hangs or missing graphics in Bleach Blade Battlers and some Tri-Ace games.", "perf.fix.gifFifo.desc" to "Emulates the GIF FIFO accurately, fixing graphical glitches in games like FIFA Street 2 and Hot Wheels.", "perf.fix.dmaBusy.desc" to "Delays the VIF1 DMA busy flag, fixing hangs in games such as Mana Khemia and Metal Saga.", diff --git a/platforms/android/app/src/main/java/com/armsx2/ui/emulation/EmulationMenuScreen.kt b/platforms/android/app/src/main/java/com/armsx2/ui/emulation/EmulationMenuScreen.kt index e874d0c2ca..c455b99fd7 100644 --- a/platforms/android/app/src/main/java/com/armsx2/ui/emulation/EmulationMenuScreen.kt +++ b/platforms/android/app/src/main/java/com/armsx2/ui/emulation/EmulationMenuScreen.kt @@ -1060,7 +1060,7 @@ private fun PerformancePane(state: EmulationMenuUiState, viewModel: EmulationMen ) HorizontalOptions( title = str("perf.eeFpuClamping.label"), - options = listOf(str("perf.clamp.none"), str("perf.clamp.normal"), str("perf.clamp.extra"), str("perf.clamp.full")) + options = listOf(str("perf.clamp.none"), str("perf.clamp.normal"), str("perf.clamp.extra"), str("perf.clamp.full"), str("perf.clamp.exact")) .mapIndexed { index, label -> index to label }, selected = settings.eeClampMode, onSelect = { value -> viewModel.updateSettings { it.copy(eeClampMode = value) } }, diff --git a/platforms/android/app/src/main/java/com/armsx2/ui/settings/FixesTab.kt b/platforms/android/app/src/main/java/com/armsx2/ui/settings/FixesTab.kt index 183797eb30..f042b00e76 100644 --- a/platforms/android/app/src/main/java/com/armsx2/ui/settings/FixesTab.kt +++ b/platforms/android/app/src/main/java/com/armsx2/ui/settings/FixesTab.kt @@ -473,7 +473,6 @@ fun FixesTab(state: MutableState) { ToggleRow(str("perf.fix.eeTiming"), s.gamefixEETiming, description = str("perf.fix.eeTiming.desc")) { apply(s.copy(enableGameFixes = true, gamefixEETiming = it)) } ToggleRow(str("perf.fix.instantDma"), s.gamefixInstantDma, description = str("perf.fix.instantDma.desc")) { apply(s.copy(enableGameFixes = true, gamefixInstantDma = it)) } ToggleRow(str("perf.fix.blitFps"), s.gamefixBlitInternalFps, description = str("perf.fix.blitFps.desc")) { apply(s.copy(enableGameFixes = true, gamefixBlitInternalFps = it)) } - ToggleRow(str("perf.fix.fpuMultiply"), s.gamefixFpuMul, description = str("perf.fix.fpuMultiply.desc")) { apply(s.copy(enableGameFixes = true, gamefixFpuMul = it)) } ToggleRow(str("perf.fix.ophFlag"), s.gamefixOphFlag, description = str("perf.fix.ophFlag.desc")) { apply(s.copy(enableGameFixes = true, gamefixOphFlag = it)) } ToggleRow(str("perf.fix.gifFifo"), s.gamefixGifFifo, description = str("perf.fix.gifFifo.desc")) { apply(s.copy(enableGameFixes = true, gamefixGifFifo = it)) } ToggleRow(str("perf.fix.dmaBusy"), s.gamefixDmaBusy, description = str("perf.fix.dmaBusy.desc")) { apply(s.copy(enableGameFixes = true, gamefixDmaBusy = it)) } diff --git a/platforms/android/app/src/main/java/com/armsx2/ui/settings/PerformanceTab.kt b/platforms/android/app/src/main/java/com/armsx2/ui/settings/PerformanceTab.kt index 8f8f1ac0d4..6b228f8fd4 100644 --- a/platforms/android/app/src/main/java/com/armsx2/ui/settings/PerformanceTab.kt +++ b/platforms/android/app/src/main/java/com/armsx2/ui/settings/PerformanceTab.kt @@ -244,8 +244,8 @@ fun PerformanceTab(state: MutableState) { // at a speed cost. Needs a recompiler reset, so restart the game. SegmentedRow( label = str("perf.eeFpuClamping.label"), - options = listOf(str("perf.clamp.none"), str("perf.clamp.normal"), str("perf.clamp.extra"), str("perf.clamp.full")), - selectedIndex = s.eeClampMode.coerceIn(0, 3), + options = listOf(str("perf.clamp.none"), str("perf.clamp.normal"), str("perf.clamp.extra"), str("perf.clamp.full"), str("perf.clamp.exact")), + selectedIndex = s.eeClampMode.coerceIn(0, 4), description = str("perf.eeFpuClamping.description"), onChange = { apply(s.copy(eeClampMode = it)) }, ) diff --git a/platforms/android/app/src/main/java/com/armsx2/ui/settingshub/SettingsResetFields.kt b/platforms/android/app/src/main/java/com/armsx2/ui/settingshub/SettingsResetFields.kt index 62f711f7c0..a29aa8f4bf 100644 --- a/platforms/android/app/src/main/java/com/armsx2/ui/settingshub/SettingsResetFields.kt +++ b/platforms/android/app/src/main/java/com/armsx2/ui/settingshub/SettingsResetFields.kt @@ -73,7 +73,7 @@ internal val SETTINGS_CATEGORY_FIELDS: Map> = map // from Performance and from the retired Recompiler tab. SettingsCategory.Advanced to listOf( "enableFastBoot", "enableGameFixes", "pineEnabled", "pineSlot", - "gamefixBlitInternalFps", "gamefixDmaBusy", "gamefixEETiming", "gamefixFpuMul", + "gamefixBlitInternalFps", "gamefixDmaBusy", "gamefixEETiming", "gamefixFullVu0Sync", "gamefixGifFifo", "gamefixGoemonTlb", "gamefixIbit", "gamefixInstantDma", "gamefixOphFlag", "gamefixSkipMpeg", "gamefixSoftwareRendererFmv", "gamefixVif1Stall", "gamefixVuAddSub", diff --git a/platforms/android/app/src/main/java/com/armsx2/ui/settingshub/SettingsSearchIndex.kt b/platforms/android/app/src/main/java/com/armsx2/ui/settingshub/SettingsSearchIndex.kt index 6b4f259880..bcf3342d63 100644 --- a/platforms/android/app/src/main/java/com/armsx2/ui/settingshub/SettingsSearchIndex.kt +++ b/platforms/android/app/src/main/java/com/armsx2/ui/settingshub/SettingsSearchIndex.kt @@ -34,7 +34,6 @@ internal val SETTINGS_SEARCH_INDEX: List = listOf( SettingsSearchEntry("perf.fix.eeTiming", true, SettingsCategory.Advanced), SettingsSearchEntry("perf.fix.instantDma", true, SettingsCategory.Advanced), SettingsSearchEntry("perf.fix.blitFps", true, SettingsCategory.Advanced), - SettingsSearchEntry("perf.fix.fpuMultiply", true, SettingsCategory.Advanced), SettingsSearchEntry("perf.fix.ophFlag", true, SettingsCategory.Advanced), SettingsSearchEntry("perf.fix.gifFifo", true, SettingsCategory.Advanced), SettingsSearchEntry("perf.fix.dmaBusy", true, SettingsCategory.Advanced), diff --git a/platforms/ios/app/src/main/cpp/ARMSX2Bridge.mm b/platforms/ios/app/src/main/cpp/ARMSX2Bridge.mm index 012bb0f725..fb53d8ffb0 100644 --- a/platforms/ios/app/src/main/cpp/ARMSX2Bridge.mm +++ b/platforms/ios/app/src/main/cpp/ARMSX2Bridge.mm @@ -1945,7 +1945,7 @@ static void ARMSX2ApplyPerGameSettingsOverrides(NSMutableDictionary* perGameFixes = [NSMutableDictionary dictionary]; static constexpr const char* kARMSX2GameFixKeys[] = { - "VuAddSubHack", "FpuMulHack", "XgKickHack", "EETimingHack", "InstantDMAHack", + "VuAddSubHack", "XgKickHack", "EETimingHack", "InstantDMAHack", "SoftwareRendererFMVHack", "SkipMPEGHack", "OPHFlagHack", "DMABusyHack", "VIF1StallHack", "GIFFIFOHack", "GoemonTlbHack", "IbitHack", "VUSyncHack", "VUOverflowHack", "BlitInternalFPSHack", "FullVU0SyncHack" diff --git a/platforms/ios/app/src/main/swift/Models/SettingsStore+GameFixes.swift b/platforms/ios/app/src/main/swift/Models/SettingsStore+GameFixes.swift index ffc59e6d42..3c78c6da3e 100644 --- a/platforms/ios/app/src/main/swift/Models/SettingsStore+GameFixes.swift +++ b/platforms/ios/app/src/main/swift/Models/SettingsStore+GameFixes.swift @@ -7,7 +7,6 @@ extension SettingsStore { /// Manual EmuCore/Gamefixes toggles, in display order. static let gameFixOptions: [GameFixOption] = [ .init(key: "VuAddSubHack", label: "VU Add-Sub Hack"), - .init(key: "FpuMulHack", label: "FPU Multiply Hack"), .init(key: "XgKickHack", label: "Extra XGKICK Hack"), .init(key: "EETimingHack", label: "EE Timing Hack"), .init(key: "InstantDMAHack", label: "Instant DMA Hack"), diff --git a/tests/ctest/core/Arm64JitBuffer.h b/tests/ctest/core/Arm64JitBuffer.h new file mode 100644 index 0000000000..1622447dd6 --- /dev/null +++ b/tests/ctest/core/Arm64JitBuffer.h @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2026 ARMSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include +#include + +// RAII executable buffer, allocated exactly like PCSX2's macOS JIT regions +// (SharedMemoryMappingArea::Create with jit=true): MAP_JIT so that +// pthread_jit_write_protect_np can flip it between writable and executable. +// +// Emit into it through the recompilers' own lifecycle — armSetAsmPtr, then +// armStartBlock/armEndBlock, which toggle the write protection and flush the +// I-cache — and call the returned pointer. +class JitBuffer +{ +public: + explicit JitBuffer(size_t size) + : m_size(size) + { + int flags = MAP_ANONYMOUS | MAP_PRIVATE; +#ifdef __APPLE__ + flags |= MAP_JIT; +#endif + void* ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE | PROT_EXEC, flags, -1, 0); + m_ptr = (ptr == MAP_FAILED) ? nullptr : ptr; + } + + ~JitBuffer() + { + if (m_ptr) + munmap(m_ptr, m_size); + } + + JitBuffer(const JitBuffer&) = delete; + JitBuffer& operator=(const JitBuffer&) = delete; + + void* ptr() const { return m_ptr; } + size_t size() const { return m_size; } + +private: + void* m_ptr = nullptr; + size_t m_size; +}; diff --git a/tests/ctest/core/CMakeLists.txt b/tests/ctest/core/CMakeLists.txt index fcda445092..57569a0f64 100644 --- a/tests/ctest/core/CMakeLists.txt +++ b/tests/ctest/core/CMakeLists.txt @@ -1,4 +1,7 @@ add_pcsx2_test(core_test + ee_fpu_regfile_tests.cpp + ee_clamp_mode_tests.cpp + ee_fpu_reloc_tests.cpp patch_tests.cpp savestate_legacy_tests.cpp MockMemoryInterface.h diff --git a/tests/ctest/core/arm64_emit_test.cpp b/tests/ctest/core/arm64_emit_test.cpp index 615526270d..8f8d00dcda 100644 --- a/tests/ctest/core/arm64_emit_test.cpp +++ b/tests/ctest/core/arm64_emit_test.cpp @@ -19,6 +19,8 @@ #include "Config.h" #include "vtlb.h" +#include "Arm64JitBuffer.h" + #include #include @@ -27,47 +29,8 @@ #include #include -#include - using namespace vixl::aarch64; -namespace -{ - // RAII executable buffer, allocated exactly like PCSX2's macOS JIT regions - // (SharedMemoryMappingArea::Create with jit=true): MAP_JIT so that - // pthread_jit_write_protect_np can flip it between writable and executable. - class JitBuffer - { - public: - explicit JitBuffer(size_t size) - : m_size(size) - { - int flags = MAP_ANONYMOUS | MAP_PRIVATE; -#ifdef __APPLE__ - flags |= MAP_JIT; -#endif - void* ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE | PROT_EXEC, flags, -1, 0); - m_ptr = (ptr == MAP_FAILED) ? nullptr : ptr; - } - - ~JitBuffer() - { - if (m_ptr) - munmap(m_ptr, m_size); - } - - JitBuffer(const JitBuffer&) = delete; - JitBuffer& operator=(const JitBuffer&) = delete; - - void* ptr() const { return m_ptr; } - size_t size() const { return m_size; } - - private: - void* m_ptr = nullptr; - size_t m_size; - }; -} // namespace - // int add(int a, int b) { return a + b; } // Args arrive in w0/w1, return value in w0 (AAPCS64). TEST(Arm64Emit, AddTwoArgs) diff --git a/tests/ctest/core/ee_clamp_mode_tests.cpp b/tests/ctest/core/ee_clamp_mode_tests.cpp new file mode 100644 index 0000000000..1f9d00b2e7 --- /dev/null +++ b/tests/ctest/core/ee_clamp_mode_tests.cpp @@ -0,0 +1,126 @@ +// SPDX-FileCopyrightText: 2026 ARMSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// The EE clamp mode is stored as four ordered bools and used as a number, in +// three places that have to agree: Get/SetEEClampMode, the GameDatabase's own +// unpacking of eeClampMode, and ApplySanityCheck, which rejects a mode whose +// lower bits are not set. +// +// Mode 4 is the reason these are pinned: adding a mode on top of three that +// had gone unasserted is the change that leaves a `>= 3` where a `>= 4` was +// meant. + +#include + +#include "Config.h" +#include "GameDatabase.h" + +namespace +{ + +struct ModeBits +{ + bool overflow, extra, full, exact; +}; + +ModeBits BitsOf(const Pcsx2Config::RecompilerOptions& r) +{ + return {r.fpuOverflow, r.fpuExtraOverflow, r.fpuFullMode, r.fpuExactMode}; +} + +// What each mode number means as a set of bits. This is the table the GameDB, +// the INI and the picker all encode and decode through. +constexpr ModeBits kModes[] = { + {false, false, false, false}, // 0 none + {true, false, false, false}, // 1 normal + {true, true, false, false}, // 2 extra + preserve sign + {true, true, true, false}, // 3 full + {true, true, true, true}, // 4 exact +}; + +bool Same(const ModeBits& a, const ModeBits& b) +{ + return a.overflow == b.overflow && a.extra == b.extra && a.full == b.full && + a.exact == b.exact; +} + +} // namespace + +TEST(EeClampMode, SetAndGetRoundTripEveryMode) +{ + for (u32 mode = 0; mode < std::size(kModes); mode++) + { + Pcsx2Config::RecompilerOptions r; + r.SetEEClampMode(mode); + EXPECT_TRUE(Same(BitsOf(r), kModes[mode])) << "mode " << mode; + EXPECT_EQ(r.GetEEClampMode(), mode); + } +} + +TEST(EeClampMode, TheGameDatabaseUnpacksTheSameBits) +{ + for (u32 mode = 0; mode < std::size(kModes); mode++) + { + GameDatabaseSchema::GameEntry entry; + entry.eeClampMode = static_cast(mode); + + Pcsx2Config config; + // Something other than the mode under test, so a no-op apply fails. + config.Cpu.Recompiler.SetEEClampMode(mode == 1 ? 4 : 1); + entry.applyGameFixes(config, true); + + EXPECT_EQ(config.Cpu.Recompiler.GetEEClampMode(), mode); + EXPECT_TRUE(Same(BitsOf(config.Cpu.Recompiler), kModes[mode])) + << "mode " << mode; + } +} + +// Undefined is the "no entry" value and must leave the config alone, or every +// game without a clampModes block would be forced to whatever 0 means. +TEST(EeClampMode, AnUndefinedGameDbEntryDoesNotMoveTheMode) +{ + GameDatabaseSchema::GameEntry entry; + ASSERT_EQ(entry.eeClampMode, GameDatabaseSchema::ClampMode::Undefined); + + Pcsx2Config config; + config.Cpu.Recompiler.SetEEClampMode(2); + entry.applyGameFixes(config, true); + EXPECT_EQ(config.Cpu.Recompiler.GetEEClampMode(), 2u); +} + +// ApplySanityCheck's contract: a whole mode survives, and a set of bits that is +// not one is thrown away rather than half-honoured. The default is mode 1, so +// the rejected configs land there. +TEST(EeClampMode, SanityCheckKeepsWholeModesAndRejectsGaps) +{ + for (u32 mode = 0; mode < std::size(kModes); mode++) + { + Pcsx2Config::RecompilerOptions r; + r.SetEEClampMode(mode); + r.ApplySanityCheck(); + EXPECT_EQ(r.GetEEClampMode(), mode) << "whole mode " << mode; + } + + const u32 fallback = Pcsx2Config::RecompilerOptions().GetEEClampMode(); + struct Gap + { + ModeBits bits; + const char* what; + }; + static const Gap kGaps[] = { + {{false, true, false, false}, "extra without overflow"}, + {{true, false, true, false}, "full without extra"}, + {{true, true, false, true}, "exact without full"}, + {{false, false, false, true}, "exact with nothing under it"}, + }; + for (const Gap& g : kGaps) + { + Pcsx2Config::RecompilerOptions r; + r.fpuOverflow = g.bits.overflow; + r.fpuExtraOverflow = g.bits.extra; + r.fpuFullMode = g.bits.full; + r.fpuExactMode = g.bits.exact; + r.ApplySanityCheck(); + EXPECT_EQ(r.GetEEClampMode(), fallback) << g.what; + } +} diff --git a/tests/ctest/core/ee_fpu_regfile_tests.cpp b/tests/ctest/core/ee_fpu_regfile_tests.cpp new file mode 100644 index 0000000000..3f24b163ec --- /dev/null +++ b/tests/ctest/core/ee_fpu_regfile_tests.cpp @@ -0,0 +1,149 @@ +// SPDX-FileCopyrightText: 2026 ARMSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ +// +// The FPR slot is 64 bits (R5900.h); the architectural register and the +// savestate block are 32. This pins that boundary. + +#include "R5900.h" +#include "Config.h" +#include "EeFpuFormat.h" + +#include "common/Pcsx2Defs.h" + +#include + +#include +#include + +namespace +{ + // Zero and negative zero, the denormal boundary, an ordinary value, the top + // binade, the EE maximum, and a raw CVT.W.S integer payload. + constexpr u32 kWords[] = { + 0x00000000, 0x80000000, 0x00000001, 0x807FFFFF, 0x00800000, 0x80800000, + 0x3F800000, 0x7F7FFFFF, 0x7F800000, 0xFF800000, 0x7FC00000, 0x7FFFFFFF, + 0xFFFFFFFF, 0x0000000A, 0x80000001, 0x4E800000, + }; +} // namespace + +// The 264-byte block every savestate carries, field for field. +TEST(EeFpuRegFile, TheWireBlockIsTheSavestateLayout) +{ + EXPECT_EQ(sizeof(fpuRegistersWire), 264u); + EXPECT_EQ(offsetof(fpuRegistersWire, fpr), 0u); + EXPECT_EQ(offsetof(fpuRegistersWire, fprc), 128u); + EXPECT_EQ(offsetof(fpuRegistersWire, ACC), 256u); + EXPECT_EQ(offsetof(fpuRegistersWire, ACCflag), 260u); +} + +// The stride the emitters compute from &fpuRegs.fpr[n] is the slot's size, so +// it has to be 8 bytes. SetWord writes all of it and Word reads back what went +// in. +TEST(EeFpuRegFile, TheAccessorOwnsTheWholeSlot) +{ + EXPECT_EQ(sizeof(FPRreg), 8u); + + for (u32 word : kWords) + { + FPRreg slot; + slot.UD = UINT64_C(0xA5A5A5A5) << 32 | 0x5A5A5A5A; + slot.SetWord(word); + EXPECT_EQ(slot.Word(), word) << std::hex << word; + + FPRreg fresh; + fresh.UD = 0; + fresh.SetWord(word); + EXPECT_EQ(slot.UD, fresh.UD) << std::hex << word; + } +} + +TEST(EeFpuRegFile, TheWireRoundTripIsExact) +{ + fpuRegisters saved; + std::memcpy(&saved, &fpuRegs, sizeof(fpuRegisters)); + + for (int i = 0; i < 32; i++) + { + fpuRegs.fpr[i].SetWord(kWords[i % std::size(kWords)] ^ static_cast(i)); + fpuRegs.fprc[i] = 0xC0DE0000u | static_cast(i); + } + fpuRegs.ACC.SetWord(0x7FFFFFFF); + fpuRegs.ACCflag = 1; + + fpuRegistersWire wire; + fpuRegsToWire(wire); + + for (int i = 0; i < 32; i++) + { + EXPECT_EQ(wire.fpr[i], fpuRegs.fpr[i].Word()) << "fpr" << i; + EXPECT_EQ(wire.fprc[i], fpuRegs.fprc[i]) << "fprc" << i; + } + EXPECT_EQ(wire.ACC, 0x7FFFFFFFu); + EXPECT_EQ(wire.ACCflag, 1u); + + // 0xA5 everywhere, so a load that writes only part of a slot leaves the + // rest of it set and the whole-slot comparison below catches it. + std::memset(&fpuRegs, 0xA5, sizeof(fpuRegisters)); + fpuRegsFromWire(wire); + + for (int i = 0; i < 32; i++) + { + FPRreg fresh; + fresh.UD = 0; + fresh.SetWord(wire.fpr[i]); + EXPECT_EQ(fpuRegs.fpr[i].UD, fresh.UD) << "fpr" << i; + EXPECT_EQ(fpuRegs.fprc[i], wire.fprc[i]) << "fprc" << i; + } + FPRreg fresh_acc; + fresh_acc.UD = 0; + fresh_acc.SetWord(0x7FFFFFFF); + EXPECT_EQ(fpuRegs.ACC.UD, fresh_acc.UD); + EXPECT_EQ(fpuRegs.ACCflag, 1u); + + std::memcpy(&fpuRegs, &saved, sizeof(fpuRegisters)); +} + +// The format follows eeClampMode, and the file is converted when the mode +// moves. Both directions: a sync that only ever widened would hand mode 0 a +// relocated file. +TEST(EeFpuRegFile, TheFormatFollowsTheClampModeInBothDirections) +{ + fpuRegisters saved; + std::memcpy(&saved, &fpuRegs, sizeof(fpuRegisters)); + const bool saved_mode = EmuConfig.Cpu.Recompiler.fpuFullMode; + + EmuConfig.Cpu.Recompiler.fpuFullMode = false; + eeFprSyncSlotFormat(); + ASSERT_FALSE(g_eeFprSlotsRelocated); + + for (int i = 0; i < 32; i++) + fpuRegs.fpr[i].SetWord(kWords[i % std::size(kWords)]); + fpuRegs.ACC.SetWord(0x7FFFFFFF); + EXPECT_EQ(fpuRegs.fpr[6].UD, UINT64_C(0x3F800000)) << "mode 0 keeps the word in the low half"; + + EmuConfig.Cpu.Recompiler.fpuFullMode = true; + eeFprSyncSlotFormat(); + EXPECT_TRUE(g_eeFprSlotsRelocated); + for (int i = 0; i < 32; i++) + { + const u32 word = kWords[i % std::size(kWords)]; + EXPECT_EQ(fpuRegs.fpr[i].Word(), word) << "fpr" << i; + EXPECT_EQ(fpuRegs.fpr[i].UD, eeFprWidenBits(word)) << "fpr" << i << " slot"; + } + EXPECT_EQ(fpuRegs.ACC.UD, eeFprWidenBits(0x7FFFFFFF)); + + EmuConfig.Cpu.Recompiler.fpuFullMode = false; + eeFprSyncSlotFormat(); + EXPECT_FALSE(g_eeFprSlotsRelocated); + for (int i = 0; i < 32; i++) + { + const u32 word = kWords[i % std::size(kWords)]; + EXPECT_EQ(fpuRegs.fpr[i].Word(), word) << "fpr" << i; + EXPECT_EQ(fpuRegs.fpr[i].UD, static_cast(word)) << "fpr" << i << " slot"; + } + EXPECT_EQ(fpuRegs.ACC.UD, UINT64_C(0x7FFFFFFF)); + + EmuConfig.Cpu.Recompiler.fpuFullMode = saved_mode; + eeFprSyncSlotFormat(); + std::memcpy(&fpuRegs, &saved, sizeof(fpuRegisters)); +} diff --git a/tests/ctest/core/ee_fpu_reloc_tests.cpp b/tests/ctest/core/ee_fpu_reloc_tests.cpp new file mode 100644 index 0000000000..8d1c386bea --- /dev/null +++ b/tests/ctest/core/ee_fpu_reloc_tests.cpp @@ -0,0 +1,419 @@ +// SPDX-FileCopyrightText: 2026 ARMSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ +// +// The EE FPR word <-> host double relocation (EeFpuFormat.h). +// +// Checked over all 2^32 words: one threaded pass fills the counters and each +// test reads one property out of it. The value law's reference is arithmetic, +// ldexp of the significand, rather than a second bit assembly. + +#include "EeFpuFormat.h" + +#include "common/Pcsx2Defs.h" + +#if defined(__aarch64__) +#include "arm64/AsmHelpers.h" +#include "Config.h" +#include "Arm64JitBuffer.h" +#endif + +#include + +#include +#include +#include +#include +#include +#include + +namespace +{ + // The EE value of a normal word, built by arithmetic. + double EeValueOf(u32 word) + { + const u32 exp = (word >> 23) & 0xFF; + const u32 man = word & 0x007FFFFF; + const double v = std::ldexp(1.0 + static_cast(man) / 8388608.0, static_cast(exp) - 127); + return (word & 0x80000000u) ? -v : v; + } + + u32 DoubleExponentField(u64 bits) { return static_cast((bits >> 52) & 0x7FF); } + u64 DoubleMantissaField(u64 bits) { return bits & 0x000FFFFFFFFFFFFFull; } + + // What one contiguous slice of the word space says about the format. + struct RelocScan + { + u64 round_trip_failures = 0; + u32 first_round_trip_failure = 0; + u64 value_failures = 0; + u32 first_value_failure = 0; + u64 normals_checked = 0; + u32 max_exponent_field = 0; + u32 max_exponent_witness = 0; + u64 denormal_mismatches = 0; + u32 first_denormal_mismatch = 0; + u64 denormals = 0; + + // Slices are merged in ascending word order, so the earlier slice's + // first failure is the whole scan's. + void Absorb(const RelocScan& later) + { + if (!round_trip_failures) + first_round_trip_failure = later.first_round_trip_failure; + if (!value_failures) + first_value_failure = later.first_value_failure; + if (!denormal_mismatches) + first_denormal_mismatch = later.first_denormal_mismatch; + round_trip_failures += later.round_trip_failures; + value_failures += later.value_failures; + denormal_mismatches += later.denormal_mismatches; + normals_checked += later.normals_checked; + denormals += later.denormals; + if (later.max_exponent_field > max_exponent_field) + { + max_exponent_field = later.max_exponent_field; + max_exponent_witness = later.max_exponent_witness; + } + } + }; + + RelocScan ScanWords(u64 begin, u64 end) + { + // 2^(e-127) for every EE exponent field, so the reference side of the + // value law costs a multiply rather than a libm call. + double pow2[256] = {}; + for (int e = 1; e <= 255; e++) + pow2[e] = std::ldexp(1.0, e - 127); + const double scale = std::ldexp(1.0, kEeFprScaleExp); + + RelocScan scan; + for (u64 i = begin; i < end; i++) + { + const u32 word = static_cast(i); + const u64 bits = eeFprWidenBits(word); + + if (eeFprNarrowBits(bits) != word) + { + if (!scan.round_trip_failures++) + scan.first_round_trip_failure = word; + } + + const u32 dexp = DoubleExponentField(bits); + if (dexp > scan.max_exponent_field) + { + scan.max_exponent_field = dexp; + scan.max_exponent_witness = word; + } + + const u32 eexp = (word >> 23) & 0xFF; + const u32 eman = word & 0x007FFFFF; + const bool ee_denormal = (eexp == 0) && (eman != 0); + const bool host_denormal = (dexp == 0) && (DoubleMantissaField(bits) != 0); + if (ee_denormal != host_denormal) + { + if (!scan.denormal_mismatches++) + scan.first_denormal_mismatch = word; + } + scan.denormals += ee_denormal ? 1 : 0; + + if (eexp == 0) + continue; // no normal value to compare against + + scan.normals_checked++; + double stored; + std::memcpy(&stored, &bits, sizeof(stored)); + const double magnitude = pow2[eexp] * (1.0 + static_cast(eman) / 8388608.0); + const double reference = (word & 0x80000000u) ? -magnitude : magnitude; + if (stored * scale != reference) + { + if (!scan.value_failures++) + scan.first_value_failure = word; + } + } + return scan; + } +} // namespace + +class EeFpuReloc : public ::testing::Test +{ +protected: + static void SetUpTestSuite() + { + unsigned slices = std::thread::hardware_concurrency(); + slices = std::clamp(slices, 1u, 16u); + const u64 per_slice = 0x100000000ull / slices; + + std::vector results(slices); + std::vector workers; + for (unsigned i = 0; i < slices; i++) + { + const u64 begin = i * per_slice; + const u64 end = (i + 1 == slices) ? 0x100000000ull : begin + per_slice; + workers.emplace_back([i, begin, end, &results] { results[i] = ScanWords(begin, end); }); + } + for (std::thread& worker : workers) + worker.join(); + for (const RelocScan& slice : results) + s_scan.Absorb(slice); + } + + static inline RelocScan s_scan; +}; + +// CVT.W.S parks a raw int32 in an FPR slot, so the relocation has to be +// lossless on bit patterns that are not sensible floats too. +TEST_F(EeFpuReloc, EveryWordSurvivesTheRoundTrip) +{ + EXPECT_EQ(s_scan.round_trip_failures, 0ull) + << "first failing word 0x" << std::hex << s_scan.first_round_trip_failure; +} + +// stored * 2^896 is the EE value the word denotes, uniformly. +TEST_F(EeFpuReloc, EveryNormalWordScalesToItsEeValue) +{ + EXPECT_EQ(s_scan.normals_checked, 4278190080ull) << "2^32 words less the 2^24 whose exponent field is 0"; + EXPECT_EQ(s_scan.value_failures, 0ull) + << "first failing word 0x" << std::hex << s_scan.first_value_failure; + + // Both ends of the range, readable without running the loop. + EXPECT_EQ(std::ldexp(eeFprWiden(0x00800000), kEeFprScaleExp), EeValueOf(0x00800000)); + EXPECT_EQ(std::ldexp(eeFprWiden(0x7FFFFFFF), kEeFprScaleExp), EeValueOf(0x7FFFFFFF)); + EXPECT_EQ(std::ldexp(eeFprWiden(0xFFFFFFFF), kEeFprScaleExp), EeValueOf(0xFFFFFFFF)); +} + +// The EE's smallest normal has to land on the host's, or FPCR.FZ flushes on a +// different boundary than the EE does. +TEST_F(EeFpuReloc, TheDenormalBoundaryIsTheHostOne) +{ + EXPECT_EQ(eeFprWidenBits(0x00800000), 0x0010000000000000ull) << "2^-126 must map to 0x1p-1022"; + EXPECT_EQ(eeFprWidenBits(0x80800000), 0x8010000000000000ull); + EXPECT_EQ(s_scan.denormal_mismatches, 0ull) + << "first mismatching word 0x" << std::hex << s_scan.first_denormal_mismatch; + EXPECT_EQ(s_scan.denormals, 16777214ull) << "2 * (2^23 - 1) words are EE denormals; the class must not be empty"; +} + +// Exponent 255 is an ordinary binade on the EE, so the host's 2047 has to be +// out of reach by construction rather than by clamping. +TEST_F(EeFpuReloc, NoWordReachesTheHostInfinityExponent) +{ + EXPECT_EQ(s_scan.max_exponent_field, 255u); + EXPECT_EQ(DoubleExponentField(eeFprWidenBits(s_scan.max_exponent_witness)), 255u) + << "witness 0x" << std::hex << s_scan.max_exponent_witness; + EXPECT_EQ(DoubleExponentField(eeFprWidenBits(0x7F800000)), 255u); + EXPECT_EQ(DoubleExponentField(eeFprWidenBits(0x7FFFFFFF)), 255u); +} + +#if defined(__aarch64__) + +using namespace vixl::aarch64; + +namespace +{ + // Sign x exponent x a handful of mantissas, the interesting words, and a + // pseudo-random tail. Every exponent field including 0 and 255 appears with + // both signs. + std::vector RelocSampleWords() + { + std::vector words = { + 0x00000000, 0x80000000, 0x00000001, 0x80000001, 0x007FFFFF, 0x807FFFFF, + 0x00800000, 0x80800000, 0x00800001, 0x3F800000, 0xBF800000, 0x3E800000, + 0x40490FDB, 0x3F490FDA, 0x7F7FFFFF, 0xFF7FFFFF, 0x7F800000, 0xFF800000, + 0x7FC00000, 0xFFC00000, 0x7FFFFFFF, 0xFFFFFFFF, 0x7FFFFFFE, 0x00000002}; + + static constexpr u32 kMantissas[] = {0, 1, 2, 0x2AA, 0x555, 0x400000, 0x7FFFFE, 0x7FFFFF}; + for (u32 sign = 0; sign < 2; sign++) + for (u32 exp = 0; exp < 256; exp++) + for (u32 man : kMantissas) + words.push_back((sign << 31) | (exp << 23) | man); + + u32 state = 0x9E3779B9; + for (int i = 0; i < 200000; i++) + { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + words.push_back(state); + } + return words; + } +} // namespace + +// The emitted sequences have to agree with the C++ helpers word for word: the +// two sides meet at every boundary the architectural word is observable at. +TEST(EeFpuRelocEmit, BothDirectionsAgreeWithTheHelpers) +{ + JitBuffer buf(4096); + ASSERT_NE(buf.ptr(), nullptr) << "MAP_JIT allocation failed"; + u8* const base = static_cast(buf.ptr()); + size_t used = 0; + + // u64 widen_reg(u32 word) + armSetAsmPtr(base + used, buf.size() - used, nullptr); + u8* const widen_reg = armStartBlock(); + const ptrdiff_t widen_reg_start = armAsm->GetCursorOffset(); + armEmitEeFprWiden(d0, w0, x8); + const ptrdiff_t widen_reg_bytes = armAsm->GetCursorOffset() - widen_reg_start; + armAsm->Fmov(x0, d0); + armAsm->Ret(); + used = static_cast(armEndBlock() - base); + + // u64 widen_mem(const u32* word) + armSetAsmPtr(base + used, buf.size() - used, nullptr); + u8* const widen_mem = armStartBlock(); + const ptrdiff_t widen_mem_start = armAsm->GetCursorOffset(); + armEmitEeFprWidenFromMem(d0, MemOperand(x0), x8); + const ptrdiff_t widen_mem_bytes = armAsm->GetCursorOffset() - widen_mem_start; + armAsm->Fmov(x0, d0); + armAsm->Ret(); + used = static_cast(armEndBlock() - base); + + // u64 narrow(u64 stored_bits) + armSetAsmPtr(base + used, buf.size() - used, nullptr); + u8* const narrow = armStartBlock(); + armAsm->Fmov(d0, x0); + const ptrdiff_t narrow_start = armAsm->GetCursorOffset(); + armEmitEeFprNarrow(x0, d0, x8); + const ptrdiff_t narrow_bytes = armAsm->GetCursorOffset() - narrow_start; + armAsm->Ret(); + used = static_cast(armEndBlock() - base); + + // void narrow_mem(u64 stored_bits, u32* out) + armSetAsmPtr(base + used, buf.size() - used, nullptr); + u8* const narrow_mem = armStartBlock(); + armAsm->Fmov(d0, x0); + const ptrdiff_t narrow_mem_start = armAsm->GetCursorOffset(); + armEmitEeFprNarrowToMem(MemOperand(x1), d0, x2, x8); + const ptrdiff_t narrow_mem_bytes = armAsm->GetCursorOffset() - narrow_mem_start; + armAsm->Ret(); + armEndBlock(); + + // A mask that stopped encoding as a logical immediate would be silently + // materialized instead, so pin the widths. + EXPECT_EQ(widen_reg_bytes, 3 * 4); + EXPECT_EQ(widen_mem_bytes, 4 * 4); + EXPECT_EQ(narrow_bytes, 3 * 4); + EXPECT_EQ(narrow_mem_bytes, 4 * 4); + + const auto widen_reg_fn = reinterpret_cast(widen_reg); + const auto widen_mem_fn = reinterpret_cast(widen_mem); + const auto narrow_fn = reinterpret_cast(narrow); + const auto narrow_mem_fn = reinterpret_cast(narrow_mem); + + const std::vector words = RelocSampleWords(); + u64 failures = 0; + u32 first_failure = 0; + for (const u32 word : words) + { + const u64 wide = eeFprWidenBits(word); + u32 out = 0; + narrow_mem_fn(wide, &out); + const bool ok = widen_reg_fn(word) == wide && widen_mem_fn(&word) == wide && + narrow_fn(wide) == static_cast(word) && out == word; + if (!ok && !failures++) + first_failure = word; + } + EXPECT_EQ(failures, 0ull) << "first failing word 0x" << std::hex << first_failure + << std::dec << " of " << words.size(); +} + +// The slot pair is the same relocation with memory and register swapped, so it +// has its own encodings to pin and its own composition to check. +TEST(EeFpuRelocEmit, TheSlotPairIsTheIdentityThroughMemory) +{ + JitBuffer buf(4096); + ASSERT_NE(buf.ptr(), nullptr) << "MAP_JIT allocation failed"; + u8* const base = static_cast(buf.ptr()); + size_t used = 0; + + // void store_slot(u32 word, u64* slot) + armSetAsmPtr(base + used, buf.size() - used, nullptr); + u8* const store = armStartBlock(); + const ptrdiff_t store_start = armAsm->GetCursorOffset(); + armEmitEeFprStoreSlotWord(MemOperand(x1), w0, x8); + const ptrdiff_t store_bytes = armAsm->GetCursorOffset() - store_start; + armAsm->Ret(); + used = static_cast(armEndBlock() - base); + + // u32 load_slot(const u64* slot) + armSetAsmPtr(base + used, buf.size() - used, nullptr); + u8* const load = armStartBlock(); + const ptrdiff_t load_start = armAsm->GetCursorOffset(); + armEmitEeFprLoadSlotWord(x0, MemOperand(x0), x8); + const ptrdiff_t load_bytes = armAsm->GetCursorOffset() - load_start; + armAsm->Ret(); + armEndBlock(); + + EXPECT_EQ(store_bytes, 3 * 4); + EXPECT_EQ(load_bytes, 3 * 4); + + const auto store_fn = reinterpret_cast(store); + const auto load_fn = reinterpret_cast(load); + + const std::vector words = RelocSampleWords(); + u64 failures = 0; + u32 first_failure = 0; + for (const u32 word : words) + { + u64 slot = 0; + store_fn(word, &slot); + if ((slot != eeFprWidenBits(word) || load_fn(&slot) != static_cast(word)) && !failures++) + first_failure = word; + } + EXPECT_EQ(failures, 0ull) << "first failing word 0x" << std::hex << first_failure + << std::dec << " of " << words.size(); +} + +// Underflow costs nothing here: FPCR.FZ flushes on the EE's own boundary as +// part of each op. Tested in emitted code, because a compiler folds constant +// arithmetic in the mode it was built with and not the one the msr installed. +TEST(EeFpuRelocEmit, FlushToZeroLandsOnTheEeDenormalBoundary) +{ + const u64 fpcr = Pcsx2Config::CpuOptions().FPUFPCR.bitmask; + + JitBuffer buf(4096); + ASSERT_NE(buf.ptr(), nullptr) << "MAP_JIT allocation failed"; + + // u64 mul(u64 a_bits, u64 b_bits). Multiplying two stored values squares + // the scale, so one operand is unscaled by 2^896 first. + armSetAsmPtr(buf.ptr(), buf.size(), nullptr); + u8* const code = armStartBlock(); + armAsm->Fmov(d0, x0); + armAsm->Fmov(d1, x1); + armAsm->Mov(x11, 0x77F0000000000000ull); // 2^896 + armAsm->Fmov(d2, x11); + armAsm->Mrs(x9, FPCR); + armAsm->Mov(x10, fpcr); + armAsm->Msr(FPCR, x10); + armAsm->Fmul(d0, d0, d2); + armAsm->Fmul(d0, d0, d1); + armAsm->Msr(FPCR, x9); + armAsm->Fmov(x0, d0); + armAsm->Ret(); + armEndBlock(); + + const auto raw = reinterpret_cast(code); + const auto mul = [raw](u32 a, u32 b) { + return eeFprNarrowBits(raw(eeFprWidenBits(a), eeFprWidenBits(b))); + }; + + // A denormal operand is gone before the multiply, sign kept. + EXPECT_EQ(mul(0x00000001, 0x3F800000), 0x00000000u); + EXPECT_EQ(mul(0x807FFFFF, 0x3F800000), 0x80000000u); + + // A product that underflows out of the EE's range goes the same way. + EXPECT_EQ(mul(0x00800000, 0x3F000000), 0x00000000u); // 2^-126 * 0.5 + EXPECT_EQ(mul(0x80800000, 0x3F000000), 0x80000000u); + + // 2^-126 itself is a normal double here: the half of the boundary a flush + // one binade too high would break. + EXPECT_EQ(mul(0x00800000, 0x3F800000), 0x00800000u); + EXPECT_EQ(mul(0x80800000, 0x3F800000), 0x80800000u); + + // The top of the EE's range multiplies as an ordinary number, where a host + // single returns an infinity the fast path's clamp folds back to FLT_MAX. + EXPECT_EQ(mul(0x7F7FFFFF, 0x40000000), 0x7FFFFFFFu); + EXPECT_EQ(mul(0x7FFFFFFF, 0x3F800000), 0x7FFFFFFFu); + EXPECT_EQ(mul(0x7F800000, 0x3F000000), 0x7F000000u); // 2^128 * 0.5 +} + +#endif // __aarch64__ diff --git a/tests/ctest/core/recompilers/CMakeLists.txt b/tests/ctest/core/recompilers/CMakeLists.txt index bc4b818d41..41fb34b33f 100644 --- a/tests/ctest/core/recompilers/CMakeLists.txt +++ b/tests/ctest/core/recompilers/CMakeLists.txt @@ -74,6 +74,7 @@ add_pcsx2_test(recompiler_tests vtlb_get_guest_address_tests.cpp vif_unpack_dynarec_tests.cpp ee_vu0_cop2_maxmini_conv_tests.cpp + ee_vu0_cop2_madd_clamp_tests.cpp ee_vu0_cop2_madda_bc_tests.cpp vu0_harness_validation_tests.cpp vu0_alu_upper_tests.cpp @@ -134,6 +135,7 @@ add_pcsx2_test(recompiler_tests ee_fpu_divunit_console_tests.cpp ee_fpu_divunit_exhaustive_tests.cpp ee_fpu_guarded_addsub_console_tests.cpp + ee_fpu_ou_rounding_console_tests.cpp ee_fpu_underflow_console_tests.cpp ee_fpu_top_binade_console_tests.cpp ee_fpu_zero_divisor_console_tests.cpp diff --git a/tests/ctest/core/recompilers/ee_fpu_divunit_console_tests.cpp b/tests/ctest/core/recompilers/ee_fpu_divunit_console_tests.cpp index b8c4551178..1ff305c19e 100644 --- a/tests/ctest/core/recompilers/ee_fpu_divunit_console_tests.cpp +++ b/tests/ctest/core/recompilers/ee_fpu_divunit_console_tests.cpp @@ -61,11 +61,12 @@ // two rsqrt rows it lost on -- 3895AEC3/4938608B and 43CD0CEB/365AF7C1, right // by cancellation under the old code and then not -- match again. // -// The `ieee_*` column stays as the other engine's column: both recompilers -// still take the host's correctly-rounded fdiv/fsqrt, so the divergence between -// the two engines is exactly the 68 cells where the console and correct -// rounding differ. TheFastPathStaysCorrectlyRoundedAndSaysSoHere asserts both -// halves of that. +// The `ieee_*` column is still the arm64 fast path's, which takes the host's +// correctly-rounded fdiv/fsqrt: its divergence from the model is the 68 cells +// where the console and correct rounding differ. +// TheFastPathStaysCorrectlyRoundedAndSaysSoHere asserts both halves of that, +// and ExactModeMatchesTheConsoleOnEveryRow asserts eeClampMode 4 against the +// console column. // // Evidence archive: captures/fpmatrix/rsqprobe.c, hw-rsq-run{1,2,3}.bin and // PROBE-divsqrt-rounding.md in the notes tree; the recurrence and the @@ -235,6 +236,20 @@ u32 RunInterp(const ConsoleRow& r, Op op) return h.GetFprBitsInterp(kFd); } +// eeClampMode 4 reaches the same two models through emitDivideUnitIsland, and +// holds the top binade as well, so it owes the table what the interpreter does. +u32 RunJitExact(const ConsoleRow& r, Op op) +{ + EeRecTestHarness h; + h.EnableCop1(); + h.EnableFpuExactMode(); + h.SetFprBits(kFs, r.fs); + h.SetFprBits(kFt, r.ft); + h.LoadProgram({ Encode(op) }); + h.RunJitNoDiff(); + return h.GetFprBitsJit(kFd); +} + } // namespace // --------------------------------------------------------------------------- @@ -342,11 +357,9 @@ TEST(EeFpuDivUnitConsole, SiliconIsOneUlpOffInTheseExactWays) // --------------------------------------------------------------------------- // 4. The fast path does not get the model. // -// Both recompilers inherit the host's correctly-rounded fdiv/fsqrt, so off -// the top binade the JIT must still produce the `ieee_*` column exactly. -// Whether it should take the model too is a separate call with its own -// measurement, as it was for the multiplier's deficit (eeMulRound is -// interpreter-only as well). +// It inherits the host's correctly-rounded fdiv/fsqrt, so off the top binade +// it must still produce the `ieee_*` column exactly. Only eeClampMode 4 gets +// the model, at the price section 5 charges. // --------------------------------------------------------------------------- TEST(EeFpuDivUnitConsole, TheFastPathStaysCorrectlyRoundedAndSaysSoHere) { @@ -381,3 +394,40 @@ TEST(EeFpuDivUnitConsole, TheFastPathStaysCorrectlyRoundedAndSaysSoHere) << "the interpreter is supposed to leave the fast path behind on exactly " "the rows where the console is not correctly rounded"; } + +// --------------------------------------------------------------------------- +// 5. eeClampMode 4 does get the model. +// +// Same table, same counts as section 2, because it is the same recurrence +// reached from the recompiler rather than from the interpreter. The +// followed_silicon count is what keeps this from passing on the 193 cells a +// correctly rounded divider would also get right. +// --------------------------------------------------------------------------- +TEST(EeFpuDivUnitConsole, ExactModeMatchesTheConsoleOnEveryRow) +{ + int match[3] = {}, total[3] = {}, followed_silicon = 0; + + for (const ConsoleRow& r : kRows) + { + for (Op op : { OP_SQRT, OP_DIV, OP_RSQRT }) + { + const u32 got = RunJitExact(r, op), con = Con(r, op), ieee = Ieee(r, op); + ++total[op]; + match[op] += (got == con); + followed_silicon += (got == con && con != ieee); + EXPECT_EQ(got, con) + << OpName(op) << " fs=" << std::hex << r.fs << " ft=" << r.ft + << " (" << r.what << "), correctly rounded would be " << ieee; + } + } + + EXPECT_EQ(match[OP_SQRT], 87) << "sqrt.s"; + EXPECT_EQ(match[OP_DIV], 87) << "div.s"; + EXPECT_EQ(match[OP_RSQRT], 87) << "rsqrt.s"; + EXPECT_EQ(total[OP_SQRT] + total[OP_DIV] + total[OP_RSQRT], 261); + + EXPECT_EQ(followed_silicon, 68) + << "cells where silicon and correct rounding differ AND mode 4 took " + "silicon's side -- 0 means the island stopped being emitted and this " + "test is passing on the rows that cannot tell the two apart"; +} diff --git a/tests/ctest/core/recompilers/ee_fpu_guarded_addsub_console_tests.cpp b/tests/ctest/core/recompilers/ee_fpu_guarded_addsub_console_tests.cpp index 3d2ab1f005..e1f18e123f 100644 --- a/tests/ctest/core/recompilers/ee_fpu_guarded_addsub_console_tests.cpp +++ b/tests/ctest/core/recompilers/ee_fpu_guarded_addsub_console_tests.cpp @@ -87,11 +87,15 @@ const char* FormName(Form f) } // One instruction, one engine. `acc` seeds the accumulator for the mad forms; -// the result is read from wherever the form writes. -u32 RunOne(Form f, u32 fs, u32 ft, u32 acc, bool jit) +// the result is read from wherever the form writes. `full` selects the DOUBLE +// path (eeClampMode 3), a different emitter from the fast one the rest of this +// file exercises. +u32 RunOne(Form f, u32 fs, u32 ft, u32 acc, bool jit, bool full = false) { EeRecTestHarness h; h.EnableCop1(); + if (full) + h.EnableFpuFullMode(); h.SetAccBits(acc); h.SetFprBits(kFs, fs); h.SetFprBits(kFt, ft); @@ -235,6 +239,24 @@ TEST(EeFpuGuardedAddSubConsole, JitMatchesConsoleOnEveryGuardBitRow) } } +// --------------------------------------------------------------------------- +// The DOUBLE path masks in the wide domain instead of on the word, so it is a +// second emitter of the same rule and needs its own console pin. The rows below +// reach the sign-only arm in both directions and the mask-fs arm; the mask-ft +// arm the console never sampled is covered against the interpreter by +// EeRecFpuFull.AddSubGuardMaskAcrossExponentDifferences. +// --------------------------------------------------------------------------- +TEST(EeFpuGuardedAddSubConsole, FullModeJitMatchesConsoleOnEveryGuardBitRow) +{ + for (int i = 0; i < kConsoleCount; ++i) + { + const ConsoleCase& c = kConsole[i]; + SCOPED_TRACE(testing::Message() << "[fpm] case " << c.ordinal << " " + << FormName(c.form)); + EXPECT_EQ(RunOne(c.form, c.fs, c.ft, c.acc, true, true), c.want); + } +} + // --------------------------------------------------------------------------- // On every row the console value differs from what plain IEEE arithmetic gives, // by one representable step toward zero. Without this the two tests above would diff --git a/tests/ctest/core/recompilers/ee_fpu_ou_rounding_console_tests.cpp b/tests/ctest/core/recompilers/ee_fpu_ou_rounding_console_tests.cpp new file mode 100644 index 0000000000..96d91a371e --- /dev/null +++ b/tests/ctest/core/recompilers/ee_fpu_ou_rounding_console_tests.cpp @@ -0,0 +1,298 @@ +// SPDX-FileCopyrightText: 2026 ARMSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ +// +// Where FCR31's O comes from on the EE adder, against the console. +// +// The adder masks the smaller operand's low bits by the exponent difference +// and erases it entirely past 24 (eeGuardedSum in FPU.cpp). O follows the sum +// that mask produced, and follows it after the rounding: +// +// * an addend 25 or more exponents down is gone, so 0x7FFFFFFF + it is +// 0x7FFFFFFF and nothing is raised, however large the addend was; +// * at exactly 24 the addend survives as its leading bit, and +// 0x7FFFFFFF + 2^104 is 2^129 - 2^104 -- above the largest EE number, but +// 25 significant bits, so it chops back onto 0x7FFFFFFF. Still nothing +// raised; +// * at 23 the sum is 2^129, which no rounding brings back, and O|SO is set. +// +// The corpus cannot see any of this: every in-class corpus row puts its addend +// 128 exponents down, where the mask erases it outright. The rows below are a +// standalone probe of exponents 180 to 255, 1293 cases. Every result and FCR31 +// is a console reading. +// +// The single-precision fast path (eeClampMode 1 and 2) is not run here: it +// saturates arithmetic to FLT_MAX, a binade below this whole region. + +#include "harness/EeRecTestHarness.h" +#include "harness/MipsEncode.h" + +#include "Config.h" + +#include + +using namespace recompiler_tests; +using namespace mips; +using namespace mips::ee; + +namespace { + +constexpr u32 kFd = 4, kFs = 5, kFt = 6; + +enum Form +{ + FORM_ADD, FORM_SUB, FORM_ADDA, FORM_SUBA, + FORM_MADD, FORM_MSUB, FORM_MADDA, FORM_MSUBA, +}; + +bool WritesAcc(Form f) +{ + return f == FORM_ADDA || f == FORM_SUBA || f == FORM_MADDA || f == FORM_MSUBA; +} + +u32 Encode(Form f) +{ + switch (f) + { + case FORM_ADD: return ADD_S(kFd, kFs, kFt); + case FORM_SUB: return SUB_S(kFd, kFs, kFt); + case FORM_ADDA: return ADDA_S(kFs, kFt); + case FORM_SUBA: return SUBA_S(kFs, kFt); + case FORM_MADD: return MADD_S(kFd, kFs, kFt); + case FORM_MSUB: return MSUB_S(kFd, kFs, kFt); + case FORM_MADDA: return MADDA_S(kFs, kFt); + default: return MSUBA_S(kFs, kFt); + } +} + +const char* FormName(Form f) +{ + static const char* kNames[] = {"add.s", "sub.s", "adda.s", "suba.s", + "madd.s", "msub.s", "madda.s", "msuba.s"}; + return kNames[f]; +} + +struct Out { u32 val, fcr; }; + +// `exact` picks eeClampMode 4 over 3; the two emit the same code for +// everything this file touches, so both are run. +Out RunOne(Form f, u32 fs, u32 ft, u32 acc, u32 pre, bool jit, bool exact) +{ + EeRecTestHarness h; + h.EnableCop1(); + if (exact) + h.EnableFpuExactMode(); + else + h.EnableFpuFullMode(); + h.SetFprBits(kFs, fs); + h.SetFprBits(kFt, ft); + h.SetAccBits(acc); + h.SetFcr31(pre); + h.LoadProgram({Encode(f)}); + Out o{}; + if (jit) + { + h.RunJitNoDiff(); + o.fcr = h.JitSnapshot().fprs.fprc[31]; + o.val = WritesAcc(f) ? h.GetAccBitsJit() : h.GetFprBitsJit(kFd); + } + else + { + h.RunInterpOnly(); + o.fcr = h.InterpSnapshot().fprs.fprc[31]; + o.val = WritesAcc(f) ? h.GetAccBitsInterp() : h.GetFprBitsInterp(kFd); + } + return o; +} + +constexpr u32 FPUflagO = 0x00008000u; +constexpr u32 FPUflagU = 0x00004000u; +constexpr u32 FPUflagSO = 0x00000010u; +constexpr u32 FPUflagSU = 0x00000008u; +constexpr u32 kOU = FPUflagO | FPUflagU | FPUflagSO | FPUflagSU; + +struct ConsoleCase +{ + Form form; + u32 fs, ft, acc, pre; + u32 want; // console result + u32 wantFcr; // console FCR31 + int addendExp; // exponent field of the term the mask acts on + const char* note; +}; + +constexpr ConsoleCase kConsole[] = { + {FORM_ADD, 0x7FFFFFFFu, 0x65800000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 203, "window"}, + {FORM_SUB, 0x7FFFFFFFu, 0xE5800000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 203, "window"}, + {FORM_ADDA, 0x7FFFFFFFu, 0x65800000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 203, "window"}, + {FORM_SUBA, 0x7FFFFFFFu, 0xE5800000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 203, "window"}, + {FORM_MADD, 0x65800000u, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 203, "window"}, + {FORM_MSUB, 0xE5800000u, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 203, "window"}, + {FORM_MADDA, 0x65800000u, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 203, "window"}, + {FORM_MSUBA, 0xE5800000u, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 203, "window"}, + {FORM_ADD, 0x7FFFFFFFu, 0x6B800000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 215, "window"}, + {FORM_SUB, 0x7FFFFFFFu, 0xEB800000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 215, "window"}, + {FORM_ADDA, 0x7FFFFFFFu, 0x6B800000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 215, "window"}, + {FORM_SUBA, 0x7FFFFFFFu, 0xEB800000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 215, "window"}, + {FORM_MADD, 0x6B800000u, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 215, "window"}, + {FORM_MSUB, 0xEB800000u, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 215, "window"}, + {FORM_MADDA, 0x6B800000u, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 215, "window"}, + {FORM_MSUBA, 0xEB800000u, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 215, "window"}, + {FORM_ADD, 0x7FFFFFFFu, 0x72800000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 229, "window"}, + {FORM_SUB, 0x7FFFFFFFu, 0xF2800000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 229, "window"}, + {FORM_ADDA, 0x7FFFFFFFu, 0x72800000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 229, "window"}, + {FORM_SUBA, 0x7FFFFFFFu, 0xF2800000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 229, "window"}, + {FORM_MADD, 0x72800000u, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 229, "window"}, + {FORM_MSUB, 0xF2800000u, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 229, "window"}, + {FORM_MADDA, 0x72800000u, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 229, "window"}, + {FORM_MSUBA, 0xF2800000u, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 229, "window"}, + {FORM_ADD, 0x7FFFFFFFu, 0x73000000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 230, "window"}, + {FORM_SUB, 0x7FFFFFFFu, 0xF3000000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 230, "window"}, + {FORM_ADDA, 0x7FFFFFFFu, 0x73000000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 230, "window"}, + {FORM_SUBA, 0x7FFFFFFFu, 0xF3000000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 230, "window"}, + {FORM_MADD, 0x73000000u, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 230, "window"}, + {FORM_MSUB, 0xF3000000u, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 230, "window"}, + {FORM_MADDA, 0x73000000u, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 230, "window"}, + {FORM_MSUBA, 0xF3000000u, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 230, "window"}, + {FORM_ADD, 0x7FFFFFFFu, 0x73800000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 231, "boundary"}, + {FORM_SUB, 0x7FFFFFFFu, 0xF3800000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 231, "boundary"}, + {FORM_ADDA, 0x7FFFFFFFu, 0x73800000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 231, "boundary"}, + {FORM_SUBA, 0x7FFFFFFFu, 0xF3800000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 231, "boundary"}, + {FORM_MADD, 0x73800000u, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 231, "boundary"}, + {FORM_MSUB, 0xF3800000u, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 231, "boundary"}, + {FORM_MADDA, 0x73800000u, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 231, "boundary"}, + {FORM_MSUBA, 0xF3800000u, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 231, "boundary"}, + {FORM_ADD, 0x7FFFFFFFu, 0x74000000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01008011u, 232, "raises"}, + {FORM_SUB, 0x7FFFFFFFu, 0xF4000000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01008011u, 232, "raises"}, + {FORM_ADDA, 0x7FFFFFFFu, 0x74000000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01008011u, 232, "raises"}, + {FORM_SUBA, 0x7FFFFFFFu, 0xF4000000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01008011u, 232, "raises"}, + {FORM_MADD, 0x74000000u, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01008011u, 232, "raises"}, + {FORM_MSUB, 0xF4000000u, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01008011u, 232, "raises"}, + {FORM_MADDA, 0x74000000u, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01008011u, 232, "raises"}, + {FORM_MSUBA, 0xF4000000u, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01008011u, 232, "raises"}, + {FORM_ADD, 0x7FFFFFFFu, 0x78000000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01008011u, 240, "raises"}, + {FORM_SUB, 0x7FFFFFFFu, 0xF8000000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01008011u, 240, "raises"}, + {FORM_ADDA, 0x7FFFFFFFu, 0x78000000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01008011u, 240, "raises"}, + {FORM_SUBA, 0x7FFFFFFFu, 0xF8000000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01008011u, 240, "raises"}, + {FORM_MADD, 0x78000000u, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01008011u, 240, "raises"}, + {FORM_MSUB, 0xF8000000u, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01008011u, 240, "raises"}, + {FORM_MADDA, 0x78000000u, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01008011u, 240, "raises"}, + {FORM_MSUBA, 0xF8000000u, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01008011u, 240, "raises"}, + {FORM_ADD, 0xFFFFFFFFu, 0xF3000000u, 0x00000000u, 0x00000000u, 0xFFFFFFFFu, 0x01000001u, 230, "mirrored"}, + {FORM_SUB, 0xFFFFFFFFu, 0x73000000u, 0x00000000u, 0x00000000u, 0xFFFFFFFFu, 0x01000001u, 230, "mirrored"}, + {FORM_ADDA, 0xFFFFFFFFu, 0xF3000000u, 0x00000000u, 0x00000000u, 0xFFFFFFFFu, 0x01000001u, 230, "mirrored"}, + {FORM_SUBA, 0xFFFFFFFFu, 0x73000000u, 0x00000000u, 0x00000000u, 0xFFFFFFFFu, 0x01000001u, 230, "mirrored"}, + {FORM_MADD, 0xF3000000u, 0x3F800000u, 0xFFFFFFFFu, 0x00000000u, 0xFFFFFFFFu, 0x01000001u, 230, "mirrored"}, + {FORM_MSUB, 0x73000000u, 0x3F800000u, 0xFFFFFFFFu, 0x00000000u, 0xFFFFFFFFu, 0x01000001u, 230, "mirrored"}, + {FORM_MADDA, 0xF3000000u, 0x3F800000u, 0xFFFFFFFFu, 0x00000000u, 0xFFFFFFFFu, 0x01000001u, 230, "mirrored"}, + {FORM_MSUBA, 0x73000000u, 0x3F800000u, 0xFFFFFFFFu, 0x00000000u, 0xFFFFFFFFu, 0x01000001u, 230, "mirrored"}, + {FORM_ADD, 0xFFFFFFFFu, 0xF3800000u, 0x00000000u, 0x00000000u, 0xFFFFFFFFu, 0x01000001u, 231, "mirrored"}, + {FORM_SUB, 0xFFFFFFFFu, 0x73800000u, 0x00000000u, 0x00000000u, 0xFFFFFFFFu, 0x01000001u, 231, "mirrored"}, + {FORM_ADDA, 0xFFFFFFFFu, 0xF3800000u, 0x00000000u, 0x00000000u, 0xFFFFFFFFu, 0x01000001u, 231, "mirrored"}, + {FORM_SUBA, 0xFFFFFFFFu, 0x73800000u, 0x00000000u, 0x00000000u, 0xFFFFFFFFu, 0x01000001u, 231, "mirrored"}, + {FORM_MADD, 0xF3800000u, 0x3F800000u, 0xFFFFFFFFu, 0x00000000u, 0xFFFFFFFFu, 0x01000001u, 231, "mirrored"}, + {FORM_MSUB, 0x73800000u, 0x3F800000u, 0xFFFFFFFFu, 0x00000000u, 0xFFFFFFFFu, 0x01000001u, 231, "mirrored"}, + {FORM_MADDA, 0xF3800000u, 0x3F800000u, 0xFFFFFFFFu, 0x00000000u, 0xFFFFFFFFu, 0x01000001u, 231, "mirrored"}, + {FORM_MSUBA, 0x73800000u, 0x3F800000u, 0xFFFFFFFFu, 0x00000000u, 0xFFFFFFFFu, 0x01000001u, 231, "mirrored"}, + {FORM_ADD, 0xFFFFFFFFu, 0xF4000000u, 0x00000000u, 0x00000000u, 0xFFFFFFFFu, 0x01008011u, 232, "mirrored"}, + {FORM_SUB, 0xFFFFFFFFu, 0x74000000u, 0x00000000u, 0x00000000u, 0xFFFFFFFFu, 0x01008011u, 232, "mirrored"}, + {FORM_ADDA, 0xFFFFFFFFu, 0xF4000000u, 0x00000000u, 0x00000000u, 0xFFFFFFFFu, 0x01008011u, 232, "mirrored"}, + {FORM_SUBA, 0xFFFFFFFFu, 0x74000000u, 0x00000000u, 0x00000000u, 0xFFFFFFFFu, 0x01008011u, 232, "mirrored"}, + {FORM_MADD, 0xF4000000u, 0x3F800000u, 0xFFFFFFFFu, 0x00000000u, 0xFFFFFFFFu, 0x01008011u, 232, "mirrored"}, + {FORM_MSUB, 0x74000000u, 0x3F800000u, 0xFFFFFFFFu, 0x00000000u, 0xFFFFFFFFu, 0x01008011u, 232, "mirrored"}, + {FORM_MADDA, 0xF4000000u, 0x3F800000u, 0xFFFFFFFFu, 0x00000000u, 0xFFFFFFFFu, 0x01008011u, 232, "mirrored"}, + {FORM_MSUBA, 0x74000000u, 0x3F800000u, 0xFFFFFFFFu, 0x00000000u, 0xFFFFFFFFu, 0x01008011u, 232, "mirrored"}, + {FORM_ADD, 0x7FFFFFFFu, 0x737FFFFFu, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 230, "mantissa"}, + {FORM_MADD, 0x737FFFFFu, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 230, "mantissa"}, + {FORM_ADD, 0x7FFFFFFFu, 0x73FFFFFFu, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 231, "mantissa"}, + {FORM_MADD, 0x73FFFFFFu, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 231, "mantissa"}, + {FORM_ADD, 0x7FFFFFFFu, 0x747FFFFFu, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01008011u, 232, "mantissa"}, + {FORM_MADD, 0x747FFFFFu, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01008011u, 232, "mantissa"}, + {FORM_ADD, 0x7F800000u, 0x73000000u, 0x00000000u, 0x00000000u, 0x7F800000u, 0x01000001u, 230, "big shape"}, + {FORM_MADD, 0x73000000u, 0x3F800000u, 0x7F800000u, 0x00000000u, 0x7F800000u, 0x01000001u, 230, "big shape"}, + {FORM_ADD, 0x7F800000u, 0x73800000u, 0x00000000u, 0x00000000u, 0x7F800000u, 0x01000001u, 231, "big shape"}, + {FORM_MADD, 0x73800000u, 0x3F800000u, 0x7F800000u, 0x00000000u, 0x7F800000u, 0x01000001u, 231, "big shape"}, + {FORM_ADD, 0x7FC00000u, 0x73000000u, 0x00000000u, 0x00000000u, 0x7FC00000u, 0x01000001u, 230, "big shape"}, + {FORM_MADD, 0x73000000u, 0x3F800000u, 0x7FC00000u, 0x00000000u, 0x7FC00000u, 0x01000001u, 230, "big shape"}, + {FORM_ADD, 0x7FC00000u, 0x73800000u, 0x00000000u, 0x00000000u, 0x7FC00000u, 0x01000001u, 231, "big shape"}, + {FORM_MADD, 0x73800000u, 0x3F800000u, 0x7FC00000u, 0x00000000u, 0x7FC00000u, 0x01000001u, 231, "big shape"}, + {FORM_ADD, 0x7FFFFFFEu, 0x73000000u, 0x00000000u, 0x00000000u, 0x7FFFFFFEu, 0x01000001u, 230, "big shape"}, + {FORM_MADD, 0x73000000u, 0x3F800000u, 0x7FFFFFFEu, 0x00000000u, 0x7FFFFFFEu, 0x01000001u, 230, "big shape"}, + {FORM_ADD, 0x7FFFFFFEu, 0x73800000u, 0x00000000u, 0x00000000u, 0x7FFFFFFEu, 0x01000001u, 231, "big shape"}, + {FORM_MADD, 0x73800000u, 0x3F800000u, 0x7FFFFFFEu, 0x00000000u, 0x7FFFFFFEu, 0x01000001u, 231, "big shape"}, + {FORM_ADD, 0x7FFFFFFFu, 0x7FFFFFFFu, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01008011u, 255, "control: saturates"}, + {FORM_ADD, 0x7F800000u, 0x7F800000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01008011u, 255, "control: saturates"}, + {FORM_SUB, 0x7FFFFFFFu, 0xFFFFFFFFu, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01008011u, 255, "control: saturates"}, + {FORM_MADD, 0x7FFFFFFFu, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01008011u, 255, "control: saturates"}, + {FORM_MADDA, 0x7FFFFFFFu, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01008011u, 255, "control: saturates"}, + {FORM_ADD, 0x7FFFFFFFu, 0x00000000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 0, "control: nothing to raise"}, + {FORM_ADD, 0x7FFFFFFFu, 0x80000000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 0, "control: nothing to raise"}, + {FORM_SUB, 0x7FFFFFFFu, 0x00000000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 0, "control: nothing to raise"}, + {FORM_ADD, 0x7F7FFFFFu, 0x6E000000u, 0x00000000u, 0x00000000u, 0x7F7FFFFFu, 0x01000001u, 220, "control: nothing to raise"}, + {FORM_ADD, 0x7F000000u, 0x6E000000u, 0x00000000u, 0x00000000u, 0x7F000000u, 0x01000001u, 220, "control: nothing to raise"}, + {FORM_MADD, 0x00000000u, 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 0, "control: nothing to raise"}, + {FORM_SUB, 0x7FFFFFFFu, 0x65800000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 203, "control: magnitudes subtract"}, + {FORM_ADD, 0x7FFFFFFFu, 0xE5800000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 203, "control: magnitudes subtract"}, + {FORM_SUB, 0x7FFFFFFFu, 0x69000000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 210, "control: magnitudes subtract"}, + {FORM_ADD, 0x7FFFFFFFu, 0xE9000000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 210, "control: magnitudes subtract"}, + {FORM_SUB, 0x7FFFFFFFu, 0x72800000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 229, "control: magnitudes subtract"}, + {FORM_ADD, 0x7FFFFFFFu, 0xF2800000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 229, "control: magnitudes subtract"}, + {FORM_SUB, 0x7FFFFFFFu, 0x73000000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 230, "control: magnitudes subtract"}, + {FORM_ADD, 0x7FFFFFFFu, 0xF3000000u, 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0x01000001u, 230, "control: magnitudes subtract"}, + {FORM_SUB, 0x7FFFFFFFu, 0x73800000u, 0x00000000u, 0x00000000u, 0x7FFFFFFEu, 0x01000001u, 231, "control: magnitudes subtract"}, + {FORM_ADD, 0x7FFFFFFFu, 0xF3800000u, 0x00000000u, 0x00000000u, 0x7FFFFFFEu, 0x01000001u, 231, "control: magnitudes subtract"}, + {FORM_SUB, 0x7FFFFFFFu, 0x78000000u, 0x00000000u, 0x00000000u, 0x7FFFFEFFu, 0x01000001u, 240, "control: magnitudes subtract"}, + {FORM_ADD, 0x7FFFFFFFu, 0xF8000000u, 0x00000000u, 0x00000000u, 0x7FFFFEFFu, 0x01000001u, 240, "control: magnitudes subtract"}, +}; +constexpr int kConsoleCount = static_cast(std::size(kConsole)); + +} // namespace + +// Both clamp modes of the DOUBLE engine, and the interpreter, against the +// console. +TEST(EeFpuOuRoundingConsole, EveryEngineMatchesTheConsole) +{ + struct Col { const char* name; bool jit, exact; }; + constexpr Col kCols[] = { + {"jit clamp mode 3", true, false}, + {"jit clamp mode 4", true, true}, + {"interpreter", false, true}, + }; + + for (const Col& c : kCols) + { + for (int i = 0; i < kConsoleCount; i++) + { + const ConsoleCase& k = kConsole[i]; + SCOPED_TRACE(testing::Message() + << c.name << " case " << i << " " << FormName(k.form) + << " fs=" << std::hex << k.fs << " ft=" << k.ft + << " acc=" << k.acc << " addend exp " << std::dec << k.addendExp + << " (" << k.note << ")"); + const Out o = RunOne(k.form, k.fs, k.ft, k.acc, k.pre, c.jit, c.exact); + EXPECT_EQ(o.val, k.want); + EXPECT_EQ(o.fcr & kOU, k.wantFcr & kOU); + } + } +} + +// Both polarities, or an engine that never raises O passes the table. +TEST(EeFpuOuRoundingConsole, TableCarriesBothPolarities) +{ + int raised = 0, clear = 0; + for (const ConsoleCase& k : kConsole) + ((k.wantFcr & FPUflagO) ? raised : clear)++; + EXPECT_GE(raised, 8); + EXPECT_GE(clear, 8); + + // And both boundaries: the erasure one at 24, and the rounding one at 23. + int at231 = 0, at232 = 0; + for (const ConsoleCase& k : kConsole) + { + if (k.addendExp == 231 && !(k.wantFcr & FPUflagO)) + at231++; + if (k.addendExp == 232 && (k.wantFcr & FPUflagO)) + at232++; + } + EXPECT_GE(at231, 8); + EXPECT_GE(at232, 8); +} diff --git a/tests/ctest/core/recompilers/ee_fpu_rsqrt_sign_console_tests.cpp b/tests/ctest/core/recompilers/ee_fpu_rsqrt_sign_console_tests.cpp index c956894617..aac7f5917b 100644 --- a/tests/ctest/core/recompilers/ee_fpu_rsqrt_sign_console_tests.cpp +++ b/tests/ctest/core/recompilers/ee_fpu_rsqrt_sign_console_tests.cpp @@ -834,12 +834,14 @@ std::vector Program(const Row& r) } } -// leg 0 interpreter, 1 the shipping fast path, 2 fpuFullMode. +// leg 0 interpreter, 1 the shipping fast path, 2 fpuFullMode, 3 fpuExactMode. void RunRow(const Row& r, int leg, u32* result, u32* fcr31) { EeRecTestHarness h; h.EnableCop1(); - if (leg == 2) + if (leg == 3) + h.EnableFpuExactMode(); + else if (leg == 2) h.EnableFpuFullMode(); h.SetFcr31(r.seed); h.SetFprBits(5, r.fs); @@ -860,7 +862,7 @@ void RunRow(const Row& r, int leg, u32* result, u32* fcr31) } } -const char* kLegName[] = {"[interp]", "[jit fast]", "[jit full]"}; +const char* kLegName[] = {"[interp]", "[jit fast]", "[jit full]", "[jit exact]"}; // The fast path computes in host singles, where an exponent-255 operand is an // infinity or a NaN, so it saturates on the way in. That class belongs to @@ -883,14 +885,14 @@ bool DivUnitApproximation(const Row& r) } // namespace -// All three tiers owe the flag axis: FCR31 costs the fast path none of the +// Every tier owes the flag axis: FCR31 costs the fast path none of the // precision the accuracy tiers buy, so no tier has a tradeoff to make here. TEST(EeFpuRsqrtSignConsole, CauseBitsMatchConsoleOnEveryTier) { for (int i = 0; i < kRowCount; ++i) { const Row& r = kRows[i]; - for (int leg = 0; leg < 3; ++leg) + for (int leg = 0; leg < 4; ++leg) { SCOPED_TRACE(::testing::Message() << r.what << " " << kLegName[leg]); u32 res = 0, fcr = 0; @@ -914,8 +916,9 @@ TEST(EeFpuRsqrtSignConsole, InterpMatchesConsoleOnEveryRow) } } -// fpuFullMode, with its one divergence asserted as itself rather than skipped, -// so that neither closing it nor widening it can happen quietly. +// fpuFullMode divides in host doubles and rounds correctly, so it owes every +// row but the divide unit's, and that one divergence is asserted as itself +// rather than skipped. eeClampMode 4 closes it, just below. TEST(EeFpuRsqrtSignConsole, FullModeMatchesConsoleOutsideTheDivideUnitApproximation) { int approximated = 0; @@ -940,6 +943,22 @@ TEST(EeFpuRsqrtSignConsole, FullModeMatchesConsoleOutsideTheDivideUnitApproximat EXPECT_EQ(approximated, 20); } +// fpuExactMode owes every row for the same two reasons: it holds the top binade +// and it runs the divide unit's recurrence, out of line. The 20 rows above are +// what it buys, so this failing on exactly those is the island not being +// emitted rather than a new defect. +TEST(EeFpuRsqrtSignConsole, ExactModeMatchesConsoleOnEveryRow) +{ + for (int i = 0; i < kRowCount; ++i) + { + const Row& r = kRows[i]; + SCOPED_TRACE(::testing::Message() << r.what << " " << kLegName[3]); + u32 res = 0, fcr = 0; + RunRow(r, 3, &res, &fcr); + EXPECT_EQ(res, r.console); + } +} + // The fast path saturates in host singles, one binade below the EE's largest // number, so a console 0x7FFFFFFF comes back 0x7F7FFFFF with the same sign. // Top-binade operands aside, it is exact everywhere else. diff --git a/tests/ctest/core/recompilers/ee_rec_fpu_divunit_rounding_tests.cpp b/tests/ctest/core/recompilers/ee_rec_fpu_divunit_rounding_tests.cpp index 0bb9246500..bceb82c65e 100644 --- a/tests/ctest/core/recompilers/ee_rec_fpu_divunit_rounding_tests.cpp +++ b/tests/ctest/core/recompilers/ee_rec_fpu_divunit_rounding_tests.cpp @@ -6,10 +6,11 @@ // // The interpreter runs the unit's own radix-2 SRT digit recurrence (FPU.cpp, // eeSrtDigit and below), which reproduces silicon bit for bit on every capture -// this project has taken. Both recompilers take the host's fdiv/fsqrt under +// this project has taken, and arm64's eeClampMode 4 calls the same code out of +// line. The fast path this file exercises takes the host's fdiv/fsqrt under // EmuConfig.Cpu.FPUDivFPCR -- FPUFPCR with round-to-nearest, swapped in around -// the three ops the unit owns; FPU.cpp names the emitters -- which makes them -// the correctly rounded engine. +// the three ops the unit owns; FPU.cpp names the emitters -- which makes it the +// correctly rounded engine. // // So this file is the class-level regression test for the shape of that // divergence. Differing is not enough: the engines must differ by exactly one @@ -100,7 +101,7 @@ struct ScopedAmbientRoundMode // The twin of the above for the DIVIDE unit's register, used by // TheDivideUnitIgnoresItsRoundingModeKnob at the bottom -- which needs to move // the knob for both engines: the interpreter must not respond to it and the -// recompilers must. +// fast path must. struct ScopedDivideRoundMode { FPControlRegister saved_cfg, saved_host; diff --git a/tests/ctest/core/recompilers/ee_rec_fpu_full_mode_tests.cpp b/tests/ctest/core/recompilers/ee_rec_fpu_full_mode_tests.cpp index cb0bacafd4..675559df57 100644 --- a/tests/ctest/core/recompilers/ee_rec_fpu_full_mode_tests.cpp +++ b/tests/ctest/core/recompilers/ee_rec_fpu_full_mode_tests.cpp @@ -29,7 +29,9 @@ #include #include +#include #include +#include using namespace recompiler_tests; using namespace mips; @@ -586,6 +588,35 @@ TEST(EeRecFpuFull, SqrtNegativeSetsIFlagAndUsesAbs) EXPECT_EQ(h.GetGpr64Jit(reg::v0) & 0x00020040u, 0x00020040u) << "I|SI not set"; } +// recSQRT_S_xmm narrows with a plain Fcvt because a root cannot leave the band +// ToPS2FPU_Full's saturating and flushing arms exist for. These are the four +// operands nearest the ends of that band. +TEST(EeRecFpuFull, SqrtStaysInsideTheNarrowingBand) +{ + struct Case + { + u32 ft, want; + const char* what; + }; + static constexpr Case kCases[] = { + {0x7FFFFFFFu, 0x5FB504F3u, "EEMAX: the largest root there is, 2^64.5"}, + {0x00800000u, 0x20000000u, "2^-126: the smallest operand FZ keeps, root 2^-63"}, + {0x007FFFFFu, 0x00000000u, "the largest denormal, flushed ahead of the root"}, + {0x80000000u, 0x00000000u, "-0.0"}, + }; + for (const Case& c : kCases) + { + SCOPED_TRACE(c.what); + EeRecTestHarness h; + h.EnableCop1(); + h.EnableFpuFullMode(); + h.SetFprBits(1, c.ft); + h.LoadProgram({SQRT_S(2, 1)}); + h.RunJitNoDiff(); + EXPECT_EQ(h.GetFprBitsJit(2), c.want) << std::hex << "ft=" << c.ft; + } +} + TEST(EeRecFpuFull, RsqrtPseudoInfExact) { // 1.0 / sqrt(2^128) = 2^-64 = 0x1f800000 exactly. The current interp @@ -780,12 +811,17 @@ struct MaddRow }; // Runs one row and returns the destination register's bits (Fd for MADD/MSUB, -// ACC for MADDA/MSUBA) plus FCR31. -void RunMaddRow(const MaddRow& r, u32* out_val, u32* out_fcr31) +// ACC for MADDA/MSUBA) plus FCR31. `mode` is the clamp mode: 3 and 4 share +// every line of this emitter bar emitDefectiveFmul, so a row needs 4 only when +// its product is one the boundary term decides. +void RunMaddRow(const MaddRow& r, u32* out_val, u32* out_fcr31, int mode = 3) { EeRecTestHarness h; h.EnableCop1(); - h.EnableFpuFullMode(); + if (mode >= 4) + h.EnableFpuExactMode(); + else + h.EnableFpuFullMode(); h.SetFcr31(0); h.SetAccBits(r.acc); h.SetFprBits(0, r.fs); @@ -812,15 +848,11 @@ void RunMaddRow(const MaddRow& r, u32* out_val, u32* out_fcr31) // deficit reaches all 72 of them; when it landed in iFPUd, 35 rows moved one ULP // toward zero. Re-pinning them against the emitter that moved them would assert // nothing, so each value below was re-derived by running the same row through -// FPU.cpp, which models the deficit independently (eeMulRound) -- 71 of 72 agree -// exactly. +// FPU.cpp, which models the deficit independently (eeMulRound). // -// The 72nd is row 53 (ft = 0x48b65815), and it is the documented cost of the -// cheap Booth predicate: its mantissa 0x365815 has no bit of 0x2AA set, so only -// the dropped boundary term fires. The interpreter returns 0xc8b65805, this -// emitter 0xc8b65806. MulDefectDropsTheBoundaryTermTheInterpreterModels holds -// that pair on its own, so closing the gap in iFPUd trips a test that names the -// reason rather than silently re-pinning a number here. +// Row 53 (ft = 0x48b65815) is the one the Booth term alone cannot reach: its +// mantissa 0x365815 has no bit of 0x2AA set, so the boundary term is what +// decides it. constexpr MaddRow kGuardMaskWitnesses[] = { {0x3fb38acau, 0x3f800000u, 0xbacc0111u, 0, 0x3fb357cau, 0u}, {0x3ab20dd7u, 0x3f800000u, 0xc4195bd9u, 0, 0xc4195bc2u, 0u}, @@ -873,7 +905,7 @@ constexpr MaddRow kGuardMaskWitnesses[] = { {0x418eac80u, 0x3f800000u, 0x4a25b350u, 1, 0xca25b308u, 0u}, {0x35935179u, 0x3f800000u, 0xb3508e2fu, 0, 0x358ccd08u, 0u}, {0x452ecb68u, 0x3f800000u, 0xc914f502u, 0, 0xc9144636u, 0u}, - {0x3ef73c56u, 0x3f800000u, 0x48b65815u, 1, 0xc8b65806u, 0u}, // boundary term: interp says c8b65805 + {0x3ef73c56u, 0x3f800000u, 0x48b65815u, 1, 0xc8b65805u, 0u}, // the boundary term decides this one {0x3ec3ca47u, 0x3f800000u, 0x33267262u, 1, 0x3ec3ca46u, 0u}, {0x332a2e5bu, 0x3f800000u, 0xaa055622u, 0, 0x332a2e3au, 0u}, {0x45855769u, 0x3f800000u, 0xc6cac295u, 0, 0xc6a96cbau, 0u}, @@ -898,18 +930,67 @@ constexpr MaddRow kGuardMaskWitnesses[] = { } // namespace +// Run at eeClampMode 4: the expected values are the interpreter's, and one +// witness's product is decided by the boundary term that mode 3 does not emit. +// The mask itself is not mode-dependent. TEST(EeRecFpuFull, MaddGuardMaskAcrossExponentDifferences) { for (const MaddRow& r : kGuardMaskWitnesses) { u32 val = 0, fcr31 = 0; - RunMaddRow(r, &val, &fcr31); + RunMaddRow(r, &val, &fcr31, 4); EXPECT_EQ(val, r.expected) << "acc=" << std::hex << r.acc << " fs=" << r.fs << " ft=" << r.ft << " op=" << std::dec << r.op; } } +// The same mask reached from the other side. recMaddsub aligns a product against +// the ACC; recFPUOp aligns the two guest operands against each other, and it is +// the only caller that reaches the arm masking ft -- the console corpus sampled +// the sign-only arms and the one masking fs, never that one. +// +// The masked bits sit below the chop boundary on almost every pair, so each row +// was searched for by requiring that masking change the truncated sum, and each +// is a witness through the form named in its comment. Only the result word is +// asserted: FCR31's overflow bit is decided before the mask, by the unrounded +// sum. +TEST(EeRecFpuFull, AddSubGuardMaskAcrossExponentDifferences) +{ + struct Row { u32 fs, ft, add, sub; }; + static constexpr Row kRows[] = { + {0x447A1D40u, 0xC19B43AAu, 0x44754323u, 0x447EF75Du}, // diff +5, add + {0x840BC9A8u, 0x00F9ABAAu, 0x8409D651u, 0x840DBCFFu}, // diff +7, add + {0x5536E019u, 0xCD175B0Cu, 0x5536DF82u, 0x5536E0B0u}, // diff +16, add + {0x18BF028Du, 0x8AEAF6A2u, 0x18BF028Du, 0x18BF028Du}, // diff +28, add + {0xA0DD6754u, 0x1051F24Bu, 0xA0DD6754u, 0xA0DD6754u}, // diff +33, add + {0x1813816Au, 0x86F08FE9u, 0x1813816Au, 0x1813816Au}, // diff +35, add + {0x6A3A7CF3u, 0x6BA7B498u, 0x6BBF0436u, 0xEB9064FAu}, // diff -3, sub + {0x33BB818Au, 0xB8C52D09u, 0xB8C4FE29u, 0x38C55BE9u}, // diff -10, add + {0xB62FABAAu, 0xBC848575u, 0xBC848AF2u, 0x3C847FF8u}, // diff -13, sub + {0x0A82A5A9u, 0x1778F6B6u, 0x1778F6B6u, 0x9778F6B6u}, // diff -25, sub + {0xEF997E6Fu, 0xFC11CD21u, 0xFC11CD21u, 0x7C11CD21u}, // diff -25, sub + {0xA309BD95u, 0xB652AB0Cu, 0xB652AB0Cu, 0x3652AB0Cu}, // diff -38, sub + }; + + for (const Row& r : kRows) + { + SCOPED_TRACE(testing::Message() << std::hex << "fs=" << r.fs << " ft=" << r.ft); + for (int issub = 0; issub <= 1; issub++) + { + EeRecTestHarness h; + h.EnableCop1(); + h.EnableFpuFullMode(); + h.SetFcr31(0); + h.SetFprBits(0, r.fs); + h.SetFprBits(1, r.ft); + h.LoadProgram({issub ? SUB_S(2, 0, 1) : ADD_S(2, 0, 1)}); + h.RunJitNoDiff(); + EXPECT_EQ(h.GetFprBitsJit(2), issub ? r.sub : r.add) << (issub ? "sub.s" : "add.s"); + } + } +} + // ToPS2FPU_Wide's arms: saturation at the PS2 maximum, the exponent-0xff band // (whose halve/narrow/re-raise arm the wide form deletes outright -- up there a // PS2 single is just an ordinary double, so it is a plain chop), the underflow @@ -1137,64 +1218,270 @@ TEST(EeRecFpuFull, MulDefectNeverDecrementsAZeroProduct) // --------------------------------------------------------------------------- -// The one thing this emitter knowingly does not model, held on its own so that -// closing it trips a test that says why. +// The boundary term, the one line of this emitter the two clamp modes do not +// share. // -// The measured predicate has two terms: the Booth term `mant & 0x2AA` (bits -// 1,3,5,7,9 -- the sign bits of the five lowest radix-4 Booth digits) and a -// boundary term at the truncation column, -// `bit11 != (8 <= (mant >> 12 & 0xF) <= 13)`. iFPUd emits only the first: the -// second needs a bitfield extract NEON does not have, about eight more -// instructions on every multiply, and it can only change the answer where the -// product's tail is already zero. +// The predicate has two terms: the Booth term `mant & 0x2AA` (bits 1,3,5,7,9, +// the sign bits of the five lowest radix-4 Booth digits) and a boundary term at +// the truncation column, `bit11 != (8 <= (mant >> 12 & 0xF) <= 13)`. They are +// Or'd into one register, so only the operands tell them apart. // -// ft = 0x48b65815 is a witness. Its mantissa 0x365815 has no bit of 0x2AA set, -// bit 11 is 1, and (0x365815 >> 12) & 0xF is 5, so the boundary term is the -// only one that fires. The interpreter (FPU.cpp eeMulArray) models both and -// returns the decremented product; iFPUd returns the IEEE one. +// Three ft values with the same exponent and the same top mantissa nibble +// (5, so `8 <= nib <= 13` is false throughout and the boundary term reduces to +// bit 11), fs = 1.0 so the product is ft and the tail is zero on all of them: // -// If iFPUd ever grows the boundary term, this test fails and the fix is to -// delete it -- along with the carve-out in kGuardMaskWitnesses row 53. -TEST(EeRecFpuFull, MulDefectDropsTheBoundaryTermTheInterpreterModels) +// 0x365015 Booth off, bit11 0 -> neither term -> exact +// 0x365815 Booth off, bit11 1 -> boundary only -> one ULP low +// 0x365215 Booth on, bit11 0 -> Booth only -> one ULP low +// +// The first two are one bit of ft apart, so together they say the boundary +// term is computed and not always on; the third pins the Booth term on its own. +// The middle row is the only one where the modes answer differently, and both +// answers are asserted. +// +// The interpreter (FPU.cpp eeMulArray) reaches all three independently and is +// asserted alongside the emitters. +TEST(EeRecFpuFull, MulDefectBoundaryTermSeparatesTheClampModes) { constexpr u32 kFs = 0x3f800000u; // 1.0: the product is ft, tail always zero - constexpr u32 kFt = 0x48b65815u; + struct Row + { + u32 ft; + u32 want; // interpreter and eeClampMode 4 + u32 want3; // eeClampMode 3: the Booth term alone + const char* what; + }; + constexpr Row kRows[] = { + {0x48b65015u, 0x48b65015u, 0x48b65015u, "neither term: exact"}, + {0x48b65815u, 0x48b65814u, 0x48b65815u, "boundary term alone: mode 4 only"}, + {0x48b65215u, 0x48b65214u, 0x48b65214u, "Booth term alone: both modes"}, + }; - EeRecTestHarness hi; - hi.EnableCop1(); - hi.SetFprBits(0, kFs); - hi.SetFprBits(1, kFt); - hi.LoadProgram({MUL_S(2, 0, 1)}); - hi.RunInterpOnly(); - EXPECT_EQ(hi.GetFprBitsInterp(2), 0x48b65814u) << "interp models the boundary term"; + // One leg per scope: a harness restores the clamp mode in its destructor, + // so a mode-4 harness still alive carries mode 4 into the next leg. + auto run = [](u32 ft, int mode, bool interp) { + EeRecTestHarness h; + h.EnableCop1(); + if (mode >= 4) + h.EnableFpuExactMode(); + else if (mode >= 3) + h.EnableFpuFullMode(); + h.SetFprBits(0, kFs); + h.SetFprBits(1, ft); + h.LoadProgram({MUL_S(2, 0, 1)}); + if (interp) + { + h.RunInterpOnly(); + return h.GetFprBitsInterp(2); + } + h.RunJitNoDiff(); + return h.GetFprBitsJit(2); + }; - EeRecTestHarness h; - h.EnableCop1(); - h.EnableFpuFullMode(); - h.SetFprBits(0, kFs); - h.SetFprBits(1, kFt); - h.LoadProgram({MUL_S(2, 0, 1)}); - h.RunJitNoDiff(); - EXPECT_EQ(h.GetFprBitsJit(2), 0x48b65815u) << "iFPUd drops it: one ULP high"; - - // The Booth term alone, for contrast: same shape, and here they agree. - EeRecTestHarness hb; - hb.EnableCop1(); - hb.EnableFpuFullMode(); - hb.SetFprBits(0, kFs); - hb.SetFprBits(1, 0x48b65a15u); // mantissa 0x365a15: 0x2AA hits bit 9 - hb.LoadProgram({MUL_S(2, 0, 1)}); - hb.RunJitNoDiff(); - EXPECT_EQ(hb.GetFprBitsJit(2), 0x48b65a14u); + for (const Row& r : kRows) + { + SCOPED_TRACE(r.what); + EXPECT_EQ(run(r.ft, 1, true), r.want) << "interp"; + EXPECT_EQ(run(r.ft, 4, false), r.want) << "eeClampMode 4"; + EXPECT_EQ(run(r.ft, 3, false), r.want3) << "eeClampMode 3"; + } + // Liveness: exactly one of the three rows separates the modes. + int split = 0; + for (const Row& r : kRows) + split += (r.want != r.want3); + ASSERT_EQ(split, 1); } // --------------------------------------------------------------------------- -// Randomised differential: mode 3 against the interpreter, which reaches the -// same answers independently and in completely different code (FPU.cpp -// eeMulArray reconstructs the array's truncated low columns in integers; iFPUd -// tests ft's Booth digits and lets the narrowing decide whether the decrement -// survives). Agreement across a wide operand space is what says the emitter -// implements the law rather than the handful of rows above. +// The two classes that separate the clamp modes, against silicon. +// +// Rows from the fpmul3 capture (SCPH-90000, eight fs significands crossed with +// every one of the 2^23 ft significands). `console` is what the console +// returned; `rounded` is the correctly-rounded product the probe computed on +// the EE beside it. Nothing else came back across all 8 x 2^23 rows, so a row +// is described by which of the two it is. +// +// The 8137-case hardware corpus scores the two modes identically: none of its +// 328 zero-tail multiplies reaches the boundary term, their ft mantissas being +// 0x000000, 0x400000 or 0x000001 where the term reads bits 11..15, and of its +// 31 rows inside the array's band 30 saturate past the EE maximum and the last +// is exact. +// +// Neither class can fire mode 3's predicate -- class A has the Booth term off, +// class B has a non-zero tail -- so on every row here mode 3 owes `rounded` and +// mode 4 owes `console`. Half of each table has the two equal. +namespace { +struct MulTierRow +{ + u32 fs, ft, console, rounded; +}; + +// Class A: the exact product has a zero tail and ft's Booth bits are clear, so +// the boundary term at the truncation column is the only thing that can move +// the row. +constexpr MulTierRow kBoundaryTermRows[] = { + {0x3f900000u, 0x3f8fa000u, 0x3fa193ffu, 0x3fa19400u}, + {0x3f900000u, 0x3f8fa800u, 0x3fa19d00u, 0x3fa19d00u}, + {0x3f900000u, 0x3ff33000u, 0x4008cb00u, 0x4008cb00u}, + {0x3f900000u, 0x3ff33800u, 0x4008cf7fu, 0x4008cf80u}, + {0x3fa00000u, 0x3f87d000u, 0x3fa9c3ffu, 0x3fa9c400u}, + {0x3fa00000u, 0x3f87d800u, 0x3fa9ce00u, 0x3fa9ce00u}, + {0x3fa00000u, 0x3fdc6500u, 0x4009bf20u, 0x4009bf20u}, + {0x3fa00000u, 0x3fdc7800u, 0x4009caffu, 0x4009cb00u}, + {0x3fc00000u, 0x3f87d000u, 0x3fcbb7ffu, 0x3fcbb800u}, + {0x3fc00000u, 0x3f87d800u, 0x3fcbc400u, 0x3fcbc400u}, + {0x3fc00000u, 0x3fb27400u, 0x4005d700u, 0x4005d700u}, + {0x3fc00000u, 0x3fb28000u, 0x4005dfffu, 0x4005e000u}, + {0x3fe00000u, 0x3f87d000u, 0x3fedabffu, 0x3fedac00u}, + {0x3fe00000u, 0x3f87d800u, 0x3fedba00u, 0x3fedba00u}, + {0x3fe00000u, 0x3fa1e940u, 0x400dac17u, 0x400dac18u}, + {0x3fe00000u, 0x3fa1f000u, 0x400db200u, 0x400db200u}, + {0x3ff00000u, 0x3f982100u, 0x400e9ef0u, 0x400e9ef0u}, + {0x3ff00000u, 0x3f983800u, 0x400eb47fu, 0x400eb480u}, + {0x3fff0000u, 0x3fbf0000u, 0x403e4100u, 0x403e4100u}, + {0x3fff0000u, 0x3fbf0900u, 0x403e49f6u, 0x403e49f7u}, +}; + +// Class B: the tail is non-zero but below the array's 2^15 borrow, so no +// function of ft can decide the row and only reconstructing the array's +// truncated columns does. +constexpr MulTierRow kArrayBandRows[] = { + {0x3fbfffffu, 0x3fbf8961u, 0x400fa708u, 0x400fa708u}, + {0x3fbfffffu, 0x3fbf92d1u, 0x400fae1cu, 0x400fae1cu}, + {0x3fbfffffu, 0x3fbfb47du, 0x400fc75cu, 0x400fc75du}, + {0x3fbfffffu, 0x3fbfbf69u, 0x400fcf8du, 0x400fcf8eu}, + {0x3fd2b4c1u, 0x3f83fa5cu, 0x3fd9411eu, 0x3fd9411eu}, + {0x3fd2b4c1u, 0x3f85cb9cu, 0x3fdc3efbu, 0x3fdc3efcu}, + {0x3fd2b4c1u, 0x3f87f17cu, 0x3fdfc828u, 0x3fdfc828u}, + {0x3fd2b4c1u, 0x3f8bb3c4u, 0x3fe5f834u, 0x3fe5f835u}, + {0x3fd2b4c1u, 0x3fa398c7u, 0x4006a6d6u, 0x4006a6d6u}, + {0x3fd2b4c1u, 0x3fa6df9cu, 0x40095940u, 0x40095941u}, + {0x3fd2b4c1u, 0x3fab9889u, 0x400d3c49u, 0x400d3c49u}, + {0x3fd2b4c1u, 0x3fb23f42u, 0x4012b5beu, 0x4012b5bfu}, +}; + +// One leg per scope: a harness restores the clamp mode in its destructor, so +// one still alive carries its mode into the next leg. +u32 RunMulTierRow(const MulTierRow& r, int mode, bool interp, bool madd) +{ + EeRecTestHarness h; + h.EnableCop1(); + if (mode >= 4) + h.EnableFpuExactMode(); + else if (mode >= 3) + h.EnableFpuFullMode(); + h.SetAccBits(0x00000000u); // +0, so MADD lands on the product alone + h.SetFprBits(0, r.fs); + h.SetFprBits(1, r.ft); + h.LoadProgram({madd ? MADD_S(2, 0, 1) : MUL_S(2, 0, 1)}); + if (interp) + { + h.RunInterpOnly(); + return h.GetFprBitsInterp(2); + } + h.RunJitNoDiff(); + return h.GetFprBitsJit(2); +} + +// Both emit sites, since recMULop and recMaddsub's multiply stage narrow +// through different code. +void ExpectMulTier(const MulTierRow& r) +{ + for (bool madd : {false, true}) + { + SCOPED_TRACE(madd ? "MADD.S" : "MUL.S"); + EXPECT_EQ(RunMulTierRow(r, 1, true, madd), r.console) << "interp"; + EXPECT_EQ(RunMulTierRow(r, 4, false, madd), r.console) << "eeClampMode 4"; + EXPECT_EQ(RunMulTierRow(r, 3, false, madd), r.rounded) << "eeClampMode 3"; + } +} + +int SeparatingRows(const MulTierRow* rows, size_t n) +{ + int k = 0; + for (size_t i = 0; i < n; i++) + k += (rows[i].console != rows[i].rounded); + return k; +} +} // namespace + +TEST(EeRecFpuFull, MulDeficitBoundaryTermAgainstTheConsole) +{ + for (const MulTierRow& r : kBoundaryTermRows) + { + SCOPED_TRACE(::testing::Message() << std::hex << "fs=" << r.fs << " ft=" << r.ft); + ExpectMulTier(r); + } + // Liveness, both ways: rows the term moves, and rows it must not. + const int sep = SeparatingRows(kBoundaryTermRows, std::size(kBoundaryTermRows)); + EXPECT_GT(sep, 0) << "no row left where the boundary term decides"; + EXPECT_LT(sep, static_cast(std::size(kBoundaryTermRows))) + << "every row decides, so an emitter that always decremented would pass"; +} + +TEST(EeRecFpuFull, MulDeficitArrayBandAgainstTheConsole) +{ + for (const MulTierRow& r : kArrayBandRows) + { + SCOPED_TRACE(::testing::Message() << std::hex << "fs=" << r.fs << " ft=" << r.ft); + ExpectMulTier(r); + } + const int sep = SeparatingRows(kArrayBandRows, std::size(kArrayBandRows)); + EXPECT_GT(sep, 0) << "no row left inside the band that the array moves"; + EXPECT_LT(sep, static_cast(std::size(kArrayBandRows))) + << "every row moves, so an emitter that always decremented would pass"; +} + +// Both tables above are exponent 127 with both operands positive, because that +// is what the fpmul3 sweep covered. Every model of the deficit in the tree +// reads the significands and nothing else, so these rows put the other two +// fields on the console: captures/fpmulsign, four sign combinations across five +// exponent placements for eight separating operand pairs. All 160 came back one +// ULP low, and the extremes are kept here. +// +// The operands are the ones above with their exponent fields moved, so the +// significands are unchanged; a row that fails here and passes there is an +// emitter reading the exponent or the sign. +constexpr MulTierRow kExponentAndSignRows[] = { + {0x12C00000u, 0x30F5A104u, 0x043838C2u, 0x043838C3u}, + {0x92C00000u, 0x30F5A104u, 0x843838C2u, 0x843838C3u}, + {0x12C00000u, 0xB0F5A104u, 0x843838C2u, 0x843838C3u}, + {0x92C00000u, 0xB0F5A104u, 0x043838C2u, 0x043838C3u}, + {0x00C00000u, 0x7EF5A104u, 0x403838C2u, 0x403838C3u}, + {0x80C00000u, 0x7EF5A104u, 0xC03838C2u, 0xC03838C3u}, + {0x00C00000u, 0xFEF5A104u, 0xC03838C2u, 0xC03838C3u}, + {0x80C00000u, 0xFEF5A104u, 0x403838C2u, 0x403838C3u}, + {0x00E00000u, 0x7EA2EC50u, 0x400E8EC5u, 0x400E8EC6u}, + {0x80E00000u, 0x7EA2EC50u, 0xC00E8EC5u, 0xC00E8EC6u}, + {0x00E00000u, 0xFEA2EC50u, 0xC00E8EC5u, 0xC00E8EC6u}, + {0x80E00000u, 0xFEA2EC50u, 0x400E8EC5u, 0x400E8EC6u}, + {0x00A00000u, 0x7ED47C40u, 0x4004CDA7u, 0x4004CDA8u}, + {0x80A00000u, 0x7ED47C40u, 0xC004CDA7u, 0xC004CDA8u}, + {0x00A00000u, 0xFED47C40u, 0xC004CDA7u, 0xC004CDA8u}, + {0x80A00000u, 0xFED47C40u, 0x4004CDA7u, 0x4004CDA8u}, +}; + +TEST(EeRecFpuFull, MulDeficitIgnoresTheExponentAndTheSigns) +{ + int signs = 0; + for (const MulTierRow& r : kExponentAndSignRows) + { + SCOPED_TRACE(::testing::Message() << std::hex << "fs=" << r.fs << " ft=" << r.ft); + ExpectMulTier(r); + signs |= 1 << (((r.fs >> 31) << 1) | (r.ft >> 31)); + } + EXPECT_EQ(signs, 0xF) << "not all four sign combinations are still here"; + // Every row here separates the two modes; the control against an emitter + // that decrements unconditionally is ExpectMulTier()'s mode 3 leg. + EXPECT_EQ(SeparatingRows(kExponentAndSignRows, std::size(kExponentAndSignRows)), + static_cast(std::size(kExponentAndSignRows))); +} + +// --------------------------------------------------------------------------- +// Randomised differential: mode 4 against the interpreter, which reaches the +// same answers in completely different code (FPU.cpp eeMulArray reconstructs +// the array's truncated low columns in integers; iFPUd reads the predicate off +// ft's slot bits and the tail off the double product). // // Dimensions varied and crossed: six operand classes on each side (arbitrary // words, random normals, powers of two -- which force a zero tail and so make @@ -1205,20 +1492,11 @@ TEST(EeRecFpuFull, MulDefectDropsTheBoundaryTermTheInterpreterModels) // recMaddsub's multiply stage, reached with ACC = +0 so the accumulate is a // no-op on the product's bits). // -// Two divergences are licensed, both with the interpreter one ULP nearer zero: -// -// 1. the dropped boundary term, on rows whose product is exactly -// representable; and -// 2. rows whose tail is non-zero but smaller than the array's 2^15 borrow. -// The interpreter reconstructs the array and so sees these; iFPUd decides -// on a double, where one integer ULP is 2^-29 of a single ULP, so its -// decrement is absorbed by the narrowing and the row comes back IEEE. -// -// Rows are classified by the tail below the single ULP, taken from the exact -// 48-bit significand product, so a bug in eeMulArray cannot license itself. -// Anything else -- a wrong predicate, a decrement escaping into a tail large -// enough to absorb it, a zero product turned into a NaN, a saturation or -// top-binade case the decrement walked across -- fails here. +// Mode 4 answers every row the interpreter's way, the inline predicate +// deciding the zero-tail rows and the island's call to eeMulOneUlpLow deciding +// the band below the array's borrow. The two classes that need those parts are +// counted and asserted non-empty, classified from the exact 48-bit significand +// product so that neither engine can license itself. TEST(EeRecFpuFull, MulDefectRandomisedDifferentialAgainstTheInterpreter) { auto splitmix = [](u64& state) { @@ -1263,7 +1541,7 @@ TEST(EeRecFpuFull, MulDefectRandomisedDifferentialAgainstTheInterpreter) }; u64 state = 0x1234567890ABCDEFull; - int rows = 0, boundary_gap[4] = {0, 0, 0, 0}, subulp_gap = 0; + int rows = 0, boundary_rows[4] = {0, 0, 0, 0}, subulp_rows = 0; for (int i = 0; i < 40000; i++) { const int cs = static_cast(splitmix(state) % 6); @@ -1283,7 +1561,7 @@ TEST(EeRecFpuFull, MulDefectRandomisedDifferentialAgainstTheInterpreter) EeRecTestHarness h; h.EnableCop1(); - h.EnableFpuFullMode(); + h.EnableFpuExactMode(); h.SetAccBits(0); h.SetFprBits(0, fs); h.SetFprBits(1, ft); @@ -1301,66 +1579,120 @@ TEST(EeRecFpuFull, MulDefectRandomisedDifferentialAgainstTheInterpreter) const u32 interp = hi.GetFprBitsInterp(dst); rows++; - if (jit == interp) - continue; - const u64 tail = tailBelowUlp(fs, ft); - const bool boundary_only = tail == 0 && !booth(ft) && full(ft); - const bool sub_ulp_tail = tail != 0 && tail < 0x8000u; - ASSERT_TRUE((boundary_only || sub_ulp_tail) && jit == interp + 1u) - << "unlicensed divergence: form=" << form << " cs=" << cs << " ct=" << ct - << std::hex << " fs=" << fs << " ft=" << ft << " tail=" << tail - << " jit=" << jit << " interp=" << interp; - if (boundary_only) - boundary_gap[form]++; - else - subulp_gap++; + // The class the Booth term cannot see, and the class no function of ft + // can see at all. Both are counted whether they agreed or not. + if (tail == 0 && !booth(ft) && full(ft)) + boundary_rows[form]++; + if (tail != 0 && tail < 0x8000u) + subulp_rows++; + + ASSERT_EQ(jit, interp) + << "form=" << form << " cs=" << cs << " ct=" << ct + << std::hex << " fs=" << fs << " ft=" << ft << " tail=" << tail; } EXPECT_EQ(rows, 40000); - // Liveness. "No unlicensed divergence" is also what a harness that never - // reached the emitter would report, so each licensed one must actually be - // observed -- the boundary term at both emit sites, since they narrow - // through different code. - EXPECT_GT(boundary_gap[0] + boundary_gap[1] + boundary_gap[2], 0) + // Liveness, per class. The boundary term is asserted at both emit sites + // because they narrow through different code. + EXPECT_GT(boundary_rows[0] + boundary_rows[1] + boundary_rows[2], 0) << "recMULop never reached the boundary-term class: the sweep is vacuous"; - EXPECT_GT(boundary_gap[3], 0) + EXPECT_GT(boundary_rows[3], 0) << "recMaddsub never reached the boundary-term class: the sweep is vacuous"; - EXPECT_GT(subulp_gap, 0) - << "the sweep never reached a non-zero tail below the borrow: the " - "second carve-out is vacuous and would hide a regression"; + EXPECT_GT(subulp_rows, 0) + << "the sweep never reached a non-zero tail below the borrow, so it " + "never entered the island and says nothing about the call"; +} + +// --------------------------------------------------------------------------- +// The island's call, with the allocator holding as much as it can. The tests +// above reach it from one-instruction programs, where almost nothing is +// resident. The call is plain AAPCS -- it clobbers x0-x18 and every vector +// register bar the low halves of q8-q15 -- so a block's live values are +// protected by the spill emitted around it and the pin reload after it. +// +// Each register below is used once before the multiply and once after, so the +// allocator has a reason to hold it across. FPRs and GPRs both, saved by +// different loops. +// +// $at, $s0 and $k0 are the caller-saved EE pin mirrors rather than allocator +// state: the island flushes them before the call and reloads them after, and a +// reload without the flush loses the block's writes to them. +TEST(EeRecFpuFull, MulArrayIslandPreservesTheAllocatorsLiveRegisters) +{ + // tail 0x2: below the array's borrow, so this is a row that calls out. + constexpr u32 kFs = 0x3F800001u, kFt = 0x3F800002u; + static const u32 kPinned[] = {1, 16, 26}; // $at, $s0, $k0 + + std::vector prog; + auto body = [&prog]() { + for (u32 f = 4; f < 16; f += 2) + prog.push_back(ADD_S(f, f, f + 1)); + for (u32 r = 8; r < 14; r += 2) + prog.push_back(DADDU(r, r, r + 1)); + for (u32 r : kPinned) + prog.push_back(DADDU(r, r, 8)); + }; + body(); + prog.push_back(MUL_S(2, 0, 1)); + body(); + + auto seed = [&](EeRecTestHarness& h) { + h.EnableCop1(); + h.SetFprBits(0, kFs); + h.SetFprBits(1, kFt); + for (u32 f = 4; f < 16; f++) + h.SetFprBits(f, 0x3F800000u + (f << 16)); // distinct, exactly representable + for (u32 r = 8; r < 14; r++) + h.SetGpr64(r, 0x0123456789ABCDEFull * (r + 1)); + for (u32 r : kPinned) + h.SetGpr64(r, 0xFEDCBA9876543210ull * (r + 1)); + }; + + EeRecTestHarness hi; + seed(hi); + hi.LoadProgram(prog); + hi.RunInterpOnly(); + + EeRecTestHarness h; + h.EnableFpuExactMode(); + seed(h); + h.LoadProgram(prog); + h.RunJitNoDiff(); + + EXPECT_EQ(h.GetFprBitsJit(2), 0x3F800002u) << "the island's own result"; + for (u32 f = 4; f < 16; f++) + EXPECT_EQ(h.GetFprBitsJit(f), hi.GetFprBitsInterp(f)) << "f" << f; + for (u32 r = 8; r < 14; r++) + EXPECT_EQ(h.GetGpr64Jit(r), hi.GetGpr64Interp(r)) << "r" << r; + for (u32 r : kPinned) + EXPECT_EQ(h.GetGpr64Jit(r), hi.GetGpr64Interp(r)) << "pinned r" << r; } // --------------------------------------------------------------------------- -// Residency of the predicate mask (d10, NEON_RESERVED_FPU_MULMASK). +// The predicate's operand: the allocator-resident guest ft. // -// The mask is not materialized per multiply. It is parked once per JIT entry by -// _DynGen_EnterRecompiledCode, which is what makes emitDefectiveFmul four -// instructions instead of six -- see the comment on the constant in -// iCore-arm64.h for the contract. These tests pin the two things that contract -// rests on: that no C-call seam, VU dispatch or inline macro-mode emit destroys -// the parked value, and that the allocator never hands q10 out. +// emitDefectiveFmul indexes bits 30..44 of ft's slot, read straight out of +// whatever NEON register the allocator has it in. That is a 64-bit read of a +// register the emitter does not own, so it is only correct while the full slot +// is there -- through a C-call seam (iFlushCall retains FPR-class slots in the +// callee-saved range, where AAPCS64 preserves the low 64 bits and nothing +// above), through inline macro-mode emit, and through a spill and reload. // -// Both failures are silent -- no crash, no corrupt value, just a wrong rounding -// decision on a fraction of multiplies -- and they fail in opposite directions, -// which is why every test below checks both polarities: +// A 32-bit fill, a retention that kept only the single, or an allocator that +// let something else into the register leaves those bits wrong with no crash +// and no corrupt value -- just a wrong rounding decision on a fraction of +// multiplies. Every test below checks both polarities: // -// mask zeroed -> Cmtst never fires -> every product correctly rounded -// (one ULP high wherever silicon is short) -// mask overwritten -> Cmtst fires on operands it must not -> products one -// with a live value ULP low where silicon is exact +// bits 30..44 read as zero -> the predicate never fires -> every product +// correctly rounded, one ULP high wherever +// silicon is short +// bits 30..44 read garbage -> it fires on operands it must not -> products +// one ULP low where silicon is exact // -// A test using only predicate-on operands sees the first and is blind to the -// second, because a garbage mask with any of bits 0..9 set gives the same -// answer as the correct mask on exactly those rows. That is not hypothetical: -// removing q10 from _isReservedNEONreg makes the allocator home a guest FPR -// there, and the observed break is the second kind, on the rows where ft's -// mantissa is 0. -// -// The 1147-case hardware corpus cannot see any of this: its cases are single -// ops, so every multiply in it is the first multiply after the entry that -// parked the mask. +// The 1147-case hardware corpus is single ops, so no multiply in it is far +// enough into a block to have been moved. namespace { // Two rows from kSiliconMulRows above, chosen as a matched pair on one pair of // registers -- f0 = 1.0, f1 = +FLT_MAX -- so a single block can ask the @@ -1370,35 +1702,34 @@ namespace { // MUL_S(d, 1, 0) ft = f0, mantissa 0 -> predicate off, 0x7f7fffff // // Both measured on an SCPH-90000; this is the non-commutativity of mul.s, which -// is the sharpest available probe of the mask because the two answers differ by -// exactly the decrement the mask controls. -constexpr u32 kMaskFprOne = 0x3f800000u; // f0 = 1.0 -constexpr u32 kMaskFprMax = 0x7f7fffffu; // f1 = +FLT_MAX -constexpr u32 kMaskOnWant = 0x7f7ffffeu; // 1.0 * FLT_MAX: silicon is one ULP low -constexpr u32 kMaskOffWant = 0x7f7fffffu; // FLT_MAX * 1.0: silicon is exact +// is the sharpest available probe because the two answers differ by exactly the +// decrement the predicate controls. +constexpr u32 kPredFprOne = 0x3f800000u; // f0 = 1.0 +constexpr u32 kPredFprMax = 0x7f7fffffu; // f1 = +FLT_MAX +constexpr u32 kPredOnWant = 0x7f7ffffeu; // 1.0 * FLT_MAX: silicon is one ULP low +constexpr u32 kPredOffWant = 0x7f7fffffu; // FLT_MAX * 1.0: silicon is exact -// Seed f0/f1 and assert the matched pair in fd_on / fd_off, at both emit sites. -void ExpectMaskLive(EeRecTestHarness& h, u32 fd_on, u32 fd_off, const char* where) +// Assert the matched pair in fd_on / fd_off. +void ExpectPredicateLive(EeRecTestHarness& h, u32 fd_on, u32 fd_off, const char* where) { - EXPECT_EQ(h.GetFprBitsJit(fd_on), kMaskOnWant) - << where << ": predicate did not fire -- the parked mask read as zero"; - EXPECT_EQ(h.GetFprBitsJit(fd_off), kMaskOffWant) - << where << ": predicate fired on ft mantissa 0 -- the parked mask holds garbage"; + EXPECT_EQ(h.GetFprBitsJit(fd_on), kPredOnWant) + << where << ": predicate did not fire -- ft's slot bits read as zero"; + EXPECT_EQ(h.GetFprBitsJit(fd_off), kPredOffWant) + << where << ": predicate fired on ft mantissa 0 -- ft's slot bits hold garbage"; } } // namespace // A VCALLMS in the middle of the block is both runtime hazards at once: it is a // real in-block C-call seam (the callee may clobber any caller-saved register), // and it dispatches a VU0 microprogram, whose blocks allocate NEON slots q0-q27 -// freely. d10 survives the first because AAPCS64 preserves the low 64 bits of -// d8-d15, and the second because mVUdispatcherAB's prologue Stp/Ldp-saves -// d8-d15 around the dispatch -- the same protection the s8/s9 clamp scalars get -// (see the VE-04 note in microVU-arm64.cpp). +// freely. A retained ft survives the first because AAPCS64 preserves the low 64 +// bits of v8-v15, the whole slot, and the second because mVUdispatcherAB's +// prologue Stp/Ldp-saves d8-d15 around the dispatch (see the VE-04 note in +// microVU-arm64.cpp). // -// A compile-time liveness flag in a caller-saved register would have had to -// invalidate at this seam and re-materialize after it; this is the assertion -// that the parked register needs no seam handling at all, at both emit sites. -TEST(EeRecFpuFull, MulDefectMaskSurvivesAnInBlockCallSeam) +// Both emit sites, since recMULop and recMaddsub reach the seam with different +// registers live. +TEST(EeRecFpuFull, MulDefectPredicateSurvivesAnInBlockCallSeam) { EeRecTestHarness h; h.EnableCop1(); @@ -1408,8 +1739,8 @@ TEST(EeRecFpuFull, MulDefectMaskSurvivesAnInBlockCallSeam) // Trivial immediate-E micro: the VCALLMS is here purely as the seam. h.SeedVu0Microprogram(0, {vu::EBitNopPair(), vu::NopPair()}); h.SetAccBits(0x00000000u); - h.SetFprBits(0, kMaskFprOne); - h.SetFprBits(1, kMaskFprMax); + h.SetFprBits(0, kPredFprOne); + h.SetFprBits(1, kPredFprMax); h.LoadProgram({ MUL_S(2, 0, 1), MUL_S(3, 1, 0), @@ -1422,23 +1753,22 @@ TEST(EeRecFpuFull, MulDefectMaskSurvivesAnInBlockCallSeam) }); h.RunJitNoDiff(); - ExpectMaskLive(h, 2, 3, "pre-seam MUL.S"); - ExpectMaskLive(h, 4, 5, "post-seam MUL.S"); - ExpectMaskLive(h, 6, 7, "post-seam MADD.S"); + ExpectPredicateLive(h, 2, 3, "pre-seam MUL.S"); + ExpectPredicateLive(h, 4, 5, "post-seam MUL.S"); + ExpectPredicateLive(h, 6, 7, "post-seam MADD.S"); // Liveness: the two expectations above discriminate only because the two // answers differ. If this ever fires the rows stopped being a matched pair // and the test is vacuous whatever it reports. - ASSERT_NE(kMaskOnWant, kMaskOffWant); + ASSERT_NE(kPredOnWant, kPredOffWant); } // COP2 macro mode is the one context that emits mVU code inline in an EE block, -// with no dispatcher save around it. It is safe for d10 for a structural reason -// rather than an ABI one: macro emit is bounded to NEON slots 0-3 by -// kMacroVFEvictHighWater, which mVUmacroEmitEpilogue asserts on every macro op. -// That is why d10 needs no microVU pool gate, unlike SL-13's q25/q26 -- and -// this is the end-to-end check on it, with two macro FMACs between the -// multiplies to give the mVU allocator something to spend registers on. -TEST(EeRecFpuFull, MulDefectMaskSurvivesInlineCop2MacroMode) +// with no dispatcher save around it, so an EE FPR left resident across it is +// protected by nothing but the mVU allocator's own bound: macro emit is limited +// to NEON slots 0-3 by kMacroVFEvictHighWater, which mVUmacroEmitEpilogue +// asserts on every macro op. Two macro FMACs between the multiplies give that +// allocator something to spend registers on. +TEST(EeRecFpuFull, MulDefectPredicateSurvivesInlineCop2MacroMode) { EeRecTestHarness h; h.EnableCop1(); @@ -1447,8 +1777,8 @@ TEST(EeRecFpuFull, MulDefectMaskSurvivesInlineCop2MacroMode) h.SeedVu0Vi(REG_VPU_STAT, 0); h.SeedVu0Vf(1, 1.0f, 2.0f, 3.0f, 4.0f); h.SeedVu0Vf(2, 5.0f, 6.0f, 7.0f, 8.0f); - h.SetFprBits(0, kMaskFprOne); - h.SetFprBits(1, kMaskFprMax); + h.SetFprBits(0, kPredFprOne); + h.SetFprBits(1, kPredFprMax); h.LoadProgram({ MUL_S(2, 0, 1), MUL_S(3, 1, 0), @@ -1459,25 +1789,21 @@ TEST(EeRecFpuFull, MulDefectMaskSurvivesInlineCop2MacroMode) }); h.RunJitNoDiff(); - ExpectMaskLive(h, 2, 3, "pre-macro MUL.S"); - ExpectMaskLive(h, 4, 5, "MUL.S after inline COP2 macro emit"); - ASSERT_NE(kMaskOnWant, kMaskOffWant); + ExpectPredicateLive(h, 2, 3, "pre-macro MUL.S"); + ExpectPredicateLive(h, 4, 5, "MUL.S after inline COP2 macro emit"); + ASSERT_NE(kPredOnWant, kPredOffWant); } -// The other half of the contract: nothing in EE codegen may be handed q10. -// EeVu0Cop2ClampResidency.EeAllocatorReservesClampRegs pins the reservation at -// the predicate; this pins it through the allocator, with enough simultaneously -// live FP values to drive allocation into the callee-saved range where the mask -// sits. That range is not a last resort -- the FPR class prefers it (GE-15, -// _getFreeArm64NEONInRangeNoEvict(NEON_CALLEE_SAVED_START, ...)), so q10 is one -// of the first homes an unreserved pool would reach for, not one of the last. -TEST(EeRecFpuFull, MulDefectMaskSurvivesHeavyFprPressure) +// Sixteen simultaneously live FPRs is more than the pool has, so ft goes out to +// its slot in fpuRegs and comes back. A 32-bit fill and spill would round-trip +// the word and lose the domain, leaving bits 30..44 clear on the reload. +TEST(EeRecFpuFull, MulDefectPredicateSurvivesHeavyFprPressure) { EeRecTestHarness h; h.EnableCop1(); h.EnableFpuFullMode(); - h.SetFprBits(0, kMaskFprOne); - h.SetFprBits(1, kMaskFprMax); + h.SetFprBits(0, kPredFprOne); + h.SetFprBits(1, kPredFprMax); std::vector prog; for (u32 r = 6; r < 22; r++) @@ -1494,9 +1820,9 @@ TEST(EeRecFpuFull, MulDefectMaskSurvivesHeavyFprPressure) h.LoadProgram(prog); h.RunJitNoDiff(); - ExpectMaskLive(h, 2, 3, "MUL.S under FPR pressure"); - ExpectMaskLive(h, 4, 5, "MUL.S after 16 live FPRs"); - ASSERT_NE(kMaskOnWant, kMaskOffWant); + ExpectPredicateLive(h, 2, 3, "MUL.S under FPR pressure"); + ExpectPredicateLive(h, 4, 5, "MUL.S after 16 live FPRs"); + ASSERT_NE(kPredOnWant, kPredOffWant); } // --------------------------------------------------------------------------- @@ -1555,10 +1881,9 @@ constexpr BinadeRow kTopBinadeSilent[] = { {0x3fff0000u, 0x3f808800u, 0x40000778u}, }; -// Upper binade, T == 0, only the dropped boundary term fires -> silicon is one -// ULP low and this emitter is one ULP high. `want` is the silicon value, so -// the emitter is asserted at want + 1: the licensed divergence, in the binade -// where it had never been measured. +// Upper binade, T == 0, only the boundary term fires -> silicon is one ULP low. +// The truncation column moves one bit between the binades, so a term keyed to +// fixed bit positions of ft need not survive the shift. constexpr BinadeRow kTopBinadeBoundary[] = { {0x3fc00000u, 0x3faab000u, 0x400003ffu}, {0x3fe00000u, 0x3f924940u, 0x40000017u}, @@ -1572,11 +1897,21 @@ constexpr BinadeRow kTopBinadeBoundary[] = { // multiply stage with ToPS2FPU_Wide's mask-off-the-low-29. Those two land on // different sides of the mantissa boundary that the binade shifts, so the // upper binade has to be checked through both. -void ExpectBothSites(const BinadeRow& r, u32 want, const char* what) +// +// `mode` is the clamp mode the rows need: the Booth term is on 3 and 4, the +// boundary term only on 4. +void ExpectBothSites(const BinadeRow& r, u32 want, const char* what, int mode = 3) { + auto tier = [mode](EeRecTestHarness& h) { + if (mode >= 4) + h.EnableFpuExactMode(); + else + h.EnableFpuFullMode(); + }; + EeRecTestHarness h; h.EnableCop1(); - h.EnableFpuFullMode(); + tier(h); h.SetFprBits(0, r.fs); h.SetFprBits(1, r.ft); h.LoadProgram({MUL_S(2, 0, 1)}); @@ -1586,7 +1921,7 @@ void ExpectBothSites(const BinadeRow& r, u32 want, const char* what) EeRecTestHarness hm; hm.EnableCop1(); - hm.EnableFpuFullMode(); + tier(hm); hm.SetAccBits(0x00000000u); // ACC = +0 -> fd is the rounded product alone hm.SetFprBits(0, r.fs); hm.SetFprBits(1, r.ft); @@ -1612,14 +1947,13 @@ TEST(EeRecFpuFull, MulDefectStaysSilentInTheUpperBinadeWhereSiliconIsExact) ExpectBothSites(r, r.want, "upper-binade exact"); } -// The dropped boundary term reaches the upper binade too, at the same 1/64 of -// significands. Asserting emitter == silicon + 1 keeps the gap measured rather -// than latent: closing it in iFPUd trips this and names the reason. -TEST(EeRecFpuFull, MulDefectBoundaryTermGapAlsoExistsInTheUpperBinade) +// The boundary term reaches the upper binade too: rows the Booth term cannot +// see, so they pin it where exp == 0xff is an ordinary EE number. +TEST(EeRecFpuFull, MulDefectBoundaryTermAlsoFiresInTheUpperBinade) { for (const BinadeRow& r : kTopBinadeBoundary) { - ExpectBothSites(r, r.want + 1u, "upper-binade boundary term"); + ExpectBothSites(r, r.want, "upper-binade boundary term", 4); EeRecTestHarness hi; hi.EnableCop1(); diff --git a/tests/ctest/core/recompilers/ee_rec_fpu_tests.cpp b/tests/ctest/core/recompilers/ee_rec_fpu_tests.cpp index 4e56a4ecd5..10e92118a3 100644 --- a/tests/ctest/core/recompilers/ee_rec_fpu_tests.cpp +++ b/tests/ctest/core/recompilers/ee_rec_fpu_tests.cpp @@ -1375,70 +1375,58 @@ TEST(EeRecFpu, DivSAfterAddSReadsLiveOperands) h.ExpectFpr(4, FloatBits(5.0f)); } -// ---- FpuMulHack (Tales of Destiny Remake gamefix) -------------------------- -// JIT-only: the interpreter MUL_S has no hack, so a hack-hit legitimately -// diverges from interp — assert GetFprBitsJit() under RunJitNoDiff(). The hack -// patches exactly 0.25 * (π) (0x3e800000 * 0x40490fdb) to 0x3f490fda. The -// shared emitFpuMul helper wires it into all of MUL/MULA/MADD/MSUB/MADDA/MSUBA. - -TEST(EeRecFpu, MulSFpuMulHackPatchesMagicProduct) +// ---- The FpuMulHack pair (Tales of Destiny Remake gamefix) ----------------- +// Upstream's FpuMulHack patches one product, 0.25 * (pi), from the +// correctly-rounded 0x3F490FDB to 0x3F490FDA. Measured on SCPH-90000 +// (captures/fpmul/): 0x3E800000 * 0x40490FDB returns 0x3F490FDA, and the same +// two words reversed return 0x3F490FDB. fs = 2^-2 makes the product zero-tailed +// and ft fires the Booth predicate, so this is one sample of the multiplier's +// own deficit, and the gamefix's compare against two fixed constants is why it +// did not fire reversed. +// +// Both orders on every engine: what replaces the gamefix has to reproduce the +// asymmetry as well as the value. The single-precision tier carries no cut of +// the law, so modes 1 and 2 are pinned at the IEEE product. +TEST(EeRecFpu, MulHackPairIsTheMultiplierDeficitFromModeThree) { - EeRecTestHarness h; - h.EnableCop1(); - h.EnableFpuMulHack(); - h.SetFprBits(0, 0x3e800000u); - h.SetFprBits(1, 0x40490fdbu); - h.LoadProgram({ee::MUL_S(2, 0, 1)}); - h.RunJitNoDiff(); - EXPECT_EQ(h.GetFprBitsJit(2), 0x3f490fdau); -} - -TEST(EeRecFpu, MulSFpuMulHackOffStillReachesTheConsoleValueOnInterp) -{ - // This test used to assert the opposite -- that with the gamefix off the - // "ordinary" product comes out, and that JIT and interp agree on it. Its - // premise was that the IEEE product is the console's. It is not. - // - // Measured on SCPH-90000 (captures/fpmul/): mul.s of 0x3E800000 by - // 0x40490FDB returns 0x3F490FDA, and the same two words in the reverse - // operand order return 0x3F490FDB. FpuMulHack is a one-point sample of the - // multiplier's own one-ULP deficit, not a game kludge -- which is why it - // compares fs and ft against their own constants and so does not fire - // reversed, exactly as the console behaves. - // - // The interpreter models the deficit itself (eeMulRound in FPU.cpp), so it - // lands on the console value with the gamefix off. The single-precision fast - // path does not, so the two legitimately diverge here and this cannot be a - // Run() diff -- and RunJitNoDiff() never runs the interpreter at all, so - // each engine needs its own harness. - EeRecTestHarness hi; - hi.EnableCop1(); - hi.SetFprBits(0, 0x3e800000u); - hi.SetFprBits(1, 0x40490fdbu); - hi.LoadProgram({ee::MUL_S(2, 0, 1)}); - hi.RunInterpOnly(); - EXPECT_EQ(hi.GetFprBitsInterp(2), 0x3f490fdau) << "interp should reach silicon unaided"; - - // Reversed: the console returns the un-decremented product, because the - // predicate reads ft alone. This is the half that pins it as an operand - // order effect rather than a constant fudge. - EeRecTestHarness hr; - hr.EnableCop1(); - hr.SetFprBits(0, 0x40490fdbu); - hr.SetFprBits(1, 0x3e800000u); - hr.LoadProgram({ee::MUL_S(2, 0, 1)}); - hr.RunInterpOnly(); - EXPECT_EQ(hr.GetFprBitsInterp(2), 0x3f490fdbu) << "reversed operands: no deficit"; - - // The fast path, gamefix off, still produces the IEEE product. Recorded so - // the divergence is pinned rather than discovered later as a surprise. - EeRecTestHarness hj; - hj.EnableCop1(); - hj.SetFprBits(0, 0x3e800000u); - hj.SetFprBits(1, 0x40490fdbu); - hj.LoadProgram({ee::MUL_S(2, 0, 1)}); - hj.RunJitNoDiff(); - EXPECT_EQ(hj.GetFprBitsJit(2), 0x3f490fdbu); + struct Leg { u32 fs, ft, want, want_fast; const char* what; }; + static const Leg legs[] = { + {0x3e800000u, 0x40490fdbu, 0x3f490fdau, 0x3f490fdbu, "0.25 * pi"}, + {0x40490fdbu, 0x3e800000u, 0x3f490fdbu, 0x3f490fdbu, "reversed: predicate off"}, + }; + auto run = [](const Leg& l, int mode, bool interp) { + EeRecTestHarness h; + h.EnableCop1(); + if (mode >= 4) + h.EnableFpuExactMode(); + else if (mode >= 3) + h.EnableFpuFullMode(); + else if (mode >= 2) + h.EnableFpuExtraOverflow(); + h.SetFprBits(0, l.fs); + h.SetFprBits(1, l.ft); + h.LoadProgram({ee::MUL_S(2, 0, 1)}); + if (interp) + { + h.RunInterpOnly(); + return h.GetFprBitsInterp(2); + } + h.RunJitNoDiff(); + return h.GetFprBitsJit(2); + }; + for (const Leg& l : legs) + { + SCOPED_TRACE(l.what); + EXPECT_EQ(run(l, 1, true), l.want) << "interp"; + EXPECT_EQ(run(l, 1, false), l.want_fast) << "eeClampMode 1, the fast path"; + EXPECT_EQ(run(l, 2, false), l.want_fast) << "eeClampMode 2, still the fast path"; + EXPECT_EQ(run(l, 3, false), l.want) << "eeClampMode 3"; + EXPECT_EQ(run(l, 4, false), l.want) << "eeClampMode 4"; + } + // Liveness: the two orders have to disagree, or neither the asymmetry nor + // the fast path's gap is being tested. + ASSERT_NE(legs[0].want, legs[1].want); + ASSERT_NE(legs[0].want, legs[0].want_fast); } TEST(EeRecFpu, MulSMultiplierDeficitMatchesSilicon) @@ -1519,6 +1507,12 @@ TEST(EeRecFpu, MulSMultiplierDeficitReachesResultsWithANonZeroTail) // 2^15 and kept reading ft passes the first group and fails the next two; // the old zero-tail form fails the first two and passes the last two. The // corpus contains no row in any of them. + // + // Mode 4 guards for this band and calls the same eeMulOneUlpLow as the + // interpreter, so the two answer it alike. Mode 3's predicate is the Booth + // term on a zero tail and every row here has a non-zero one, so it returns + // the correctly-rounded product, read off mode 1 rather than hand-derived. + // This band is what the array call buys over mode 3. struct Row { u32 fs, ft, want; }; static const Row rows[] = { // One ULP low with a non-zero tail: the old ft-only form said "exact" @@ -1541,26 +1535,55 @@ TEST(EeRecFpu, MulSMultiplierDeficitReachesResultsWithANonZeroTail) {0x3F800001u, 0x3F808000u, 0x3F808001u}, // tail 0x8000 {0x3F800002u, 0x3F804002u, 0x3F804004u}, // tail 0x8004 }; - for (const Row& r : rows) - { + // One leg per scope: a harness restores the clamp mode in its destructor, + // so a mode-4 harness still alive carries mode 4 into the next leg. + auto run = [](const Row& r, int mode, bool interp) { EeRecTestHarness h; h.EnableCop1(); + if (mode >= 4) + h.EnableFpuExactMode(); + else if (mode >= 3) + h.EnableFpuFullMode(); h.SetFprBits(0, r.fs); h.SetFprBits(1, r.ft); h.LoadProgram({ee::MUL_S(2, 0, 1)}); - h.RunInterpOnly(); - EXPECT_EQ(h.GetFprBitsInterp(2), r.want) - << "mul.s fs=" << std::hex << r.fs << " ft=" << r.ft; + if (interp) + { + h.RunInterpOnly(); + return h.GetFprBitsInterp(2); + } + h.RunJitNoDiff(); + return h.GetFprBitsJit(2); + }; + + int band_rows = 0; + for (const Row& r : rows) + { + SCOPED_TRACE(::testing::Message() << "mul.s fs=" << std::hex << r.fs + << " ft=" << r.ft); + EXPECT_EQ(run(r, 1, true), r.want) << "interp"; + EXPECT_EQ(run(r, 4, false), r.want) << "eeClampMode 4"; + const u32 rounded = run(r, 1, false); + EXPECT_EQ(run(r, 3, false), rounded) + << "eeClampMode 3 cannot reach this band"; + if (r.want != rounded) + ++band_rows; } + // Liveness: six rows the array call moves, the first two groups. The third + // group is inside the gate and the array calls it exact; the fourth has a + // tail that absorbs the borrow. + EXPECT_EQ(band_rows, 6) + << "the table stopped covering the band the array call exists for"; } -TEST(EeRecFpu, MaddSFpuMulHackAppliesToProduct) +TEST(EeRecFpu, MaddSReachesTheMulHackProductFromModeThree) { - // MADD routes its multiply through the same helper: ACC=0 + hack(Fs*Ft) -> - // the patched product. Proves the family-wide wiring, not just MUL_S. + // MADD's multiply stage is a separate emit site from MUL's, so the pair + // above is checked through it too: ACC = +0 leaves MADD landing on the + // product alone. EeRecTestHarness h; h.EnableCop1(); - h.EnableFpuMulHack(); + h.EnableFpuFullMode(); h.SetAccBits(0x00000000u); // +0 h.SetFprBits(0, 0x3e800000u); h.SetFprBits(1, 0x40490fdbu); diff --git a/tests/ctest/core/recompilers/ee_rec_fuzz_tests.cpp b/tests/ctest/core/recompilers/ee_rec_fuzz_tests.cpp index 6d67001496..e670449f2d 100644 --- a/tests/ctest/core/recompilers/ee_rec_fuzz_tests.cpp +++ b/tests/ctest/core/recompilers/ee_rec_fuzz_tests.cpp @@ -565,3 +565,63 @@ TEST(EeFuzz, DelaySlotTargetLoopMix) } } } + +// ── The callee-saved NEON budget ───────────────────────────────────────────── +// What is left of [NEON_CALLEE_SAVED_START, NEON_CALLEE_SAVED_END) after the +// parked constants is the whole supply for guest quads: _allocGPRtoNEONreg +// searches that range alone and _getFreeArm64NEON pxFailRels once it is empty. +// Three slots still carry the whole recompiler suite, two abort in that +// pxFailRel, and a fourth costs an FPU multiply-accumulate its fourth +// call-surviving FPR home. q11 is parked, so five are left. +// +// The probes carry high-water marks of the per-op needed set. +u32 eeTestNeonCalleeSavedSlots(); // iCore-arm64.cpp +void eeTestResetNeonNeededPeak(); // +u32 eeTestNeonGprNeededPeak(); // +u32 eeTestNeonRangeNeededPeak(); // + +TEST(EeFuzz, CalleeSavedNeonBudget) +{ + ASSERT_EQ(eeTestNeonCalleeSavedSlots(), 5u); + + // Three by construction: rd, rs and rt distinct, none of them $zero. + { + Lcg r{1}; + EeRecTestHarness h; + SeedState(h, r); + eeTestResetNeonNeededPeak(); + h.LoadProgram({e::PADDW(8, 9, 10), e::PSUBW(11, 12, 13)}); + h.Run(); + EXPECT_EQ(eeTestNeonGprNeededPeak(), 3u); + } + + // Three by generation: 48000 ops of the straight-line mix, whose MMI forms + // share an 8-register focus set with LQ/SQ and the scalar traffic. + eeTestResetNeonNeededPeak(); + const u32 start = seedStart(), count = seedCount(400); + for (u32 seed = start; seed < start + count; ++seed) + { + Lcg r{seed * 0x9E3779B97F4A7C15ull + 0xDEADBEEFull}; + const Focus f = makeFocus(r); + EeRecTestHarness h; + SeedState(h, r); + std::vector prog; + for (u32 i = 0; i < 120; ++i) + prog.push_back(genOp(r, f)); + h.LoadProgram(prog); + h.Run(); + } + EXPECT_EQ(eeTestNeonGprNeededPeak(), 3u); + + // The fourth slot is not headroom either. FPR homes prefer the same range + // (_allocFPtoNEONreg), and a multiply-accumulate's Fd, Fs, Ft and ACC take + // four of them at once. + { + EeRecTestHarness h; + h.EnableCop1(); + eeTestResetNeonNeededPeak(); + h.LoadProgram({e::MADD_S(1, 2, 3), e::MUL_S(4, 5, 6), e::MADD_S(7, 8, 9)}); + h.Run(); + EXPECT_EQ(eeTestNeonRangeNeededPeak(), 4u); + } +} diff --git a/tests/ctest/core/recompilers/ee_rec_harness_validation_tests.cpp b/tests/ctest/core/recompilers/ee_rec_harness_validation_tests.cpp index d08d4d338e..a67978cd45 100644 --- a/tests/ctest/core/recompilers/ee_rec_harness_validation_tests.cpp +++ b/tests/ctest/core/recompilers/ee_rec_harness_validation_tests.cpp @@ -14,6 +14,8 @@ #include "harness/EeRecTestHarness.h" +#include "Config.h" +#include "EeFpuFormat.h" #include "R5900.h" #include @@ -123,3 +125,63 @@ TEST(EeRecHarnessValidation, RecLutMapsRom2AndMirrors) // Guard: the page just past ROM2 stays unmapped. EXPECT_FALSE(recEeIsPcMapped(0x1e800000u)); } + +// An FPR slot does not say which format it is in (R5900.h, EeFpuFormat.h) and +// the global that does moves with eeClampMode, so a snapshot carries its own. +// Two harnesses alive at once separate them: the interpreter runs with raw +// words in the low half of each slot, the mode-3 harness relocates the file, +// and the interpreter's results are read after that. +TEST(EeRecHarnessValidation, ASnapshotKeepsTheFprFormatItWasCapturedIn) +{ + constexpr u32 kA = 0x40490FDBu, kB = 0x3FB504F3u; + constexpr u32 kSum = 0x4091C92Au; // pi + sqrt(2) + + EeRecTestHarness hi; + hi.EnableCop1(); + hi.SetFprBits(0, kA); + hi.SetFprBits(1, kB); + hi.LoadProgram({ee::ADD_S(2, 0, 1)}); + hi.RunInterpOnly(); + ASSERT_EQ(hi.GetFprBitsInterp(2), kSum); + + // Deliberately outlives the reads below: the destructor puts the clamp mode + // and the file's format back, which would hide exactly what this pins. + EeRecTestHarness h; + h.EnableCop1(); + h.EnableFpuFullMode(); + h.SetFprBits(0, kA); + h.SetFprBits(1, kB); + h.LoadProgram({ee::ADD_S(2, 0, 1)}); + h.RunJitNoDiff(); + + EXPECT_EQ(h.GetFprBitsJit(2), kSum) << "mode 3, read under mode 3"; + EXPECT_EQ(hi.GetFprBitsInterp(2), kSum) << "mode 0 snapshot, read under mode 3"; +} + +// Restoring means putting the snapshot's words back, so when the file has +// changed format since the capture the slots are re-encoded on the way in. +// A plain memcpy would write mode-0 words into a relocated file and every one +// of them would read as a different number. +TEST(EeRecHarnessValidation, RestoringASnapshotConvertsToTheFilesFormat) +{ + constexpr u32 kWord = 0x40490FDBu; + + EeRecTestHarness h; + h.EnableCop1(); + h.SetFprBits(3, kWord); + h.LoadProgram({ee::ADD_S(2, 3, 3)}); + h.RunInterpOnly(); + ASSERT_FALSE(h.InterpSnapshot().fprs_relocated); + + const bool saved = EmuConfig.Cpu.Recompiler.fpuFullMode; + EmuConfig.Cpu.Recompiler.fpuFullMode = true; + eeFprSyncSlotFormat(); + ASSERT_TRUE(g_eeFprSlotsRelocated); + + h.InterpSnapshot().Restore(); + EXPECT_EQ(fpuRegs.fpr[3].Word(), kWord); + EXPECT_EQ(fpuRegs.fpr[3].UD, eeFprWidenBits(kWord)); + + EmuConfig.Cpu.Recompiler.fpuFullMode = saved; + eeFprSyncSlotFormat(); +} diff --git a/tests/ctest/core/recompilers/ee_vu0_cop2_clamp_residency_tests.cpp b/tests/ctest/core/recompilers/ee_vu0_cop2_clamp_residency_tests.cpp index 6daefd16eb..1deabb183e 100644 --- a/tests/ctest/core/recompilers/ee_vu0_cop2_clamp_residency_tests.cpp +++ b/tests/ctest/core/recompilers/ee_vu0_cop2_clamp_residency_tests.cpp @@ -252,22 +252,20 @@ TEST(EeVu0Cop2ClampResidency, SyncStubsReDupClampConsts) } // q25/q26 are reserved out of the EE NEON allocator pool (like q8/q9, and like -// q10 = NEON_RESERVED_FPU_MULMASK); the rest of the pool is untouched. +// q11 = NEON_RESERVED_EEFPU_UNSCALE); the rest of the pool is untouched. // -// q10 moved from the usable list to the reserved list when the mode-3 -// multiplier-defect mask was parked there. That move is the whole reservation: -// before it, _getFreeArm64NEON would hand q10 out as an ordinary FPR home, and -// the first allocation to land there would have overwritten the mask and turned -// the predicate off for every later multiply in the block. Note also what this -// test always said about q16-q24 — they are allocator registers, not the free -// scratch space a `grep` for literal `v16`..`v24` tokens suggests (the COP2 VF -// residency cache takes q16-q20 through computed indices, and evicts allocator -// residency to do it). +// Without the reservation _getFreeArm64NEON would hand the register out as an +// ordinary FPR home, and the first allocation to land there would overwrite the +// constant for the rest of the block. Note also +// what this test always said about q16-q24 — they are allocator registers, not +// the free scratch space a `grep` for literal `v16`..`v24` tokens suggests (the +// COP2 VF residency cache takes q16-q20 through computed indices, and evicts +// allocator residency to do it). TEST(EeVu0Cop2ClampResidency, EeAllocatorReservesClampRegs) { - for (int reserved : {8, 9, 10, 25, 26}) + for (int reserved : {8, 9, 11, 25, 26}) EXPECT_TRUE(eeTestNeonRegIsReserved(reserved)) << "q" << reserved; - for (int usable : {0, 7, 11, 15, 16, 24, 27, 28}) + for (int usable : {0, 7, 10, 12, 15, 16, 24, 27, 28}) EXPECT_FALSE(eeTestNeonRegIsReserved(usable)) << "q" << usable; } diff --git a/tests/ctest/core/recompilers/ee_vu0_cop2_madd_clamp_tests.cpp b/tests/ctest/core/recompilers/ee_vu0_cop2_madd_clamp_tests.cpp new file mode 100644 index 0000000000..347466baa9 --- /dev/null +++ b/tests/ctest/core/recompilers/ee_vu0_cop2_madd_clamp_tests.cpp @@ -0,0 +1,169 @@ +// SPDX-FileCopyrightText: 2026 ARMSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +// COP2 macro-mode MADD/MSUB operand clamping: fd = ACC +/- VF[fs] * VF[ft]. +// +// An exponent-0xFF word is an ordinary large number to the VU, so operands are +// clamped to +/-FLT_MAX before a host FMUL/FADD sees them. microVU_Upper-arm64.inl +// sets the clamp per op, and two rows differ under isCOP2: +// +// mVU_MADDw (isCOP2) ? (cACC|cFt|cFs) : cFs +// mVU_MSUB (isCOP2) ? cFs : 0 +// +// arm64 hand-rolls these ops rather than going through mVU, so those rows are +// retyped in iCOP2-arm64.cpp and were missing. The oracle is the VU0 +// interpreter, which reads every operand through vuDouble(); rows the table +// gives no clamp (MADD, MSUBx/y/z/w) diverge by design and are not asserted. + +#include "harness/EeRecTestHarness.h" + +#include "VU.h" +#include "VUmicro.h" +#include "Config.h" + +#include + +namespace recompiler_tests { + +using namespace mips; +using namespace mips::ee; +using namespace vu; + +namespace { + +// Exponent-0xFF words. As host floats these are Inf and NaN; to the VU they are +// 2^128 scaled by their mantissa, and 0x7FFFFFFF is simply the largest value a +// VU register can hold. +constexpr u32 kPosInf = 0x7F800000u; +constexpr u32 kNegInf = 0xFF800000u; +constexpr u32 kPosNan = 0x7FC00000u; +constexpr u32 kMaxExpFf = 0x7FFFFFFFu; +constexpr u32 kZero = 0x00000000u; +constexpr u32 kOne = 0x3F800000u; + +struct Case +{ + const char* what; + u32 fs[4]; + u32 ft[4]; + u32 acc[4]; +}; + +// A zero Fs lane against an exp-0xFF broadcast: 0 * Inf = NaN on the host, +// 0 * 2^128 = 0 on the VU. It is the shape a homogeneous transform produces. +const Case kCases[] = { + {"zero Fs, exp-FF Ft", + {kZero, kZero, kZero, kZero}, + {kOne, kOne, kOne, kPosInf}, + {kOne, kOne, kOne, kOne}}, + {"zero Fs, NaN-pattern Ft", + {kZero, kZero, kZero, kZero}, + {kOne, kOne, kOne, kPosNan}, + {kOne, kOne, kOne, kOne}}, + {"zero Fs, largest exp-FF Ft", + {kZero, kZero, kZero, kZero}, + {kOne, kOne, kOne, kMaxExpFf}, + {kOne, kOne, kOne, kOne}}, + {"zero Fs, -inf Ft, negative ACC", + {kZero, kZero, kZero, kZero}, + {kOne, kOne, kOne, kNegInf}, + {0xBF800000u, 0xBF800000u, 0xBF800000u, 0xBF800000u}}, + {"exp-FF Ft and exp-FF ACC", + {kOne, kOne, kOne, kOne}, + {kPosInf, kPosInf, kPosInf, kPosInf}, + {kNegInf, kNegInf, kNegInf, kNegInf}}, + // cACC on its own. The product overflows to a host infinity and a raw exp-FF + // ACC is the opposite one, so the add gives NaN. An in-range product does not + // separate the two — the result clamp folds both answers to the same word. + {"exp-FF ACC cancelling an overflowed product", + {0xFF000000u, 0xFF000000u, 0xFF000000u, 0xFF000000u}, + {kOne, kOne, kOne, 0x7F000000u}, + {kPosInf, kPosInf, kPosInf, kPosInf}}, +}; + +void CheckVfCase(const char* opName, u32 code, u32 fdReg, u32 mask, const Case& c) +{ + EeRecTestHarness h; + h.EnableVu0Capture(); + h.EnableCop1(); + h.SeedVu0VfBits(1, c.fs[0], c.fs[1], c.fs[2], c.fs[3]); + h.SeedVu0VfBits(2, c.ft[0], c.ft[1], c.ft[2], c.ft[3]); + h.SeedVu0VfBits(fdReg, kZero, kZero, kZero, kZero); + h.SeedVu0AccBits(c.acc[0], c.acc[1], c.acc[2], c.acc[3]); + h.LoadProgram({code}); + h.Run(); + + for (char l : {'x', 'y', 'z', 'w'}) + { + EXPECT_EQ(h.GetVu0VfBitsJit(fdReg, l), h.GetVu0VfBitsInterp(fdReg, l)) + << opName << " (" << c.what << ") dest mask 0x" << std::hex << mask + << std::dec << " vf" << fdReg << "." << l + << ": JIT and interpreter disagree"; + } +} + +} // namespace + +// One case per half of MADDw's set, so dropping either cFt or cACC fails here. +TEST(EeVu0Cop2MaddClamp, MaddwClampsFtAndAccLikeInterp) +{ + for (const Case& c : kCases) + { + for (u32 mask = 1; mask <= 0xF; mask++) + { + SCOPED_TRACE(::testing::Message() + << "VMADDw mask=0x" << std::hex << mask << " " << c.what); + CheckVfCase("VMADDw", VMADDw_C2(mask, /*fd*/3, /*fs*/1, /*ft*/2), 3, mask, c); + } + } +} + +// Scope control: the sweep above is exotic enough that a harness fault would +// read as a fix. Broadcast lanes stay in range — MADDx/y/z have no cFt, so an +// exp-FF lane through them is a by-design divergence and is not assertable. +TEST(EeVu0Cop2MaddClamp, MaddxyzKeepTheirOwnClampSet) +{ + const struct { const char* name; u32 (*enc)(u32, u32, u32, u32); } ops[] = { + {"VMADDx", VMADDx_C2}, {"VMADDy", VMADDy_C2}, {"VMADDz", VMADDz_C2}, + }; + const Case c = {"in-range operands", + {kOne, kOne, kOne, kOne}, + {kOne, kOne, kOne, kPosInf}, + {kOne, kOne, kOne, kOne}}; + for (const auto& op : ops) + { + for (u32 mask = 1; mask <= 0xF; mask++) + { + SCOPED_TRACE(::testing::Message() + << op.name << " mask=0x" << std::hex << mask); + CheckVfCase(op.name, op.enc(mask, /*fd*/3, /*fs*/1, /*ft*/2), 3, mask, c); + } + } +} + +// The table's other isCOP2 row. MSUBx/y/z/w keep clampType 0 and are not +// asserted. +TEST(EeVu0Cop2MaddClamp, MsubClampsFsLikeInterp) +{ + const Case fsCases[] = { + {"exp-FF Fs against a zero Ft", + {kPosInf, kNegInf, kPosNan, kMaxExpFf}, + {kZero, kZero, kZero, kZero}, + {kOne, kOne, kOne, kOne}}, + {"exp-FF Fs against a one Ft", + {kPosInf, kNegInf, kPosNan, kMaxExpFf}, + {kOne, kOne, kOne, kOne}, + {kZero, kZero, kZero, kZero}}, + }; + for (const Case& c : fsCases) + { + for (u32 mask = 1; mask <= 0xF; mask++) + { + SCOPED_TRACE(::testing::Message() + << "VMSUB mask=0x" << std::hex << mask << " " << c.what); + CheckVfCase("VMSUB", VMSUB_C2(mask, /*fd*/3, /*fs*/1, /*ft*/2), 3, mask, c); + } + } +} + +} // namespace recompiler_tests diff --git a/tests/ctest/core/recompilers/harness/EeRecTestHarness.cpp b/tests/ctest/core/recompilers/harness/EeRecTestHarness.cpp index 33f6bd5864..2d4621ed5e 100644 --- a/tests/ctest/core/recompilers/harness/EeRecTestHarness.cpp +++ b/tests/ctest/core/recompilers/harness/EeRecTestHarness.cpp @@ -104,12 +104,12 @@ EeRecTestHarness::~EeRecTestHarness() EmuConfig.Speedhacks.vu1Instant = false; } + if (fpu_exact_mode_changed_) + EmuConfig.Cpu.Recompiler.fpuExactMode = prev_fpu_exact_mode_; + if (fpu_full_mode_changed_) EmuConfig.Cpu.Recompiler.fpuFullMode = prev_fpu_full_mode_; - if (fpu_mul_hack_changed_) - EmuConfig.Gamefixes.FpuMulHack = prev_fpu_mul_hack_; - if (fpu_guarded_changed_) EmuConfig.Cpu.Recompiler.fpuGuardedAddSub = prev_fpu_guarded_; @@ -118,6 +118,10 @@ EeRecTestHarness::~EeRecTestHarness() if (fpu_overflow_changed_) EmuConfig.Cpu.Recompiler.fpuOverflow = prev_fpu_overflow_; + + // Hand the file back in the format the restored mode calls for, so the next + // harness -- and anything that reads fpuRegs between them -- starts square. + eeFprSyncSlotFormat(); } void EeRecTestHarness::SetGpr64(u32 reg_idx, u64 value) @@ -143,17 +147,21 @@ void EeRecTestHarness::SetHiPair(u64 lo_qw, u64 hi_qw) { cpuRegs.HI.UD[0] = lo_q void EeRecTestHarness::SetCp0(u32 reg_idx, u32 value) { cpuRegs.CP0.r[reg_idx] = value; } -void EeRecTestHarness::SetFpr(u32 reg_idx, float value) { fpuRegs.fpr[reg_idx].f = value; } -void EeRecTestHarness::SetFprBits(u32 reg_idx, u32 bits) { fpuRegs.fpr[reg_idx].UL = bits; } -void EeRecTestHarness::SetAcc(float value) { fpuRegs.ACC.f = value; } -void EeRecTestHarness::SetAccBits(u32 bits) { fpuRegs.ACC.UL = bits; } +void EeRecTestHarness::SetFpr(u32 reg_idx, float value) { u32 b; std::memcpy(&b, &value, 4); fpuRegs.fpr[reg_idx].SetWord(b); } +void EeRecTestHarness::SetFprBits(u32 reg_idx, u32 bits) { fpuRegs.fpr[reg_idx].SetWord(bits); } +void EeRecTestHarness::SetAcc(float value) { u32 b; std::memcpy(&b, &value, 4); fpuRegs.ACC.SetWord(b); } +void EeRecTestHarness::SetAccBits(u32 bits) { fpuRegs.ACC.SetWord(bits); } void EeRecTestHarness::SetFcr31(u32 value) { fpuRegs.fprc[31] = value; } void EeRecTestHarness::EnableCop0() { cpuRegs.CP0.n.Status.val |= (1u << 28); /* CU0 */ } void EeRecTestHarness::EnableCop1() { cpuRegs.CP0.n.Status.val |= (1u << 29); /* CU1 */ } +// GameDatabase sets the four bits from a single eeClampMode and ApplySanityCheck +// rejects a higher one without the lower, so each helper below sets a whole +// mode. void EeRecTestHarness::EnableFpuFullMode() { + EnableFpuExtraOverflow(); if (!fpu_full_mode_changed_) { prev_fpu_full_mode_ = EmuConfig.Cpu.Recompiler.fpuFullMode; @@ -162,14 +170,15 @@ void EeRecTestHarness::EnableFpuFullMode() EmuConfig.Cpu.Recompiler.fpuFullMode = true; } -void EeRecTestHarness::EnableFpuMulHack() +void EeRecTestHarness::EnableFpuExactMode() { - if (!fpu_mul_hack_changed_) + EnableFpuFullMode(); + if (!fpu_exact_mode_changed_) { - prev_fpu_mul_hack_ = EmuConfig.Gamefixes.FpuMulHack; - fpu_mul_hack_changed_ = true; + prev_fpu_exact_mode_ = EmuConfig.Cpu.Recompiler.fpuExactMode; + fpu_exact_mode_changed_ = true; } - EmuConfig.Gamefixes.FpuMulHack = true; + EmuConfig.Cpu.Recompiler.fpuExactMode = true; } void EeRecTestHarness::DisableFpuGuarded() @@ -184,6 +193,12 @@ void EeRecTestHarness::DisableFpuGuarded() void EeRecTestHarness::EnableFpuExtraOverflow() { + if (!fpu_overflow_changed_) + { + prev_fpu_overflow_ = EmuConfig.Cpu.Recompiler.fpuOverflow; + fpu_overflow_changed_ = true; + } + EmuConfig.Cpu.Recompiler.fpuOverflow = true; if (!fpu_extra_overflow_changed_) { prev_fpu_extra_overflow_ = EmuConfig.Cpu.Recompiler.fpuExtraOverflow; @@ -368,6 +383,11 @@ void EeRecTestHarness::Run(RunMode mode) ASSERT_FALSE(program_words_.empty()) << "LoadProgram() must be called before Run()"; + // The clamp mode may have been set either side of the SetFpr* calls and it + // decides what a slot holds. Production reaches this through the code-cache + // reset a mode change forces; here the mode is poked into EmuConfig. + eeFprSyncSlotFormat(); + SeedEntryState(); pre_snapshot_ = EeSnapshot::Capture(mem_windows_); @@ -490,6 +510,8 @@ void EeRecTestHarness::RunJitNoDiff(RunMode mode) ASSERT_FALSE(program_words_.empty()) << "LoadProgram() must be called before RunJitNoDiff()"; + eeFprSyncSlotFormat(); // see Run() + SeedEntryState(); pre_snapshot_ = EeSnapshot::Capture(mem_windows_); @@ -546,10 +568,10 @@ u64 EeRecTestHarness::GetHiUpper64Jit () const { return jit_snapshot_.regs.HI u64 EeRecTestHarness::GetLoUpper64Jit () const { return jit_snapshot_.regs.LO.UD[1]; } u64 EeRecTestHarness::GetGprUpper64Interp(u32 r) const { return interp_snapshot_.regs.GPR.r[r].UD[1]; } u64 EeRecTestHarness::GetGprUpper64Jit (u32 r) const { return jit_snapshot_.regs.GPR.r[r].UD[1]; } -u32 EeRecTestHarness::GetFprBitsInterp(u32 r) const { return interp_snapshot_.fprs.fpr[r].UL; } -u32 EeRecTestHarness::GetFprBitsJit (u32 r) const { return jit_snapshot_.fprs.fpr[r].UL; } -u32 EeRecTestHarness::GetAccBitsInterp() const { return interp_snapshot_.fprs.ACC.UL; } -u32 EeRecTestHarness::GetAccBitsJit () const { return jit_snapshot_.fprs.ACC.UL; } +u32 EeRecTestHarness::GetFprBitsInterp(u32 r) const { return interp_snapshot_.FprWord(r); } +u32 EeRecTestHarness::GetFprBitsJit (u32 r) const { return jit_snapshot_.FprWord(r); } +u32 EeRecTestHarness::GetAccBitsInterp() const { return interp_snapshot_.AccWord(); } +u32 EeRecTestHarness::GetAccBitsJit () const { return jit_snapshot_.AccWord(); } u32 EeRecTestHarness::GetCp0Interp(u32 r) const { return interp_snapshot_.regs.CP0.r[r]; } u32 EeRecTestHarness::GetCp0Jit (u32 r) const { return jit_snapshot_.regs.CP0.r[r]; } @@ -571,14 +593,14 @@ void EeRecTestHarness::ExpectGpr128(u32 reg_idx, u64 lo, u64 hi) const void EeRecTestHarness::ExpectFpr(u32 reg_idx, u32 bits) const { - EXPECT_EQ(interp_snapshot_.fprs.fpr[reg_idx].UL, bits) << "fpr" << reg_idx << " (interp)"; - EXPECT_EQ(jit_snapshot_.fprs.fpr[reg_idx].UL, bits) << "fpr" << reg_idx << " (jit)"; + EXPECT_EQ(interp_snapshot_.FprWord(reg_idx), bits) << "fpr" << reg_idx << " (interp)"; + EXPECT_EQ(jit_snapshot_.FprWord(reg_idx), bits) << "fpr" << reg_idx << " (jit)"; } void EeRecTestHarness::ExpectAcc(u32 bits) const { - EXPECT_EQ(interp_snapshot_.fprs.ACC.UL, bits) << "ACC (interp)"; - EXPECT_EQ(jit_snapshot_.fprs.ACC.UL, bits) << "ACC (jit)"; + EXPECT_EQ(interp_snapshot_.AccWord(), bits) << "ACC (interp)"; + EXPECT_EQ(jit_snapshot_.AccWord(), bits) << "ACC (jit)"; } // ---- VU0 cross-tree handoff helpers ---- diff --git a/tests/ctest/core/recompilers/harness/EeRecTestHarness.h b/tests/ctest/core/recompilers/harness/EeRecTestHarness.h index 41ad9538ad..837dda47b8 100644 --- a/tests/ctest/core/recompilers/harness/EeRecTestHarness.h +++ b/tests/ctest/core/recompilers/harness/EeRecTestHarness.h @@ -67,7 +67,11 @@ public: // a hand-computed double-mode value rather than the auto-diffing Run(). Restored // to its previous value in the dtor. void EnableFpuFullMode(); - void EnableFpuMulHack(); + + // Enables "Exact" (CHECK_FPU_EXACT / GameDB eeClampMode:4): mode 3 plus the + // rest of the EE multiplier's one-ULP deficit. Implies EnableFpuFullMode(); + // restored in the dtor. + void EnableFpuExactMode(); // Turns ON the (default-OFF) fpuExtraOverflow Recompiler option — GameDB // eeClampMode >= 2, CHECK_FPU_EXTRA_OVERFLOW — so the JIT clamps each fpr @@ -381,8 +385,8 @@ private: bool fpu_full_mode_changed_ = false; bool prev_fpu_full_mode_ = false; - bool fpu_mul_hack_changed_ = false; - bool prev_fpu_mul_hack_ = false; + bool fpu_exact_mode_changed_ = false; + bool prev_fpu_exact_mode_ = false; bool fpu_guarded_changed_ = false; bool prev_fpu_guarded_ = false; bool fpu_extra_overflow_changed_ = false; diff --git a/tests/ctest/core/recompilers/harness/StateSnapshot.cpp b/tests/ctest/core/recompilers/harness/StateSnapshot.cpp index 3ad065d13e..b7f1556da9 100644 --- a/tests/ctest/core/recompilers/harness/StateSnapshot.cpp +++ b/tests/ctest/core/recompilers/harness/StateSnapshot.cpp @@ -9,6 +9,7 @@ #include "R5900.h" #include +#include #include namespace recompiler_tests { @@ -142,11 +143,23 @@ void CopyToEeMem(u32 addr, size_t count, const u8* src) } // namespace +u32 EeSnapshot::FprWord(u32 reg_idx) const +{ + const u64 slot = fprs.fpr[reg_idx].UD; + return fprs_relocated ? eeFprNarrowBits(slot) : static_cast(slot); +} + +u32 EeSnapshot::AccWord() const +{ + return fprs_relocated ? eeFprNarrowBits(fprs.ACC.UD) : static_cast(fprs.ACC.UD); +} + EeSnapshot EeSnapshot::Capture(const std::vector& windows_to_capture) { EeSnapshot s; std::memcpy(&s.regs, &cpuRegs, sizeof(cpuRegisters)); std::memcpy(&s.fprs, &fpuRegs, sizeof(fpuRegisters)); + s.fprs_relocated = g_eeFprSlotsRelocated; s.mem_windows.reserve(windows_to_capture.size()); for (const auto& w : windows_to_capture) { @@ -161,6 +174,14 @@ void EeSnapshot::Restore() const { std::memcpy(&cpuRegs, ®s, sizeof(cpuRegisters)); std::memcpy(&fpuRegs, &fprs, sizeof(fpuRegisters)); + // The memcpy put back the slots as they were written. If the file has since + // changed format, what the snapshot means is its words, so re-encode them. + if (fprs_relocated != g_eeFprSlotsRelocated) + { + for (u32 i = 0; i < std::size(fpuRegs.fpr); i++) + fpuRegs.fpr[i].SetWord(FprWord(i)); + fpuRegs.ACC.SetWord(AccWord()); + } for (const auto& w : mem_windows) CopyToEeMem(w.addr, w.bytes.size(), w.bytes.data()); } @@ -228,13 +249,13 @@ std::vector DiffEe(const EeSnapshot& a, const EeSnapshot& b) for (int i = 0; i < 32; ++i) { std::string name = "fpr[" + std::to_string(i) + "]"; - emit32(name.c_str(), a.fprs.fpr[i].UL, b.fprs.fpr[i].UL); + emit32(name.c_str(), a.FprWord(static_cast(i)), b.FprWord(static_cast(i))); } // PS2 FPU accumulator — written by ADDA/SUBA/MULA/MADDA/MSUBA and read // by MADD/MSUB. Diverging ACC corrupts geometry/lighting silently if // not included in the diff. - emit32("ACC", a.fprs.ACC.UL, b.fprs.ACC.UL); + emit32("ACC", a.AccWord(), b.AccWord()); emit32("pc", a.regs.pc, b.regs.pc); emit32("sa", a.regs.sa, b.regs.sa); diff --git a/tests/ctest/core/recompilers/harness/StateSnapshot.h b/tests/ctest/core/recompilers/harness/StateSnapshot.h index 7ac77bb145..d797a4e1cb 100644 --- a/tests/ctest/core/recompilers/harness/StateSnapshot.h +++ b/tests/ctest/core/recompilers/harness/StateSnapshot.h @@ -63,9 +63,17 @@ struct EeSnapshot { cpuRegisters regs{}; fpuRegisters fprs{}; + // g_eeFprSlotsRelocated as it stood when `fprs` was copied: a snapshot taken + // under one clamp mode and read under another decodes to garbage without it. + // Read the FPRs through FprWord / AccWord, never fprs.fpr[i].Word(). + bool fprs_relocated = false; std::vector mem_windows; + u32 FprWord(u32 reg_idx) const; + u32 AccWord() const; + static EeSnapshot Capture(const std::vector& windows_to_capture); + // Puts the file back, converting the FPRs if the format has moved since. void Restore() const; static void ZeroGlobals(); };