mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
Merge remote-tracking branch 'yaps2/main' into jit-transplant
# Conflicts: # pcsx2/arm64/BaseblockEx-arm64.h
This commit is contained in:
@@ -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
|
||||
@@ -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
@@ -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 ---------------
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
// (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.)
|
||||
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.
|
||||
// (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);
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return blocks.insert(startpc, fnptr);
|
||||
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
@@ -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()); \
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1304,9 +1304,9 @@ 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))
|
||||
s_pCurBlockEx = recBlocks.New(HWADDR(startpc), block_fnptr);
|
||||
// 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;
|
||||
|
||||
|
||||
@@ -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))
|
||||
s_pCurBlockEx = recBlocks.New(HWADDR(startpc), block_fnptr);
|
||||
// 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;
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
@@ -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
Reference in New Issue
Block a user