Files
ARMSX2/pcsx2/ee_divtrace.cpp
T
pstef 39319f35b4 EE/FPU: read and write an FPR through an accessor
The slot is 64 bits and the architectural register is 32, so a word view
of the union is only right while the two coincide. Word() and SetWord()
replace f/UL/SL and are the identity, so nothing that runs changes; what
it buys is that the compiler names every place that reads a slot as a
word, which is the set the next commit has to relocate.

The x86 tier is not built here and still reads the members directly.
2026-08-15 23:57:13 +02:00

171 lines
5.0 KiB
C++

// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "ee_divtrace.h"
#include "Memory.h"
#include "MemoryTypes.h"
#include "VUmicro.h"
#include <cstring>
// Self-contained xxh3 (same guard pattern as GS/GSXXH.h) for the memory hash.
#ifndef XXH_versionNumber
#define XXH_STATIC_LINKING_ONLY 1
#define XXH_INLINE_ALL 1
#include <xxhash.h>
#endif
namespace ee_divtrace
{
std::atomic<bool> g_enabled{false};
bool g_emit_block_hook = false;
bool g_fp_exclude = false;
bool g_vu0_include = false;
bool g_skip_flushcache_syscall = false;
std::vector<Sample> g_stream;
std::vector<FullSnap> g_snaps;
u32 g_full_lo = 0;
u32 g_full_hi = 0;
// FNV-1a style mix, matching microVU_Divtrace so the two diagnostics read
// the same way.
static inline u64 hash_mix(u64 h, u32 v)
{
h ^= static_cast<u64>(v);
h *= 1099511628211ull;
return h;
}
static inline u64 hash_mix64(u64 h, u64 v)
{
h = hash_mix(h, static_cast<u32>(v));
h = hash_mix(h, static_cast<u32>(v >> 32));
return h;
}
u64 FingerprintCpu()
{
u64 h = 1469598103934665603ull;
// GPRs — full 128 bits each (.UD[0]/.UD[1]).
for (int i = 0; i < 32; ++i)
{
// Skip k0/k1 (26/27): the EE kernel exception handler loads EPC/Cause
// into these reserved scratch registers, so they inherit the same
// cycle-phase interrupt noise as EPC and linger into user code until
// overwritten; they reconverge by frame end. Excluding them from the
// alignment fingerprint lets the zoom walk through every interrupt
// handler. (The frame-level full-snapshot comparison still reports them.)
if (i == 26 || i == 27)
continue;
h = hash_mix64(h, cpuRegs.GPR.r[i].UD[0]);
h = hash_mix64(h, cpuRegs.GPR.r[i].UD[1]);
}
h = hash_mix64(h, cpuRegs.HI.UD[0]);
h = hash_mix64(h, cpuRegs.HI.UD[1]);
h = hash_mix64(h, cpuRegs.LO.UD[0]);
h = hash_mix64(h, cpuRegs.LO.UD[1]);
// CP0 — skip the dispatcher-driven counters:
// 1 Random, 9 Count, 11 Compare, plus 13 Cause + 14 EPC (interrupt
// TIMING: EPC/Cause IP bits are a pure function of cpuRegs.cycle phase,
// so JIT/interp take the same interrupt a few cycles apart in an idle
// poll loop; excluding them keeps the streams aligned across every
// interrupt instead of breaking at each one).
for (int i = 0; i < 32; ++i)
{
if (i == 1 || i == 9 || i == 11 || i == 13 || i == 14)
continue;
h = hash_mix(h, cpuRegs.CP0.r[i]);
}
// FPU register file + accumulator (control regs are excluded). Omitted
// when g_fp_exclude is set, so the alignment walk skips past FP-register
// divergences to hunt a non-FP (integer/control) divergence.
if (!g_fp_exclude)
{
for (int i = 0; i < 32; ++i)
h = hash_mix(h, fpuRegs.fpr[i].Word());
h = hash_mix(h, fpuRegs.ACC.Word());
}
// VU0 macro-visible state — opt-in (EERUNNER_VU0FP): catches COP2 macro
// divergence at the producing block instead of downstream in GPRs/memory.
if (g_vu0_include)
{
for (int i = 0; i < 32; ++i)
{
h = hash_mix64(h, VU0.VF[i].UD[0]);
h = hash_mix64(h, VU0.VF[i].UD[1]);
}
h = hash_mix64(h, VU0.ACC.UD[0]);
h = hash_mix64(h, VU0.ACC.UD[1]);
h = hash_mix(h, VU0.VI[REG_STATUS_FLAG].UL);
h = hash_mix(h, VU0.VI[REG_MAC_FLAG].UL);
h = hash_mix(h, VU0.VI[REG_CLIP_FLAG].UL);
}
h = hash_mix(h, cpuRegs.pc);
h = hash_mix(h, cpuRegs.sa);
return h;
}
void RecordSample(u32 pc)
{
const size_t idx = g_stream.size();
Sample s;
s.cycle = cpuRegs.cycle;
s.fp = FingerprintCpu();
s.pc = pc;
s._pad = 0;
g_stream.push_back(s);
if (idx >= g_full_lo && idx < g_full_hi)
{
FullSnap& fs = g_snaps[idx - g_full_lo];
std::memcpy(&fs.cpu, &cpuRegs, sizeof(cpuRegisters));
std::memcpy(&fs.fpu, &fpuRegs, sizeof(fpuRegisters));
std::memcpy(fs.vu0_vf, VU0.VF, 32 * 16);
std::memcpy(fs.vu0_vf + 32 * 16, &VU0.ACC, 16);
fs.vu0_vi_status = VU0.VI[REG_STATUS_FLAG].UL;
fs.vu0_vi_mac = VU0.VI[REG_MAC_FLAG].UL;
fs.vu0_vi_clip = VU0.VI[REG_CLIP_FLAG].UL;
fs._pad0 = 0;
fs.cycle = cpuRegs.cycle;
fs.pc = pc;
fs._pad = 0;
}
}
u64 HashMemory()
{
if (!eeMem)
return 0;
// Real PS2 main RAM is 32 MB (TotalRam is the 128 MB address-wrap span;
// only the first MainRam bytes are physical). Scratchpad is 16 KB.
u64 h = XXH3_64bits(eeMem->Main, Ps2MemSize::MainRam);
h = XXH3_64bits_withSeed(eeMem->Scratch, Ps2MemSize::Scratch, h);
return h;
}
void Reset()
{
g_stream.clear();
if (!g_snaps.empty())
g_snaps.assign(g_snaps.size(), FullSnap{});
}
void ConfigureFullWindow(u32 lo, u32 len)
{
g_full_lo = lo;
g_full_hi = lo + len;
g_snaps.assign(len, FullSnap{});
}
void ReserveStream(size_t samples)
{
g_stream.reserve(samples);
}
} // namespace ee_divtrace
extern "C" void ee_divtrace_jit_block_hook(u32 startpc)
{
if (ee_divtrace::g_enabled.load(std::memory_order_relaxed))
ee_divtrace::RecordSample(startpc);
}