Merge remote-tracking branch 'yaps2/main' into jit-transplant

# Conflicts:
#	pcsx2/arm64/BaseblockEx-arm64.h
This commit is contained in:
Brian Degenhardt
2026-07-21 19:04:47 -07:00
27 changed files with 1120 additions and 83 deletions
+92
View File
@@ -0,0 +1,92 @@
# DESCRIPTION: GitLab CI/CD for libRetro (NOT FOR GitLab-proper)
#
# Builds yaps2_libretro.so on the libretro buildbot (git.libretro.com mirror)
# for Linux x86_64 and aarch64. Inert on GitHub — the GitHub Actions
# workflows under .github/workflows/ are the project's own CI.
#
# The buildbot containers ship no Clang and none of the pinned third-party
# libraries, so before_script installs clang-17 from apt.llvm.org and reuses
# .github/workflows/scripts/linux/build-dependencies-runner.sh to build the
# same dependency set the GitHub nightly uses (shaderc, SDL3, zstd, lz4, ...).
##############################################################################
################################# BOILERPLATE ################################
##############################################################################
# Core definitions
.core-defs:
variables:
GIT_SUBMODULE_STRATEGY: recursive
CORENAME: yaps2
# The core is emitted in the pcsx2-libretro/ subdirectory of the build
# tree; the ci-templates pick it up from $BUILD_DIR/$EXTRA_PATH/.
EXTRA_PATH: pcsx2-libretro
CORE_ARGS: >-
-DCMAKE_C_COMPILER=clang-17
-DCMAKE_CXX_COMPILER=clang++-17
-DCMAKE_EXE_LINKER_FLAGS_INIT=-fuse-ld=lld-17
-DCMAKE_MODULE_LINKER_FLAGS_INIT=-fuse-ld=lld-17
-DCMAKE_SHARED_LINKER_FLAGS_INIT=-fuse-ld=lld-17
-DCMAKE_PREFIX_PATH=$CI_PROJECT_DIR/.deps
-DENABLE_LIBRETRO=ON
-DENABLE_QT_UI=OFF
-DENABLE_SDL_FRONTEND=OFF
-DENABLE_GSRUNNER=OFF
-DENABLE_TESTS=OFF
-DENABLE_RECOMPILER_TEST_HOOKS=OFF
-DENABLE_SETCAP=OFF
-DUSE_BACKTRACE=OFF
-DX11_API=OFF
-DWAYLAND_API=OFF
-DDISABLE_ADVANCE_SIMD=TRUE
-DOVERRIDE_HOST_PAGE_SIZE=4096
-DCMAKE_DISABLE_PRECOMPILE_HEADERS=ON
.core-defs-linux:
extends: .core-defs
before_script:
# The ci-template's script builds with `-j $NUMPROC`; its before_script
# (which normally sets it) is replaced by this one.
- export NUMPROC=$(($(nproc) / 2)); [ "$NUMPROC" -lt 1 ] && export NUMPROC=1
- apt-get update
- >-
DEBIAN_FRONTEND=noninteractive apt-get -y install
build-essential cmake curl git ninja-build nasm pkg-config wget
gnupg lsb-release software-properties-common
zlib1g-dev libaio-dev libasound2-dev libcurl4-openssl-dev
libdbus-1-dev libdrm-dev libegl-dev libevdev-dev libgbm-dev
libgudev-1.0-dev libopengl-dev libpcap-dev libssl-dev libudev-dev
- wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh
- chmod +x /tmp/llvm.sh
- /tmp/llvm.sh 17
- DEBIAN_FRONTEND=noninteractive apt-get -y install lld-17
- ./.github/workflows/scripts/linux/build-dependencies-runner.sh "$CI_PROJECT_DIR/.deps"
# Inclusion templates, required for the build to work
include:
# Linux
- project: 'libretro-infrastructure/ci-templates'
file: '/linux-cmake.yml'
# Stages for building
stages:
- build-prepare
- build-shared
##############################################################################
#################################### STAGES ##################################
##############################################################################
#
################################### DESKTOPS #################################
# Linux 64-bit
libretro-build-linux-x64:
extends:
- .libretro-linux-cmake-x86_64
- .core-defs-linux
image: $CI_SERVER_HOST:5050/libretro-infrastructure/libretro-build-amd64-ubuntu:backports
# Linux 64-bit (ARM)
libretro-build-linux-aarch64:
extends:
- .libretro-linux-cmake-aarch64
- .core-defs-linux
+5
View File
@@ -40,3 +40,8 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
set_target_properties(pcsx2-libretro PROPERTIES
LINK_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/link.T")
endif()
# The libretro buildbot's ci-templates build the target named
# <CORENAME>_libretro (see .gitlab-ci.yml).
add_custom_target(yaps2_libretro)
add_dependencies(yaps2_libretro pcsx2-libretro)
@@ -23,6 +23,7 @@ AdvancedSettingsWidget::AdvancedSettingsWidget(SettingsWindow* settings_dialog,
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.eeFastmem, "EmuCore/CPU/Recompiler", "EnableFastmem", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.pauseOnTLBMiss, "EmuCore/CPU/Recompiler", "PauseOnTLBMiss", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.extraMemory, "EmuCore/CPU", "ExtraMemory", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.eeFpuGuardedAddSub, "EmuCore/CPU/Recompiler", "fpuGuardedAddSub", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.vu0Recompiler, "EmuCore/CPU/Recompiler", "EnableVU0", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.vu1Recompiler, "EmuCore/CPU/Recompiler", "EnableVU1", true);
@@ -99,6 +100,11 @@ AdvancedSettingsWidget::AdvancedSettingsWidget(SettingsWindow* settings_dialog,
dialog()->registerWidgetHelp(m_ui.extraMemory, tr("Enable Extended RAM (Dev Console)"), tr("Unchecked"),
tr("Exposes additional memory to the virtual machine, expanding the EE and IOP memory to 128MB and 8MB respectively."));
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."));
dialog()->registerWidgetHelp(m_ui.vu0RoundingMode, tr("VU0 Rounding Mode"), tr("Chop/Zero (Default)"), tr("Changes how PCSX2 handles rounding while emulating the Emotion Engine's Vector Unit 0 (EE VU0). "
"The default value handles the vast majority of games; <b>modifying this setting when a game is not having a visible problem will cause stability issues and/or crashes.</b>"));
@@ -180,6 +180,13 @@
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="eeFpuGuardedAddSub">
<property name="text">
<string>FPU Add/Sub Guard Bits</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
@@ -555,6 +562,7 @@
<tabstop>eeFastmem</tabstop>
<tabstop>pauseOnTLBMiss</tabstop>
<tabstop>extraMemory</tabstop>
<tabstop>eeFpuGuardedAddSub</tabstop>
<tabstop>vu0RoundingMode</tabstop>
<tabstop>vu0ClampMode</tabstop>
<tabstop>vu1RoundingMode</tabstop>
+3 -1
View File
@@ -680,7 +680,8 @@ struct Pcsx2Config
bool
fpuOverflow : 1,
fpuExtraOverflow : 1,
fpuFullMode : 1;
fpuFullMode : 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
EnableEECache : 1;
@@ -1589,6 +1590,7 @@ namespace EmuFolders
#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_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 ---------------
+4
View File
@@ -5535,6 +5535,10 @@ void FullscreenUI::DrawAdvancedSettingsPage()
DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_MEMORY, "Enable Extended RAM (Dev Console)"),
FSUI_CSTR("Exposes additional memory to the virtual machine, expanding the EE and IOP memory to 128MB and 8MB respectively."),
"EmuCore/CPU", "ExtraMemory", false);
DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_PLUS_MINUS, "FPU Add/Sub Guard Bits"),
FSUI_CSTR("Emulates the EE FPU's missing add/sub guard bits for hardware-accurate results (default). A few games need it; "
"disabling is a minor speedup for EE-FPU-heavy games verified correct without it. No effect in Full clamping mode."),
"EmuCore/CPU/Recompiler", "fpuGuardedAddSub", true);
MenuHeading(FSUI_CSTR("Vector Units"));
DrawIntListSetting(bsi, FSUI_ICONSTR(ICON_FA_ARROW_TREND_DOWN, "VU0 Rounding Mode"),
+2
View File
@@ -473,6 +473,7 @@ Pcsx2Config::RecompilerOptions::RecompilerOptions()
fpuOverflow = true;
//fpuExtraOverflow = false;
//fpuFullMode = false;
fpuGuardedAddSub = true; // PS2-accurate add/sub guard-bit emulation; opt-out for perf on titles verified not to need it.
}
void Pcsx2Config::RecompilerOptions::ApplySanityCheck()
@@ -551,6 +552,7 @@ void Pcsx2Config::RecompilerOptions::LoadSave(SettingsWrapper& wrap)
SettingsWrapBitBool(fpuOverflow);
SettingsWrapBitBool(fpuExtraOverflow);
SettingsWrapBitBool(fpuFullMode);
SettingsWrapBitBool(fpuGuardedAddSub);
}
u32 Pcsx2Config::RecompilerOptions::GetEEClampMode() const
+192 -18
View File
@@ -27,9 +27,28 @@
// Range: B imm26 covers ±128 MB. The EE recompiler region is 64 MB
// (HostMemoryMap::EErecSize), and JITCompile lives in the same region,
// so all link sites are reachable with a single B.
//
// Link-map liveness (2026-07-20). Entries used to accumulate for the whole
// lifetime of the map: Link() inserts unconditionally, and nothing pruned.
// Every recompile of a caller emits its branch at a *fresh* code address and
// appended another entry, so New(pc) re-patched every site the map had ever
// seen for that pc — each with its own 4-byte icache flush. Measured on
// Dirge of Cerberus: 13,372 stale sites on one PC, and 93.5% of EE-thread
// cycles inside __aarch64_sync_cache_range. Each entry now carries the
// identity of the block that emitted it (owner startpc + that block's code
// address), and both Link() and New() erase entries whose owner is gone or has
// since been recompiled. Cost becomes O(live callers) instead of O(all callers
// ever), and the map stops growing without bound.
//
// Liveness rests on two invariants: block code is bump-allocated and never
// reclaimed until a full Reset() (which also clears the map), so an fnptr
// value never identifies two different compiles; and BASEBLOCKEX::fnptr is
// authoritative for the block's *current* code, which is why New() updates
// it when a startpc is recompiled in place rather than leaving it stale.
#pragma once
#include <algorithm>
#include <map>
#include "common/HostSys.h"
@@ -39,12 +58,31 @@
class Arm64BaseBlocks
{
protected:
using linkmap_t = std::multimap<u32, uptr>;
// A registered branch site, tagged with the block that emitted it so
// New() can tell live sites from ones left behind by a superseded
// compile. See the liveness note in the file header.
struct LinkSite
{
// Patch site address, with kLinkSiteCallBit in bit 0.
uptr site;
// Block that emitted this site. owner_fnptr == 0 means "untracked":
// the site was registered outside a compile (only the direct unit
// tests do this), and is treated as permanently live.
u32 owner_startpc;
uptr owner_fnptr;
};
using linkmap_t = std::multimap<u32, LinkSite>;
BaseBlockArray blocks;
linkmap_t links;
uptr jitcompile = 0;
// The block currently being compiled, published by New(). Link() stamps
// it onto every site it records.
u32 emitting_startpc = 0;
uptr emitting_fnptr = 0;
// Link sites are 4-byte-aligned instruction addresses, so bit 0 of the
// stored uptr is free: it tags the site's branch form. Untagged = B,
// tagged = BL (call-ret stack call sites — the BL pushes the hardware
@@ -63,7 +101,13 @@ protected:
return (call ? 0x94000000u : 0x14000000u) | (static_cast<u32>(imm26) & 0x03FFFFFFu);
}
static void PatchAtomic(uptr site, u32 instr)
// Store only, no cache maintenance. Valid ONLY for a site inside the
// block currently being emitted: that buffer has not been executed yet,
// and armEndBlock() issues one whole-range flush over it before it can
// be. (AetherSX2 does the same — its Link() performs no flush at all,
// leaving it to the single range flush in armEndBlock.)
// Never use this on code that is already live; use PatchAtomic.
static void PatchWord(uptr site, u32 instr)
{
// 4-byte aligned word stores are atomic on AArch64. `site` is the RX
// address; under iOS dual-map W^X the store goes through the RW alias
@@ -71,15 +115,83 @@ protected:
// for the toggle modes; on Darwin the "signal handler" caller is
// really the Mach exception-handler thread, so that scope is safe.
*reinterpret_cast<volatile u32*>(armGetWritableCodePtr(reinterpret_cast<u8*>(site))) = instr;
// Then make sure cores fetching instructions see the new word.
}
// (HostSys::FlushInstructionCache, not the raw builtin: on Darwin the
// builtin lowers to a compiler-rt ___clear_cache call that the iOS
// link doesn't provide; the wrapper uses sys_icache_invalidate there.)
HostSys::FlushInstructionCache(reinterpret_cast<void*>(site), 4);
// builtin lowers to a compiler-rt ___clear_cache call that the iOS link
// doesn't provide; the wrapper uses sys_icache_invalidate there.)
static void FlushRange(uptr lo, uptr hi)
{
HostSys::FlushInstructionCache(reinterpret_cast<void*>(lo), static_cast<u32>(hi - lo));
}
static void PatchAtomic(uptr site, u32 instr)
{
PatchWord(site, instr);
// Then make sure cores fetching instructions see the new word.
FlushRange(site, site + 4);
// Signal-safe (counter-only) — Remove() can call this from the
// SIGSEGV fastmem handler.
}
// Drop every entry for `dest_pc` whose owner is gone or has been
// recompiled since. Called from both Link() and New(): New() alone is not
// enough, because a destination that is never recompiled again would never
// have its list revisited, and the entries its callers keep re-registering
// would accumulate for the life of the map. Erasing a multimap element
// invalidates only that element's iterator, so the end of the range stays
// valid across the walk.
void PruneDeadLinks(u32 dest_pc)
{
const auto range = links.equal_range(dest_pc);
for (auto it = range.first; it != range.second;)
{
if (IsOwnerLive(it->second))
++it;
else
it = links.erase(it);
}
}
// True when `ls` still lives inside a block that is both present and has
// not been recompiled since the site was registered.
bool IsOwnerLive(const LinkSite& ls)
{
if (ls.owner_fnptr == 0)
return true; // untracked registration (direct unit tests)
const BASEBLOCKEX* owner = Get(ls.owner_startpc);
return owner && owner->startpc == ls.owner_startpc && owner->fnptr == ls.owner_fnptr;
}
// Coalesce the just-patched sites into as few cache-maintenance ranges as
// possible. The per-call cost is dominated by the `dsb ish` / `isb` pair,
// not by the DC/IC ops, so N adjacent 4-byte flushes cost ~N barrier pairs
// where one range flush costs one. Sites emitted by the same block sit a
// few instructions apart, so this usually collapses to a single range.
static void FlushPatchedSites(uptr* sites, u32 count)
{
if (count == 0)
return;
std::sort(sites, sites + count);
constexpr uptr kMaxGap = 256;
uptr lo = sites[0], hi = sites[0] + 4;
for (u32 i = 1; i < count; i++)
{
if (sites[i] - hi <= kMaxGap)
{
hi = sites[i] + 4;
continue;
}
FlushRange(lo, hi);
lo = sites[i];
hi = sites[i] + 4;
}
FlushRange(lo, hi);
}
public:
Arm64BaseBlocks()
: blocks(0x4000)
@@ -104,25 +216,72 @@ public:
BASEBLOCKEX* target = Get(pc);
const uptr target_addr = (target && target->startpc == pc)
? target->fnptr : jitcompile;
PatchAtomic(reinterpret_cast<uptr>(patch_site),
// No flush: the site is in the block being emitted right now, and
// armEndBlock() range-flushes that buffer. See PatchWord.
PatchWord(reinterpret_cast<uptr>(patch_site),
EncodeB(reinterpret_cast<uptr>(patch_site), target_addr, call));
links.insert({pc, reinterpret_cast<uptr>(patch_site) | (call ? kLinkSiteCallBit : 0)});
// Reap this destination's dead entries before adding ours, so its list
// tracks live callers rather than every caller it has ever had.
PruneDeadLinks(pc);
links.insert({pc,
LinkSite{reinterpret_cast<uptr>(patch_site) | (call ? kLinkSiteCallBit : 0),
emitting_startpc, emitting_fnptr}});
}
// Begin compiling the block at `startpc`, whose code starts at `fnptr`.
// Returns its BASEBLOCKEX, creating one if this startpc has no live block.
//
// A startpc can be recompiled while its BASEBLOCKEX is still in the array
// (recClear resets BLOCK->fnptr across a straddled extent but deliberately
// spares the in-progress block's entry). BaseBlockArray::insert() has no
// dedup, so that case must reuse the existing entry — and it must retarget
// it at the new code, otherwise fnptr keeps pointing at the superseded
// compile and every consumer of it is wrong: x86size is computed from a
// dead base, Remove() writes its redirect stub over dead code instead of
// the live entry, and Link() sends new callers into stale code.
BASEBLOCKEX* New(u32 startpc, uptr fnptr)
{
// Patch any pending links waiting for a block at this PC. After
// patching they go directly to fnptr instead of routing through
// JITCompile.
// Published for Link() to stamp onto the sites this block emits.
emitting_startpc = startpc;
emitting_fnptr = fnptr;
const int idx = Index(startpc);
BASEBLOCKEX* block = (idx >= 0 && blocks[idx].startpc == startpc)
? &blocks[idx] : nullptr;
if (block)
block->fnptr = fnptr;
else
block = blocks.insert(startpc, fnptr);
// Repoint the links waiting on this PC at the new code, dropping any
// left behind by a block that has since been removed or recompiled.
// Those sites are unreachable code; patching them is pure waste, and
// it is that waste which used to dominate the EE thread.
uptr patched[64];
u32 npatched = 0;
const auto range = links.equal_range(startpc);
for (auto it = range.first; it != range.second; ++it)
for (auto it = range.first; it != range.second;)
{
const uptr site = it->second & ~kLinkSiteCallBit;
PatchAtomic(site, EncodeB(site, fnptr, (it->second & kLinkSiteCallBit) != 0));
if (!IsOwnerLive(it->second))
{
it = links.erase(it);
continue;
}
return blocks.insert(startpc, fnptr);
const uptr site = it->second.site & ~kLinkSiteCallBit;
PatchWord(site, EncodeB(site, fnptr, (it->second.site & kLinkSiteCallBit) != 0));
if (npatched < std::size(patched))
patched[npatched++] = site;
else
FlushRange(site, site + 4); // overflow: flush as we go
++it;
}
FlushPatchedSites(patched, npatched);
return block;
}
int LastIndex(u32 startpc) const
@@ -180,8 +339,10 @@ public:
// Signal-safe: writes a redirect stub at each removed block's entry
// point so any stale link still resolves through JITCompile, then
// erases from the flat sorted array. Does NOT touch the link map —
// stale entries there are harmless (they just trigger a re-patch on
// the next compile cycle for the same PC).
// mutating an STL container here is not signal-safe. The entries this
// strands are reaped by the next New() for their destination PC, which
// sees the owner is gone; until then the redirect stub keeps any site
// still pointing at this block correct.
//
// SL-1: a resident self-loop's back-edge is an internal B to the loop-top
// (past the entry redirect), so it gets its own atomic repoint — to the
@@ -212,6 +373,8 @@ public:
{
blocks.clear();
links.clear();
emitting_startpc = 0;
emitting_fnptr = 0;
}
#ifdef PCSX2_RECOMPILER_TESTS
@@ -235,11 +398,22 @@ public:
const auto range = links.equal_range(dst_pc);
for (auto it = range.first; it != range.second; ++it)
{
const uptr site = it->second & ~kLinkSiteCallBit;
const uptr site = it->second.site & ~kLinkSiteCallBit;
if (site >= lo && site < hi)
return true;
}
return false;
}
// Test-only: number of registered link sites targeting dst_pc, and the
// total across all destinations. Used to pin that the map stays bounded
// by *live* sites rather than growing with every recompile.
size_t LinkCount(u32 dst_pc) const
{
const auto range = links.equal_range(dst_pc);
return static_cast<size_t>(std::distance(range.first, range.second));
}
size_t TotalLinkCount() const { return links.size(); }
#endif
};
+118 -35
View File
@@ -85,9 +85,10 @@ alignas(16) static const u32 s_cop2DestMasks[16][4] = {
// tiny compile-time cache keeps them resident in q16..q20 and the op bodies
// compute 3-operand NEON straight from the cache registers.
//
// Register choice: q16-q26 have no fixed user in EE-block emission context
// Register choice: q16-q24 have no fixed user in EE-block emission context
// (q0-q7 = allocator temp/FPR first-fit + vtlb data + mVU macro window,
// q8/q9 = pinned FPU clamp constants, q10-q15 = allocator GPR-quad/FPR homes,
// q25/q26 = SL-13 clamp-constant broadcasts (cop2EnsureClampConsts below),
// q27/q28 = VOPMULA/VCLIP + flag-body scratch, q29-q31 = per-op scratch).
// They are caller-saved and NOT preserved by the fastmem fault thunk (which
// only saves allocator-tracked regs), so the cache must never survive any op
@@ -468,8 +469,12 @@ static_assert(offsetof(cpuRegistersPack, cop2Rec) + sizeof(EeCop2RecState) <= 16
// (Re)write the pack copies of the COP2 rec constants. Called from
// recResetRaw, so the harnesses that reset the rec before compiling are
// covered too. minFloat is the pre-negated clamp lower bound: the clamp
// emitters spend a 1-insn load where they used to spend an Fneg.
// covered too. minFloat is the pre-negated clamp lower bound. Since SL-13
// the clamp emitters no longer LOAD maxFloat/minFloat (the bounds live
// broadcast in q25/q26, re-materialized from s8/s9 — see
// cop2EnsureClampConsts below); the pack fields stay as the documented
// canonical values (minFloat[i] == maxFloat[i] | 0x80000000 == -FLT_MAX is
// the identity the s9 Dup relies on) and for any future dest-mask work.
void cop2RecWritePackConstants()
{
EeCop2RecState& st = _cpuRegistersPack.cop2Rec;
@@ -481,15 +486,80 @@ void cop2RecWritePackConstants()
st.denormStatusFlag = 0;
}
// =========================================================================
// SL-13: clamp-constant broadcast residency (q25/q26)
// =========================================================================
// The clamp bounds live register-resident: q25 = maxFloat.4S (+FLT_MAX per
// lane), q26 = minFloat.4S (-FLT_MAX per lane). Both are excluded from the
// EE NEON allocator pool (NEON_RESERVED_COP2_CLAMPMAX/MIN, iCore-arm64.cpp)
// and from the COP2 macro-mode mVU pool (microRegAlloc::reset(cop2mode)), so
// no EE-block emission can clobber them. Re-materialization is 2 Dups from
// the pinned s8 = +FLT_MAX / s9 = -FLT_MAX callee-saved scalars
// (_DynGen_EnterRecompiledCode) — no memory access, and the sources survive
// every C call by AAPCS64. minFloat[i] == maxFloat[i] | 0x80000000 ==
// -FLT_MAX exactly (see cop2RecWritePackConstants), so s9 is the exact
// broadcast source.
//
// Compile-time validity discipline (s_cop2ClampConstsValid):
// - false at block start; the first clamp site emits the 2 Dups.
// - iFlushCall (ANY flushtype — every real C-call seam) invalidates: the
// callee may clobber caller-saved q25/q26. The next clamp site re-Dups.
// - The VPU_STAT-conditional sync seams do NOT invalidate: the shared sync
// stubs re-Dup unconditionally on their taken path after the C calls
// (always sound — q25/q26 can hold nothing else), and their fast path
// touches no NEON.
// - Fastmem sites do NOT invalidate: vtlbGetLiveRegisterMasks ORs q25/q26
// into the recorded fpr_bitmask while valid, so a backpatched slowmem
// thunk save/restores them around its C call like any live register.
// - The mVU-reuse macro wrappers do NOT invalidate: their pool excludes
// q25/q26 under cop2mode and they emit no C calls.
// - Branch forks and superblock side exits snapshot/restore the flag via
// BranchCompileState (iR5900-arm64.cpp).
// Establishment must stay on unconditionally-executed emission paths — never
// emit the Dups inside a runtime-conditional arm (a post-merge site compiled
// valid would be wrong on the arm that skipped them). All current clamp
// sites are straight-line within their op bodies.
static bool s_cop2ClampConstsValid = false;
#ifdef PCSX2_RECOMPILER_TESTS
u32 g_cop2ClampConstEstablishCount = 0;
#endif
bool cop2ClampConstsValid()
{
return s_cop2ClampConstsValid;
}
void cop2ClampConstsSetValid(bool valid)
{
s_cop2ClampConstsValid = valid;
}
void cop2ClampConstsInvalidate()
{
s_cop2ClampConstsValid = false;
}
static void cop2EnsureClampConsts()
{
if (s_cop2ClampConstsValid)
return;
armAsm->Dup(a64::v25.V4S(), a64::v8.V4S(), 0); // +FLT_MAX broadcast
armAsm->Dup(a64::v26.V4S(), a64::v9.V4S(), 0); // -FLT_MAX broadcast
s_cop2ClampConstsValid = true;
#ifdef PCSX2_RECOMPILER_TESTS
g_cop2ClampConstEstablishCount++;
#endif
}
// Clamp the result register to [-FLT_MAX, +FLT_MAX] (removes infinities and
// NaNs). FMINNM/FMAXNM match x86 MINPS/MAXPS semantics: NaN → non-NaN operand.
static void cop2ClampResultReg(const a64::VRegister& result)
{
armAsm->Ldr(RQSCRATCH2, armCpuRegMem(&_cpuRegistersPack.cop2Rec.maxFloat));
armAsm->Ldr(RQSCRATCH3, armCpuRegMem(&_cpuRegistersPack.cop2Rec.minFloat));
armAsm->Fminnm(result.V4S(), result.V4S(), RQSCRATCH2.V4S()); // clamp to +FLT_MAX
armAsm->Fmaxnm(result.V4S(), result.V4S(), RQSCRATCH3.V4S()); // clamp to -FLT_MAX
cop2EnsureClampConsts();
armAsm->Fminnm(result.V4S(), result.V4S(), a64::v25.V4S()); // clamp to +FLT_MAX
armAsm->Fmaxnm(result.V4S(), result.V4S(), a64::v26.V4S()); // clamp to -FLT_MAX
}
static void cop2ClampResult()
@@ -497,28 +567,14 @@ static void cop2ClampResult()
cop2ClampResultReg(RQSCRATCH);
}
// Single-temp variant of cop2ClampReg: clamps `qreg` to [-FLT_MAX, +FLT_MAX]
// using just one scratch register (it reloads the bound between the two
// clamps). Needed when pre-clamping a broadcast FMAC operand, where Fs/Ft
// already occupy two of the three q-scratch regs and only one is free.
static void cop2ClampRegOneTmp(const a64::VRegister& qreg, const a64::VRegister& tmp)
{
armAsm->Ldr(tmp, armCpuRegMem(&_cpuRegistersPack.cop2Rec.maxFloat));
armAsm->Fminnm(qreg.V4S(), qreg.V4S(), tmp.V4S()); // clamp to +FLT_MAX
armAsm->Ldr(tmp, armCpuRegMem(&_cpuRegistersPack.cop2Rec.minFloat));
armAsm->Fmaxnm(qreg.V4S(), qreg.V4S(), tmp.V4S()); // clamp to -FLT_MAX
}
// Non-destructive clamp: dst = clamp(src) without modifying src (which may be
// a live VF-cache register). Same 4-insn cost as cop2ClampRegOneTmp — the
// first Fminnm is 3-operand, so preserving src is free.
static void cop2ClampInto(const a64::VRegister& dst, const a64::VRegister& src,
const a64::VRegister& tmp)
// a live VF-cache register) — the first Fminnm is 3-operand, so preserving
// src is free.
static void cop2ClampInto(const a64::VRegister& dst, const a64::VRegister& src)
{
armAsm->Ldr(tmp, armCpuRegMem(&_cpuRegistersPack.cop2Rec.maxFloat));
armAsm->Fminnm(dst.V4S(), src.V4S(), tmp.V4S());
armAsm->Ldr(tmp, armCpuRegMem(&_cpuRegistersPack.cop2Rec.minFloat));
armAsm->Fmaxnm(dst.V4S(), dst.V4S(), tmp.V4S());
cop2EnsureClampConsts();
armAsm->Fminnm(dst.V4S(), src.V4S(), a64::v25.V4S());
armAsm->Fmaxnm(dst.V4S(), dst.V4S(), a64::v26.V4S());
}
// ========================================================================
@@ -1066,6 +1122,16 @@ static const u8* cop2DynGenOneSyncStub(void (*syncFn)(), void (*finishFn)())
armReloadCycleDelta();
armReloadEEClobberedPins();
// SL-13: the callees (and any VU0 micro they ran) clobber caller-saved
// q25/q26 — re-materialize the clamp-constant broadcasts so sites whose
// compile-time validity rides through this seam stay correct. Always
// sound: q25/q26 are pool-reserved and can hold nothing else, and the
// s8/s9 sources are callee-saved (low 64 bits). The fast path above
// touches no NEON, so validity rides it untouched. Pinned by
// EeVu0Cop2ClampResidency.SyncStubsReDupClampConsts.
armAsm->Dup(a64::v25.V4S(), a64::v8.V4S(), 0);
armAsm->Dup(a64::v26.V4S(), a64::v9.V4S(), 0);
armAsm->Ldr(a64::x30, a64::MemOperand(a64::sp, 48));
armAsm->Ldp(a64::x14, a64::x15, a64::MemOperand(a64::sp, 32));
armAsm->Ldp(a64::x6, a64::x7, a64::MemOperand(a64::sp, 16));
@@ -1084,6 +1150,24 @@ void cop2DynGenSyncStubs()
s_cop2SyncStubs[kCop2SyncStubFinish] = cop2DynGenOneSyncStub(nullptr, _vu0FinishMicro);
}
#ifdef PCSX2_RECOMPILER_TESTS
// SL-13 pin surface: emitted sync-stub code ranges, so tests can assert the
// taken path re-materializes the q25/q26 clamp broadcasts (the seam-survival
// invariant is emission-level — end-to-end runs only catch it when the C
// path happens to clobber q25/q26). Kind indexes follow emission order; the
// end of stub k is the start of stub k+1 (contiguous emission), and the last
// stub is bounded by the dispatcher's Perf-registered range — tests scan to
// the final Ret instead.
int cop2TestGetSyncStubCount()
{
return kCop2SyncStubCount;
}
const u8* cop2TestGetSyncStub(int kind)
{
return (kind >= 0 && kind < kCop2SyncStubCount) ? s_cop2SyncStubs[kind] : nullptr;
}
#endif
// Emit conditional VU0 sync: uses EEINST analysis flags when available,
// falls back to runtime VPU_STAT check otherwise.
// Implements the COP2_Interlock + mVUSyncVU0/mVUFinishVU0 sync protocol.
@@ -1642,10 +1726,10 @@ static void cop2LoadBroadcast(const a64::VRegister& qreg, int vfReg, int bc)
a64::VRegister mulA = fs; \
if (mulClamp) \
{ \
cop2ClampInto(RQSCRATCH, fs, RQSCRATCH3); \
cop2ClampInto(RQSCRATCH, fs); \
mulA = RQSCRATCH; \
if (_XYZW_cop2 == 0xf) \
cop2ClampRegOneTmp(RQSCRATCH2, RQSCRATCH3); \
cop2ClampResultReg(RQSCRATCH2); /* in-place operand clamp */ \
} \
const a64::VRegister rd = cop2ResultReg(_Fd_cop2, _XYZW_cop2); \
armAsm->neonOp(rd.V4S(), mulA.V4S(), RQSCRATCH2.V4S()); \
@@ -1823,8 +1907,7 @@ 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. RQSCRATCH2/RQSCRATCH3 are
// free as the ±FLT_MAX bounds here (Ft/ACC are loaded after the clamp).
// The MADDw extras (cACC|cFt) are a separate concern.
#define COP2_MADD_BC(name, addOp, bc, clampFs) \
void recCOP2_V##name() \
{ \
@@ -1834,7 +1917,7 @@ void recCOP2_VMSUB()
a64::VRegister mulA = fs; \
if (clampFs) \
{ \
cop2ClampInto(RQSCRATCH, fs, RQSCRATCH3); \
cop2ClampInto(RQSCRATCH, fs); \
mulA = RQSCRATCH; \
} \
cop2LoadBroadcast(RQSCRATCH2, _Ft_cop2, bc); \
@@ -1970,10 +2053,10 @@ COP2_ACCUM_OP(MULA, Fmul)
a64::VRegister mulA = fs; \
if (mulClamp) \
{ \
cop2ClampInto(RQSCRATCH, fs, RQSCRATCH3); \
cop2ClampInto(RQSCRATCH, fs); \
mulA = RQSCRATCH; \
if (_XYZW_cop2 == 0xf) \
cop2ClampRegOneTmp(RQSCRATCH2, RQSCRATCH3); \
cop2ClampResultReg(RQSCRATCH2); /* in-place operand clamp */ \
} \
const a64::VRegister rdA = cop2ResultRegACC(_XYZW_cop2); \
armAsm->neonOp(rdA.V4S(), mulA.V4S(), RQSCRATCH2.V4S()); \
+19 -5
View File
@@ -709,7 +709,21 @@ static constexpr u32 NEON_RESERVED_FPU_MAX = 8;
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.)
// 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.)
static bool _isReservedNEONreg(u32 i)
{
return i == NEON_RESERVED_FPU_MAX || i == NEON_RESERVED_FPU_MIN ||
i == NEON_RESERVED_COP2_CLAMPMAX || i == NEON_RESERVED_COP2_CLAMPMIN;
}
#ifdef PCSX2_RECOMPILER_TESTS
bool eeTestNeonRegIsReserved(int hostreg)
{
return _isReservedNEONreg(static_cast<u32>(hostreg));
}
#endif
// Free-slot-only probe of a range: no eviction, -1 when the range is full.
// Used by the FPR-class allocators to PREFER a call-surviving home (GE-15)
@@ -719,7 +733,7 @@ static int _getFreeArm64NEONInRangeNoEvict(u32 minreg, u32 maxreg)
{
for (u32 i = minreg; i < maxreg; i++)
{
if (i == NEON_RESERVED_FPU_MAX || i == NEON_RESERVED_FPU_MIN)
if (_isReservedNEONreg(i))
continue;
if (!arm64neon[i].inuse)
return static_cast<int>(i);
@@ -735,7 +749,7 @@ int _getFreeArm64NEON(u32 minreg, u32 maxreg)
// Check for free registers
for (u32 i = minreg; i < maxreg; i++)
{
if (i == NEON_RESERVED_FPU_MAX || i == NEON_RESERVED_FPU_MIN)
if (_isReservedNEONreg(i))
continue;
if (!arm64neon[i].inuse)
return i;
@@ -746,7 +760,7 @@ int _getFreeArm64NEON(u32 minreg, u32 maxreg)
bestcount = 0xffff;
for (u32 i = minreg; i < maxreg; i++)
{
if (i == NEON_RESERVED_FPU_MAX || i == NEON_RESERVED_FPU_MIN)
if (_isReservedNEONreg(i))
continue;
pxAssert(arm64neon[i].inuse);
if (arm64neon[i].needed)
@@ -787,7 +801,7 @@ int _getFreeArm64NEON(u32 minreg, u32 maxreg)
bestcount = 0xffff;
for (u32 i = minreg; i < maxreg; i++)
{
if (i == NEON_RESERVED_FPU_MAX || i == NEON_RESERVED_FPU_MIN)
if (_isReservedNEONreg(i))
continue;
pxAssert(arm64neon[i].inuse);
if (arm64neon[i].needed)
+11
View File
@@ -131,6 +131,17 @@ struct _arm64gprregs
static constexpr u32 NEON_CALLEE_SAVED_START = 10;
static constexpr u32 NEON_CALLEE_SAVED_END = 16; // exclusive
// 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
// from the NEON allocator pool entirely, like q8/q9. They are lazily
// re-materialized per block from the pinned s8/s9 scalars (2 Dups, no memory)
// — see cop2EnsureClampConsts in iCOP2-arm64.cpp for the compile-time
// validity discipline. The COP2 macro-mode mVU pool excludes them too
// (microRegAlloc::reset(cop2mode)); micro-mode mVU may clobber them freely —
// every EE-side path back from micro execution re-materializes.
static constexpr u32 NEON_RESERVED_COP2_CLAMPMAX = 25;
static constexpr u32 NEON_RESERVED_COP2_CLAMPMIN = 26;
// x86 type aliases — used by shared analysis code (iR5900Analysis.cpp)
#define XMMTYPE_TEMP NEONTYPE_TEMP
#define XMMTYPE_GPRREG NEONTYPE_GPRREG
+23 -5
View File
@@ -447,11 +447,13 @@ static a64::VRegister fpuClampMinMaxOperand(const a64::VRegister& src, const a64
// positions must read as zero on hardware. This masks the low mantissa bits of
// the smaller-exponent operand by the exponent difference, then does the single
// op. It is the arm64 fast-path port of x86 FPU_ADD_SUB (iFPU.cpp:402). Both
// JITs apply this masking unconditionally (x86 FPU_ADD/FPU_SUB, iFPU.cpp) —
// games like True Crime NYC and Jak 3 misrender without it, and flagging them
// per-game proved impractical. 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.
// JITs gate this masking on the same CHECK_FPU_GUARDED option (x86 FPU_ADD/
// 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).
//
// 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;
@@ -475,6 +477,22 @@ static a64::VRegister fpuClampMinMaxOperand(const a64::VRegister& src, const a64
static void fpuEmitGuardedAddSub(const a64::VRegister& dst,
const a64::VRegister& s, const a64::VRegister& t, bool issub)
{
// Guard-bit emulation is ON by default (CHECK_FPU_GUARDED) but can be turned
// off globally via the fpuGuardedAddSub Recompiler INI bool for EE-FPU-heavy
// titles verified to render fine without it. Off = a plain single op, matching
// AetherSX2 / PCSX2 v1.0 and the x86 FPU_ADD/FPU_SUB guard-off branch
// (iFPU.cpp). Returns before the NEON-temp alloc and GPR-scratch use below so
// nothing is booked on the fast path. (Full clamp mode is unaffected either
// way: it runs the DOUBLE path, which masks guard bits itself — iFPUd-arm64.cpp.)
if (!CHECK_FPU_GUARDED)
{
if (issub)
armAsm->Fsub(dst, s, t);
else
armAsm->Fadd(dst, s, t);
return;
}
// Alloc the NEON temp FIRST, before any raw GPR scratch below goes live.
// The alloc can emit a victim eviction whose address materialization uses
// scratch (today only x16/x17 via armMoveAddressToReg); keeping w9/w10
+2 -2
View File
@@ -1304,8 +1304,8 @@ static void iopRecRecompile(const u32 startpc)
// and triggers SIGILL. Same fix as EE rec at iR5900-arm64.cpp:1751.
const uptr block_fnptr = (uptr)armGetCurrentCodePointer();
s_pCurBlockEx = recBlocks.Get(HWADDR(startpc));
if (!s_pCurBlockEx || s_pCurBlockEx->startpc != HWADDR(startpc))
// See the EE rec's equivalent: New() creates or re-binds, and publishes
// the owner for the link sites this block is about to register.
s_pCurBlockEx = recBlocks.New(HWADDR(startpc), block_fnptr);
psxbranch = 0;
+14 -2
View File
@@ -672,6 +672,12 @@ void iFlushCall(int flushtype)
// (Cop2VfCacheScope) so each fork emits its own writebacks.
cop2VfCacheFlush();
// SL-13: the callee may clobber the caller-saved q25/q26 clamp-constant
// broadcasts — pure compile-time invalidation (constants are clean by
// definition; the next clamp site re-materializes with 2 Dups).
// Unconditional on flushtype: ANY C call can clobber them.
cop2ClampConstsInvalidate();
// Free caller-saved registers
for (int i = 0; i < NUM_ARM_GPR_REGS; i++)
{
@@ -1613,10 +1619,12 @@ struct BranchCompileState
u32 blockCycles;
EEINST* instInfo;
Cop2VfCacheState vfCache;
bool clampConstsValid; // SL-13: q25/q26 broadcast validity at the fork point
void capture()
{
vfCache = cop2VfCacheGetState();
clampConstsValid = cop2ClampConstsValid();
blockCycles = s_nBlockCycles;
memcpy(constRegs, g_cpuConstRegs, sizeof(g_cpuConstRegs));
hasConstReg = g_cpuHasConstReg;
@@ -1629,6 +1637,7 @@ struct BranchCompileState
void restore() const
{
cop2VfCacheSetState(vfCache);
cop2ClampConstsSetValid(clampConstsValid);
s_nBlockCycles = blockCycles;
memcpy(g_cpuConstRegs, constRegs, sizeof(g_cpuConstRegs));
g_cpuHasConstReg = hasConstReg;
@@ -3240,12 +3249,15 @@ static void recRecompile(const u32 startpc)
// and triggers SIGILL.
const uptr block_fnptr = (uptr)armGetCurrentCodePointer();
s_pCurBlockEx = recBlocks.Get(HWADDR(startpc));
if (!s_pCurBlockEx || s_pCurBlockEx->startpc != HWADDR(startpc))
// New() both creates and re-binds: a startpc whose BASEBLOCKEX survived a
// straddled recClear is retargeted at the new code rather than left with
// a stale fnptr. It also publishes the block as the owner of every link
// site the emission below registers.
s_pCurBlockEx = recBlocks.New(HWADDR(startpc), block_fnptr);
g_branch = 0;
cop2VfCacheReset();
cop2ClampConstsInvalidate(); // SL-13: q25/q26 state unknown at block entry
s_pCurBlock->SetFnptr(block_fnptr);
s_nBlockCycles = 0;
+23 -4
View File
@@ -737,16 +737,35 @@ Cop2VfCacheState cop2VfCacheGetState(); // fork-tail peek support
void cop2VfCacheSetState(const Cop2VfCacheState&);
bool cop2OpPreservesVfCache(u32 code); // classifier for recompileNextInstruction
// RAII: preserve the compile-time cache state across a branch-fork tail
// SL-13: compile-time validity of the q25/q26 clamp-constant broadcasts
// (defined in iCOP2-arm64.cpp — see cop2EnsureClampConsts for the full
// discipline). Invalidate at every real C-call seam (iFlushCall) and at
// block start; the flag joins BranchCompileState for forks/side exits;
// vtlbGetLiveRegisterMasks reads it to make fastmem thunks preserve q25/q26.
bool cop2ClampConstsValid();
void cop2ClampConstsSetValid(bool valid);
void cop2ClampConstsInvalidate();
// RAII: preserve the compile-time COP2 residency state (VF cache + SL-13
// clamp-const validity) across a branch-fork tail
// (SetBranchImm/SetBranchImmCall/SetBranchReg) whose iFlushCall destructively
// flushes — the sibling fork must re-emit its own writebacks from the same
// pre-tail state, since the cached values stay register-resident along every
// runtime path.
// runtime path (and q25/q26 stay materialized on the not-taken path).
struct Cop2VfCacheScope
{
Cop2VfCacheState state;
Cop2VfCacheScope() : state(cop2VfCacheGetState()) {}
~Cop2VfCacheScope() { cop2VfCacheSetState(state); }
bool clampConstsValid;
Cop2VfCacheScope()
: state(cop2VfCacheGetState())
, clampConstsValid(cop2ClampConstsValid())
{
}
~Cop2VfCacheScope()
{
cop2VfCacheSetState(state);
cop2ClampConstsSetValid(clampConstsValid);
}
};
// COP2 macro-mode microVU0 state setup/teardown (defined in microVU-arm64.cpp).
+10
View File
@@ -2103,6 +2103,16 @@ bool mVUTestProbe_VIPoolUsable(int hostreg, bool cop2mode)
return usable;
}
// SL-13 twin: is host NEON reg q<hostreg> in the VF allocation pool under
// cop2mode? Macro mode must exclude q25/q26 (EE clamp-constant broadcasts).
bool mVUTestProbe_NeonPoolUsable(int hostreg, bool cop2mode)
{
microVU0.regAlloc->reset(cop2mode);
const bool usable = microVU0.regAlloc->isUsableNeon(hostreg);
microVU0.regAlloc->reset(false);
return usable;
}
// waitMTVU stub-shape probe: returns the emitted VU1-sync thunk entry
// (mVU.waitMTVU, generated once by mVUgenerateDispatchers) for VU `index`.
// mVUaddrFix branches to this thunk when VU0/COP2 touches VU1 register space
+28
View File
@@ -60,6 +60,7 @@ protected:
int counter;
int neonWatermark; // see getNeonWatermark()
int index; // VU0 or VU1
bool neonCop2Mode; // SL-13: macro mode — q25/q26 unallocatable (EE clamp consts)
VURegs& regs() const { return ::vuRegs[index]; }
@@ -88,11 +89,19 @@ protected:
armAsm->Dup(reg.V4S(), reg.V4S(), 0); // Broadcast to all lanes
}
// SL-13: NEON pool gate — cop2mode excludes q25/q26 (see reset()).
__ri bool neonUsable(int i) const
{
return !neonCop2Mode || (i != 25 && i != 26);
}
// Find least-recently-used NEON reg (recursive, for eviction)
int findFreeNeonRec(int startIdx)
{
for (int i = startIdx; i < neonAllocTotal; i++)
{
if (!neonUsable(i))
continue;
if (!neonMap[i].isNeeded)
{
int x = findFreeNeonRec(i + 1);
@@ -109,6 +118,8 @@ protected:
// Prefer unoccupied temp regs
for (int i = 0; i < neonAllocTotal; i++)
{
if (!neonUsable(i))
continue;
if (!neonMap[i].isNeeded && neonMap[i].VFreg < 0)
{
neonWatermark = std::max(neonWatermark, i + 1);
@@ -256,6 +267,15 @@ public:
// EeVu0Cop2Macro.MacroModeVIPoolExcludesEEPinHosts. (x86's cop2mode
// meaning — fastmem-base/text-pointer usability — doesn't apply here;
// those bases are pinned outside the allocatable set.)
//
// SL-13: cop2mode likewise gates the q25/q26 NEON slots — in EE-block
// context they hold the COP2 clamp-constant broadcasts (see
// NEON_RESERVED_COP2_CLAMPMAX/MIN, iCore-arm64.h), and the clamp validity
// flag deliberately RIDES THROUGH the mVU-reuse macro wrappers (they emit
// no C call), so an mVU allocation landing there would silently corrupt
// the constants for every later clamp site in the block. Micro mode keeps
// both (micro programs run under the dispatcher; EE re-materializes on
// every path back). Pinned by EeVu0Cop2ClampResidency.MacroModeNeonPool*.
void reset(bool cop2mode = false)
{
// Clear x26/x27 unconditionally so no VI binding survives a
@@ -265,6 +285,7 @@ public:
clearGPR(27);
gprMap[26].usable = !cop2mode;
gprMap[27].usable = !cop2mode;
neonCop2Mode = cop2mode;
for (int i = 0; i < neonAllocTotal; i++)
clearNeon(i);
for (int i = 0; i < gprAllocCount; i++)
@@ -874,6 +895,13 @@ public:
return i >= 0 && i < gprAllocCount && gprMap[i].usable;
}
// NEON twin of isUsableGPR (SL-13 q25/q26 clamp-const isolation) — see
// mVUTestProbe_NeonPoolUsable.
bool isUsableNeon(int i) const
{
return i >= 0 && i < neonAllocTotal && neonUsable(i);
}
// Move VI value into a specific GPR (for address computation etc.)
void moveVIToGPR(const a64::Register& dstReg, int vi, bool signext = false)
{
+7
View File
@@ -227,6 +227,13 @@ static void vtlbGetLiveRegisterMasks(u32& gpr_bitmask, u32& fpr_bitmask)
if (arm64neon[i].inuse)
fpr_bitmask |= (1u << i);
}
// SL-13: the q25/q26 clamp-constant broadcasts are not allocator state,
// but when compile-time valid they must survive a backpatched slowmem
// thunk's C call like any live register — clamp sites after this access
// were compiled without re-materialization.
if (cop2ClampConstsValid())
fpr_bitmask |= (1u << NEON_RESERVED_COP2_CLAMPMAX) | (1u << NEON_RESERVED_COP2_CLAMPMIN);
}
// Emit a single fastmem load instruction and register backpatch info.
+6 -4
View File
@@ -62,8 +62,10 @@ namespace DOUBLE
#define FPUflagSO 0x00000010
#define FPUflagSU 0x00000008
// Add/Sub opcodes produce the same results as the ps2
#define FPU_CORRECT_ADD_SUB 1
// Add/Sub guard-bit emulation (matching the PS2's missing mantissa guard bits)
// is gated at runtime on CHECK_FPU_GUARDED (the fpuGuardedAddSub Recompiler
// option, ON by default) so both JITs honor the same setting. The Full-mode
// DOUBLE path keeps its own unconditional guard (iFPUd.cpp).
alignas(16) static const u32 s_neg[4] = {0x80000000, 0xffffffff, 0xffffffff, 0xffffffff};
alignas(16) static const u32 s_pos[4] = {0x7fffffff, 0xffffffff, 0xffffffff, 0xffffffff};
@@ -481,7 +483,7 @@ void FPU_ADD_SUB(int regd, int regt, int issub)
void FPU_ADD(int regd, int regt)
{
if (FPU_CORRECT_ADD_SUB)
if (CHECK_FPU_GUARDED)
FPU_ADD_SUB(regd, regt, 0);
else
xADD.SS(xRegisterSSE(regd), xRegisterSSE(regt));
@@ -489,7 +491,7 @@ void FPU_ADD(int regd, int regt)
void FPU_SUB(int regd, int regt)
{
if (FPU_CORRECT_ADD_SUB)
if (CHECK_FPU_GUARDED)
FPU_ADD_SUB(regd, regt, 1);
else
xSUB.SS(xRegisterSSE(regd), xRegisterSSE(regt));
@@ -87,6 +87,7 @@ add_pcsx2_test(recompiler_tests
ee_vu0_cfc2_ctc2_tests.cpp
ee_vu0_qmfc2_qmtc2_tests.cpp
ee_vu0_cop2_transfer_residency_tests.cpp
ee_vu0_cop2_clamp_residency_tests.cpp
ee_vu0_cop2_macro_tests.cpp
vu1_alu_upper_tests.cpp
vu1_alu_lower_tests.cpp

Some files were not shown because too many files have changed in this diff Show More