mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-14 15:00:30 +02:00
Unit tests: Add CPUEquivalence, a differential fuzzer for the CPU cores
Runs random straight-line ALU, FPU, VFPU and load/store code from a random state on every core and compares the full end state against the interpreter, NaN payloads aside. Differing cases are minimized and printed with their inputs. Options (seed, count, instruction classes, code override, state dump for comparing hosts) are environment variables, and Tools/cpu_fuzz_reduce.py finds and reduces a crashing case. Known differences remain, so only a crash fails it by default. What it found, and what's still open: docs/cpu-equivalence.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
f32636ac45
commit
cdf238db7c
4 files changed
+745
No files matched your search
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2012- PPSSPP Project.
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, version 2.0 or later versions.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License 2.0 for more details.
|
||||
|
||||
# A copy of the GPL 2.0 should have been included with the program.
|
||||
# If not, see http://www.gnu.org/licenses/
|
||||
|
||||
# Official git repository and contact information can be found at
|
||||
# https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
||||
|
||||
"""Runs the CPUEquivalence fuzzer (see docs/cpu-equivalence.md). If a case crashes, finds it and
|
||||
reduces its code to the instructions still needed for the crash.
|
||||
|
||||
usage: cpu_fuzz_reduce.py UNITTEST_EXE FIRSTSEED COUNT [CLASSES]
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
CODE_LINE = re.compile(r' [0-9a-f]{8} ')
|
||||
|
||||
|
||||
def run(exe, seed, count, classes, code=None, verbose=False):
|
||||
env = dict(os.environ, CI='1', PPSSPP_CPUFUZZ_SEED=str(seed), PPSSPP_CPUFUZZ_COUNT=str(count))
|
||||
if classes:
|
||||
env['PPSSPP_CPUFUZZ_CLASSES'] = classes
|
||||
if code:
|
||||
env['PPSSPP_CPUFUZZ_CODE'] = ','.join(code)
|
||||
if verbose:
|
||||
env['PPSSPP_CPUFUZZ_VERBOSE'] = '1'
|
||||
# A file, not a pipe: a crash would lose whatever is still buffered in a pipe.
|
||||
with tempfile.TemporaryFile('w+') as log:
|
||||
try:
|
||||
rc = subprocess.run([exe, 'CPUEquivalence'], env=env, stdout=log, stderr=subprocess.STDOUT, timeout=1800).returncode
|
||||
except subprocess.TimeoutExpired:
|
||||
rc = -1
|
||||
log.seek(0)
|
||||
return rc, log.read()
|
||||
|
||||
|
||||
def crashed(rc):
|
||||
# 0 passed, 1 failed (differences in strict mode), 2 reported by the harness as failing.
|
||||
return rc not in (0, 1, 2)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 4:
|
||||
print('usage: cpu_fuzz_reduce.py UNITTEST_EXE FIRSTSEED COUNT [CLASSES]')
|
||||
return 1
|
||||
exe, firstSeed, count = sys.argv[1], int(sys.argv[2]), int(sys.argv[3])
|
||||
classes = sys.argv[4] if len(sys.argv) > 4 else ''
|
||||
|
||||
rc, out = run(exe, firstSeed, count, classes, verbose=True)
|
||||
if not crashed(rc):
|
||||
print(''.join(l for l in out.splitlines(True) if 'cases differ' in l), end='')
|
||||
return 0
|
||||
|
||||
lines = out.splitlines()
|
||||
idx = max(i for i, l in enumerate(lines) if re.match(r'.* seed \d+$', l))
|
||||
seed = int(lines[idx].split()[-1])
|
||||
core = lines[idx].rsplit(' seed', 1)[0]
|
||||
code = [l.split()[0] for l in lines[idx + 1:] if CODE_LINE.match(l)]
|
||||
messages = [l for l in lines[idx + 1:] if not CODE_LINE.match(l)]
|
||||
print('Crash (rc %d) in %s, seed %d' % (rc, core, seed))
|
||||
print('\n'.join(messages[:3]))
|
||||
|
||||
if not crashed(run(exe, seed, 1, classes, code)[0]):
|
||||
print('Does not crash on its own, so it depends on the cases before it.')
|
||||
return 1
|
||||
i = 0
|
||||
while i < len(code):
|
||||
trial = code[:i] + code[i + 1:]
|
||||
if trial and crashed(run(exe, seed, 1, classes, trial)[0]):
|
||||
code = trial
|
||||
else:
|
||||
i += 1
|
||||
print('Reduced: PPSSPP_CPUFUZZ_SEED=%d PPSSPP_CPUFUZZ_CODE=%s' % (seed, ','.join(code)))
|
||||
_, out = run(exe, seed, 1, classes, code, verbose=True)
|
||||
shown = set()
|
||||
for l in out.splitlines():
|
||||
if (CODE_LINE.match(l) or l.startswith('(')) and l not in shown:
|
||||
shown.add(l)
|
||||
print(l)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -16,6 +16,7 @@
|
||||
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
||||
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
@@ -367,3 +368,646 @@ bool TestJitReplacementHooks() {
|
||||
DestroyJitHarness();
|
||||
return success;
|
||||
}
|
||||
|
||||
// Differential fuzzing of the CPU cores. Random straight-line code (ALU, FPU, VFPU, loads and
|
||||
// stores) runs from a random register state on each core, and every core has to end up with the
|
||||
// interpreter's state, NaN payloads aside.
|
||||
//
|
||||
// Environment variables:
|
||||
// PPSSPP_CPUFUZZ_COUNT number of cases (default 2000)
|
||||
// PPSSPP_CPUFUZZ_SEED first case's seed (default 1). Case i always uses seed + i.
|
||||
// PPSSPP_CPUFUZZ_DUMP file to write the interpreter's end state of every case to, for diffing
|
||||
// the results of different host architectures.
|
||||
// PPSSPP_CPUFUZZ_CLASSES instruction classes to generate, as a weighted string of "aifvm" (alu,
|
||||
// imm, fpu, vfpu, memory).
|
||||
// PPSSPP_CPUFUZZ_VERBOSE print each case before running it, to find one that crashes.
|
||||
// PPSSPP_CPUFUZZ_CODE comma-separated hex instructions to run in place of the generated
|
||||
// code, from the generated start state.
|
||||
// PPSSPP_CPUFUZZ_CODE_SEED apply PPSSPP_CPUFUZZ_CODE to this seed only.
|
||||
// PPSSPP_CPUFUZZ_SKIP cores not to run, by digit: 1 IR interpreter, 2 JIT, 3 JIT IR.
|
||||
// PPSSPP_CPUFUZZ_REPORTS how many differences to minimize and print (default 20).
|
||||
// PPSSPP_CPUFUZZ_STRICT fail the test on any difference, not only on a crash.
|
||||
|
||||
namespace {
|
||||
|
||||
struct FuzzRng {
|
||||
u64 s;
|
||||
explicit FuzzRng(u64 seed) : s(seed) {
|
||||
// Otherwise seed + 1 gives the same stream, one step on.
|
||||
s = Next64();
|
||||
}
|
||||
u64 Next64() {
|
||||
u64 z = (s += 0x9E3779B97F4A7C15ULL);
|
||||
z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL;
|
||||
z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL;
|
||||
return z ^ (z >> 31);
|
||||
}
|
||||
u32 Next() { return (u32)(Next64() >> 32); }
|
||||
u32 Below(u32 n) { return (u32)(((u64)Next() * n) >> 32); }
|
||||
bool Chance(u32 percent) { return Below(100) < percent; }
|
||||
};
|
||||
|
||||
struct FuzzState {
|
||||
u32 r[32];
|
||||
u32 f[32];
|
||||
u32 v[128];
|
||||
u32 vfpuCtrl[16];
|
||||
u32 hi, lo, fcr31, fpcond;
|
||||
int llBit;
|
||||
u8 mem[1024];
|
||||
};
|
||||
|
||||
// The loads and stores hit scratch memory through k0, which points at its middle.
|
||||
static const u32 FUZZ_CODE_OFFSET = 0;
|
||||
static const u32 FUZZ_MEM_OFFSET = 0x10000;
|
||||
static const int FUZZ_BASE_REG = MIPS_REG_K0;
|
||||
static const int FUZZ_MAX_INSTRS = 24;
|
||||
|
||||
static u32 FuzzFloatBits(FuzzRng &rng) {
|
||||
static const u32 specials[] = {
|
||||
0x00000000, 0x80000000, 0x3F800000, 0xBF800000, 0x3F000000, 0xBF000000, 0x40000000, 0x3FC00000,
|
||||
0x7F800000, 0xFF800000, 0x7FC00000, 0xFFC00000, 0x7F800001, 0x7FBFFFFF, 0xFF812345,
|
||||
0x00000001, 0x80000001, 0x007FFFFF, 0x807FFFFF, 0x00800000, 0x80800000, 0x7F7FFFFF, 0xFF7FFFFF,
|
||||
0x4F000000, 0xCF000000, 0x4EFFFFFF, 0xCF000001, 0x4F800000, 0x3EFFFFFF, 0x3F000001, 0x3FBFFFFF,
|
||||
0x40400000, 0x3F7FFFFF, 0x34000000, 0x33800000, 0x47000000, 0x46FFFE00, 0x477FFF00, 0x3FC90FDB,
|
||||
};
|
||||
switch (rng.Below(8)) {
|
||||
case 0:
|
||||
case 1:
|
||||
return specials[rng.Below(ARRAY_SIZE(specials))];
|
||||
case 2: {
|
||||
// Small integers, some with a half.
|
||||
float f = (float)((int)rng.Below(2001) - 1000);
|
||||
if (rng.Chance(50)) {
|
||||
f += 0.5f;
|
||||
}
|
||||
u32 bits;
|
||||
memcpy(&bits, &f, 4);
|
||||
return bits;
|
||||
}
|
||||
case 3:
|
||||
// Denormals.
|
||||
return rng.Next() & 0x807FFFFF;
|
||||
case 4:
|
||||
// Near one.
|
||||
return (rng.Next() & 0x80000000) | ((rng.Below(16) + 119) << 23) | (rng.Next() & 0x7FFFFF);
|
||||
case 5:
|
||||
// Finite, in a wide range.
|
||||
return (rng.Next() & 0x80000000) | ((rng.Below(80) + 87) << 23) | (rng.Next() & 0x7FFFFF);
|
||||
default:
|
||||
return rng.Next();
|
||||
}
|
||||
}
|
||||
|
||||
static u32 FuzzIntBits(FuzzRng &rng) {
|
||||
static const u32 specials[] = {
|
||||
0, 1, 2, 3, 0xFFFFFFFF, 0xFFFFFFFE, 0x7FFFFFFF, 0x80000000, 0x80000001, 0x0000FFFF, 0x00008000,
|
||||
0xFFFF8000, 0x000000FF, 0x00000080, 0xFFFFFF80, 31, 32, 33, 0x12345678,
|
||||
};
|
||||
switch (rng.Below(6)) {
|
||||
case 0:
|
||||
return specials[rng.Below(ARRAY_SIZE(specials))];
|
||||
case 1:
|
||||
return rng.Below(64) - 32;
|
||||
case 2:
|
||||
return FuzzFloatBits(rng);
|
||||
default:
|
||||
return rng.Next();
|
||||
}
|
||||
}
|
||||
|
||||
static bool IsFuzzableName(const char *name) {
|
||||
static const char *const denied[] = {
|
||||
"cache", "vflush", "sync", "halt", "mfic", "mtic", "rdhwr", "synci", "mfc0", "mtc0", "rdpgpr",
|
||||
"mfmc0", "wrpgpr", "tlbr", "tlbwi", "tlbwr", "tlbp", "eret", "iack", "deret", "wait", "break",
|
||||
"syscall", "mfc2", "cfc2", "mtc2", "ctc2", "dis.int", "tge", "tgeu", "tlt", "tltu", "teq", "tne",
|
||||
"tgei", "tgeiu", "tlti", "tltiu", "teqi", "tnei", "??",
|
||||
};
|
||||
if (!name || !name[0]) {
|
||||
return false;
|
||||
}
|
||||
for (const char *d : denied) {
|
||||
if (!strcmp(name, d)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool IsVFPUMemOp(u32 op) {
|
||||
switch (op >> 26) {
|
||||
case 0x32: case 0x35: case 0x36: case 0x3A: case 0x3D: case 0x3E:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Points a generated load or store at the scratch memory.
|
||||
static u32 FixupMemOp(u32 op, FuzzRng &rng) {
|
||||
op = (op & ~(0x1F << 21)) | (FUZZ_BASE_REG << 21);
|
||||
s32 offset = (s32)rng.Below(512) - 256;
|
||||
if (IsVFPUMemOp(op)) {
|
||||
const u32 top = op >> 26;
|
||||
offset &= (top == 0x36 || top == 0x3E) ? ~15 : ~3;
|
||||
return (op & ~0xFFFC) | ((u32)offset & 0xFFFC);
|
||||
}
|
||||
const char *name = MIPSGetName(MIPSOpcode(op));
|
||||
const bool unaligned = !strcmp(name, "lwl") || !strcmp(name, "lwr") || !strcmp(name, "swl") || !strcmp(name, "swr");
|
||||
if (!unaligned) {
|
||||
const int size = MIPSGetMemoryAccessSize(MIPSOpcode(op));
|
||||
if (size > 1) {
|
||||
offset &= ~(size - 1);
|
||||
}
|
||||
}
|
||||
return (op & ~0xFFFF) | ((u32)offset & 0xFFFF);
|
||||
}
|
||||
|
||||
static u32 GenerateFuzzOp(FuzzRng &rng, const char *classes) {
|
||||
static const u8 aluOps[] = { 0x00, 0x1C, 0x1F };
|
||||
static const u8 immOps[] = { 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F };
|
||||
static const u8 vfpuOps[] = { 0x12, 0x18, 0x19, 0x1B, 0x34, 0x37, 0x3C, 0x3F };
|
||||
static const u8 memOps[] = { 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x28, 0x29, 0x2A, 0x2B, 0x2E, 0x30, 0x31, 0x32, 0x35, 0x36, 0x38, 0x39, 0x3A, 0x3D, 0x3E };
|
||||
|
||||
const u32 numClasses = (u32)strlen(classes);
|
||||
for (int tries = 0; tries < 100000; ++tries) {
|
||||
u32 top;
|
||||
switch (classes[rng.Below(numClasses)]) {
|
||||
case 'a': top = aluOps[rng.Below(ARRAY_SIZE(aluOps))]; break;
|
||||
case 'i': top = immOps[rng.Below(ARRAY_SIZE(immOps))]; break;
|
||||
case 'f': top = 0x11; break;
|
||||
case 'v': top = vfpuOps[rng.Below(ARRAY_SIZE(vfpuOps))]; break;
|
||||
default: top = memOps[rng.Below(ARRAY_SIZE(memOps))]; break;
|
||||
}
|
||||
u32 op = (top << 26) | (rng.Next() & 0x03FFFFFF);
|
||||
// Most COP1 formats are invalid, so mostly pick S or W.
|
||||
if (top == 0x11 && rng.Chance(80)) {
|
||||
op = (op & ~(0x1F << 21)) | ((rng.Chance(80) ? 16 : 20) << 21);
|
||||
}
|
||||
|
||||
const MIPSInfo info = MIPSGetInfo(MIPSOpcode(op));
|
||||
if (info & (BAD_INSTRUCTION | IS_CONDBRANCH | IS_JUMP | IS_SYSCALL | DELAYSLOT)) {
|
||||
continue;
|
||||
}
|
||||
if (!IsFuzzableName(MIPSGetName(MIPSOpcode(op)))) {
|
||||
continue;
|
||||
}
|
||||
// srl and srlv with other bits than rotr's set in the unused field: the interpreter does
|
||||
// nothing, the rest shift. TODO: Test on hardware.
|
||||
if (top == 0 && (op & 0x3F) == 2 && ((op >> 21) & 0x1F) > 1) {
|
||||
continue;
|
||||
}
|
||||
if (top == 0 && (op & 0x3F) == 6 && ((op >> 6) & 0x1F) > 1) {
|
||||
continue;
|
||||
}
|
||||
// Not by MEMTYPE, which movz and movn alias with their CONDTYPE.
|
||||
if (std::find(std::begin(memOps), std::end(memOps), top) != std::end(memOps)) {
|
||||
op = FixupMemOp(op, rng);
|
||||
}
|
||||
// Keep k0 pointing at the scratch memory.
|
||||
if ((info & OUT_RT) && ((op >> 16) & 0x1F) == FUZZ_BASE_REG) {
|
||||
continue;
|
||||
}
|
||||
if ((info & OUT_RD) && ((op >> 11) & 0x1F) == FUZZ_BASE_REG) {
|
||||
continue;
|
||||
}
|
||||
return op;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct FuzzCase {
|
||||
std::vector<u32> code;
|
||||
FuzzState init;
|
||||
};
|
||||
|
||||
static FuzzCase GenerateFuzzCase(u64 seed, const char *classes, const std::vector<u32> &codeOverride) {
|
||||
FuzzRng rng(seed);
|
||||
FuzzCase c;
|
||||
memset(&c.init, 0, sizeof(c.init));
|
||||
for (int i = 1; i < 32; ++i) {
|
||||
c.init.r[i] = FuzzIntBits(rng);
|
||||
}
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
c.init.f[i] = rng.Chance(80) ? FuzzFloatBits(rng) : FuzzIntBits(rng);
|
||||
}
|
||||
for (int i = 0; i < 128; ++i) {
|
||||
c.init.v[i] = rng.Chance(90) ? FuzzFloatBits(rng) : FuzzIntBits(rng);
|
||||
}
|
||||
for (int i = 0; i < (int)sizeof(c.init.mem); i += 4) {
|
||||
const u32 w = FuzzFloatBits(rng);
|
||||
memcpy(&c.init.mem[i], &w, 4);
|
||||
}
|
||||
c.init.hi = FuzzIntBits(rng);
|
||||
c.init.lo = FuzzIntBits(rng);
|
||||
// The default rounding mode. Like games, the code changes it with ctc1, which the JITs need to
|
||||
// see compiled before they use the mode at all.
|
||||
c.init.fcr31 = 0;
|
||||
if (rng.Chance(50)) {
|
||||
c.init.fcr31 |= 0x00800000;
|
||||
}
|
||||
c.init.fpcond = (c.init.fcr31 >> 23) & 1;
|
||||
c.init.vfpuCtrl[VFPU_CTRL_SPREFIX] = 0xE4;
|
||||
c.init.vfpuCtrl[VFPU_CTRL_TPREFIX] = 0xE4;
|
||||
c.init.vfpuCtrl[VFPU_CTRL_DPREFIX] = 0;
|
||||
c.init.vfpuCtrl[VFPU_CTRL_CC] = rng.Below(64);
|
||||
c.init.vfpuCtrl[VFPU_CTRL_REV] = 0x7772CEAB;
|
||||
c.init.vfpuCtrl[VFPU_CTRL_RCX0] = 0x3F800001;
|
||||
c.init.vfpuCtrl[VFPU_CTRL_RCX1] = 0x3F800002;
|
||||
c.init.vfpuCtrl[VFPU_CTRL_RCX2] = 0x3F800004;
|
||||
c.init.vfpuCtrl[VFPU_CTRL_RCX3] = 0x3F800008;
|
||||
for (int i = VFPU_CTRL_RCX4; i <= VFPU_CTRL_RCX7; ++i) {
|
||||
c.init.vfpuCtrl[i] = 0x3F800000;
|
||||
}
|
||||
|
||||
const int count = 1 + rng.Below(FUZZ_MAX_INSTRS);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
c.code.push_back(GenerateFuzzOp(rng, classes));
|
||||
}
|
||||
if (!codeOverride.empty()) {
|
||||
c.code = codeOverride;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
static void RunFuzzCase(const std::vector<u32> &code, const FuzzState &init, FuzzState *out) {
|
||||
const u32 codeAddr = PSP_GetUserMemoryBase() + FUZZ_CODE_OFFSET;
|
||||
const u32 memAddr = PSP_GetUserMemoryBase() + FUZZ_MEM_OFFSET;
|
||||
u32 addr = codeAddr;
|
||||
for (u32 op : code) {
|
||||
Memory::WriteUnchecked_U32(op, addr);
|
||||
addr += 4;
|
||||
}
|
||||
Memory::WriteUnchecked_U32(MIPS_MAKE_SYSCALL("UnitTestFakeSyscalls", "UnitTestTerminator"), addr);
|
||||
Memory::WriteUnchecked_U32(MIPS_MAKE_BREAK(1), addr + 4);
|
||||
Memory::WriteUnchecked_U32(MIPS_MAKE_JR_RA(), addr + 8);
|
||||
// The frontends compile and look ahead past the syscall, so don't leave an older case's code there.
|
||||
for (int i = 0; i < FUZZ_MAX_INSTRS; ++i) {
|
||||
Memory::WriteUnchecked_U32(MIPS_MAKE_NOP(), addr + 12 + i * 4);
|
||||
}
|
||||
if (MIPSComp::jit) {
|
||||
MIPSComp::jit->ClearCache();
|
||||
}
|
||||
|
||||
MIPSState *mips = currentMIPS;
|
||||
memcpy(mips->r, init.r, sizeof(init.r));
|
||||
mips->r[FUZZ_BASE_REG] = memAddr + 512;
|
||||
memcpy(mips->fi, init.f, sizeof(init.f));
|
||||
memcpy(mips->vi, init.v, sizeof(init.v));
|
||||
memcpy(mips->vfpuCtrl, init.vfpuCtrl, sizeof(init.vfpuCtrl));
|
||||
mips->hi = init.hi;
|
||||
mips->lo = init.lo;
|
||||
mips->fcr31 = init.fcr31;
|
||||
mips->fpcond = init.fpcond;
|
||||
mips->llBit = 0;
|
||||
mips->inDelaySlot = false;
|
||||
Memory::MemcpyUnchecked(memAddr, init.mem, sizeof(init.mem));
|
||||
|
||||
mips->pc = codeAddr;
|
||||
coreState = CORE_RUNNING_CPU;
|
||||
while (coreState == CORE_RUNNING_CPU) {
|
||||
mips->RunLoopUntil(CoreTiming::GetTicks(mips) + 1000000);
|
||||
}
|
||||
|
||||
memcpy(out->r, mips->r, sizeof(out->r));
|
||||
memcpy(out->f, mips->fi, sizeof(out->f));
|
||||
memcpy(out->v, mips->vi, sizeof(out->v));
|
||||
memcpy(out->vfpuCtrl, mips->vfpuCtrl, sizeof(out->vfpuCtrl));
|
||||
out->hi = mips->hi;
|
||||
out->lo = mips->lo;
|
||||
out->fcr31 = mips->fcr31;
|
||||
out->fpcond = mips->fpcond;
|
||||
out->llBit = mips->llBit;
|
||||
Memory::MemcpyUnchecked(out->mem, memAddr, sizeof(out->mem));
|
||||
}
|
||||
|
||||
static bool IsNaNBits(u32 bits) {
|
||||
return (bits & 0x7F800000) == 0x7F800000 && (bits & 0x007FFFFF) != 0;
|
||||
}
|
||||
|
||||
static bool FloatBitsMatch(u32 a, u32 b) {
|
||||
return a == b || (IsNaNBits(a) && IsNaNBits(b));
|
||||
}
|
||||
|
||||
// Appends a line per difference to out, if given. Returns the number of differences.
|
||||
static int DiffFuzzStates(const FuzzState &ref, const FuzzState &other, std::string *out) {
|
||||
int diffs = 0;
|
||||
auto report = [&](const std::string &what, u32 a, u32 b) {
|
||||
++diffs;
|
||||
if (out) {
|
||||
float fa, fb;
|
||||
memcpy(&fa, &a, 4);
|
||||
memcpy(&fb, &b, 4);
|
||||
char buf[256];
|
||||
snprintf(buf, sizeof(buf), " %s: interp %08x (%g) core %08x (%g)\n", what.c_str(), a, fa, b, fb);
|
||||
*out += buf;
|
||||
}
|
||||
};
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
if (ref.r[i] != other.r[i]) {
|
||||
report("r" + std::to_string(i), ref.r[i], other.r[i]);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
if (!FloatBitsMatch(ref.f[i], other.f[i])) {
|
||||
report("f" + std::to_string(i), ref.f[i], other.f[i]);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < 128; ++i) {
|
||||
if (!FloatBitsMatch(ref.v[i], other.v[i])) {
|
||||
report(GetVectorNotation(fromvoffset[i], V_Single), ref.v[i], other.v[i]);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
if (ref.vfpuCtrl[i] != other.vfpuCtrl[i]) {
|
||||
report("vfpuCtrl" + std::to_string(i), ref.vfpuCtrl[i], other.vfpuCtrl[i]);
|
||||
}
|
||||
}
|
||||
if (ref.hi != other.hi) {
|
||||
report("hi", ref.hi, other.hi);
|
||||
}
|
||||
if (ref.lo != other.lo) {
|
||||
report("lo", ref.lo, other.lo);
|
||||
}
|
||||
// Bit 23 is fpcond, which some cores copy into fcr31 when read and some don't.
|
||||
if ((ref.fcr31 & ~0x00800000) != (other.fcr31 & ~0x00800000)) {
|
||||
report("fcr31", ref.fcr31, other.fcr31);
|
||||
}
|
||||
if (ref.fpcond != other.fpcond) {
|
||||
report("fpcond", ref.fpcond, other.fpcond);
|
||||
}
|
||||
if (ref.llBit != other.llBit) {
|
||||
report("llBit", ref.llBit, other.llBit);
|
||||
}
|
||||
for (int i = 0; i < (int)sizeof(ref.mem); i += 4) {
|
||||
u32 a, b;
|
||||
memcpy(&a, &ref.mem[i], 4);
|
||||
memcpy(&b, &other.mem[i], 4);
|
||||
if (!FloatBitsMatch(a, b)) {
|
||||
report("mem+" + std::to_string(i), a, b);
|
||||
}
|
||||
}
|
||||
return diffs;
|
||||
}
|
||||
|
||||
static void DumpFuzzState(FILE *f, u64 seed, const FuzzState &s) {
|
||||
// All NaNs print the same, so their payloads don't show up as differences.
|
||||
auto fl = [](u32 v) { return IsNaNBits(v) ? 0x7FFFFFFFU : v; };
|
||||
fprintf(f, "%llu r", (unsigned long long)seed);
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
fprintf(f, " %08x", s.r[i]);
|
||||
}
|
||||
fprintf(f, " f");
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
fprintf(f, " %08x", fl(s.f[i]));
|
||||
}
|
||||
fprintf(f, " v");
|
||||
for (int i = 0; i < 128; ++i) {
|
||||
fprintf(f, " %08x", fl(s.v[i]));
|
||||
}
|
||||
fprintf(f, " c");
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
fprintf(f, " %08x", s.vfpuCtrl[i]);
|
||||
}
|
||||
fprintf(f, " %08x %08x %08x %08x %d m", s.hi, s.lo, s.fcr31, s.fpcond, s.llBit);
|
||||
for (int i = 0; i < (int)sizeof(s.mem); i += 4) {
|
||||
u32 w;
|
||||
memcpy(&w, &s.mem[i], 4);
|
||||
fprintf(f, " %08x", fl(w));
|
||||
}
|
||||
fprintf(f, "\n");
|
||||
}
|
||||
|
||||
static void PrintFuzzCode(const std::vector<u32> &code) {
|
||||
u32 addr = PSP_GetUserMemoryBase() + FUZZ_CODE_OFFSET;
|
||||
for (u32 op : code) {
|
||||
char line[512];
|
||||
MIPSDisAsm(MIPSOpcode(op), addr, line, sizeof(line), true);
|
||||
printf(" %08x %s\n", op, line);
|
||||
addr += 4;
|
||||
}
|
||||
}
|
||||
|
||||
// The start values of the registers the code reads (VFPU ones only as vectors of the op's size).
|
||||
static void PrintFuzzInputs(const std::vector<u32> &code, const FuzzState &init) {
|
||||
std::string line;
|
||||
char buf[64];
|
||||
auto add = [&](const std::string &name, u32 value) {
|
||||
float f;
|
||||
memcpy(&f, &value, 4);
|
||||
snprintf(buf, sizeof(buf), " %s=%08x (%g)", name.c_str(), value, f);
|
||||
if (line.find(buf) == std::string::npos) {
|
||||
line += buf;
|
||||
}
|
||||
};
|
||||
for (u32 op : code) {
|
||||
const MIPSInfo info = MIPSGetInfo(MIPSOpcode(op));
|
||||
if (info & IS_VFPU) {
|
||||
for (int vreg : { (int)((op >> 8) & 0x7F), (int)((op >> 16) & 0x7F) }) {
|
||||
u8 regs[4];
|
||||
const VectorSize sz = GetVecSize(MIPSOpcode(op));
|
||||
GetVectorRegs(regs, sz, vreg);
|
||||
for (int i = 0; i < GetNumVectorElements(sz); i++) {
|
||||
add(GetVectorNotation(regs[i], V_Single), init.v[voffset[regs[i]]]);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (info & IN_RS) {
|
||||
add("r" + std::to_string((op >> 21) & 0x1F), init.r[(op >> 21) & 0x1F]);
|
||||
}
|
||||
if (info & IN_RT) {
|
||||
add("r" + std::to_string((op >> 16) & 0x1F), init.r[(op >> 16) & 0x1F]);
|
||||
}
|
||||
if (info & IN_FS) {
|
||||
add("f" + std::to_string((op >> 11) & 0x1F), init.f[(op >> 11) & 0x1F]);
|
||||
}
|
||||
if (info & IN_FT) {
|
||||
add("f" + std::to_string((op >> 16) & 0x1F), init.f[(op >> 16) & 0x1F]);
|
||||
}
|
||||
}
|
||||
if (!line.empty()) {
|
||||
printf(" inputs:%s\n", line.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
static const char *CoreName(CPUCore core) {
|
||||
switch (core) {
|
||||
case CPUCore::INTERPRETER: return "Interpreter";
|
||||
case CPUCore::IR_INTERPRETER: return "IR interpreter";
|
||||
case CPUCore::JIT: return "JIT";
|
||||
case CPUCore::JIT_IR: return "JIT IR";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
static bool CoreMismatches(CPUCore core, const std::vector<u32> &code, const FuzzState &init) {
|
||||
FuzzState ref, other;
|
||||
mipsr4k.UpdateCore(CPUCore::INTERPRETER);
|
||||
RunFuzzCase(code, init, &ref);
|
||||
mipsr4k.UpdateCore(core);
|
||||
RunFuzzCase(code, init, &other);
|
||||
return DiffFuzzStates(ref, other, nullptr) != 0;
|
||||
}
|
||||
|
||||
// Drops instructions while the core still disagrees with the interpreter.
|
||||
static std::vector<u32> MinimizeFuzzCase(CPUCore core, std::vector<u32> code, const FuzzState &init) {
|
||||
for (size_t i = 0; i < code.size(); ) {
|
||||
std::vector<u32> shorter = code;
|
||||
shorter.erase(shorter.begin() + i);
|
||||
if (!shorter.empty() && CoreMismatches(core, shorter, init)) {
|
||||
code = shorter;
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool TestCPUEquivalence() {
|
||||
SetupJitHarness();
|
||||
g_Config.bFastMemory = true;
|
||||
// Otherwise the JITs drop the registers a syscall would fill with 0xDEADBEEF, which the
|
||||
// terminating syscall doesn't.
|
||||
const bool oldSkipDeadbeef = g_Config.bSkipDeadbeefFilling;
|
||||
g_Config.bSkipDeadbeefFilling = true;
|
||||
|
||||
const char *countStr = getenv("PPSSPP_CPUFUZZ_COUNT");
|
||||
const char *seedStr = getenv("PPSSPP_CPUFUZZ_SEED");
|
||||
const char *dumpPath = getenv("PPSSPP_CPUFUZZ_DUMP");
|
||||
const char *classes = getenv("PPSSPP_CPUFUZZ_CLASSES");
|
||||
const bool verbose = getenv("PPSSPP_CPUFUZZ_VERBOSE") != nullptr;
|
||||
const char *skipCores = getenv("PPSSPP_CPUFUZZ_SKIP");
|
||||
const int count = countStr ? atoi(countStr) : 2000;
|
||||
const u64 firstSeed = seedStr ? strtoull(seedStr, nullptr, 10) : 1;
|
||||
if (!classes || !classes[0]) {
|
||||
classes = "aaiffvvvmm";
|
||||
}
|
||||
std::vector<u32> codeOverride;
|
||||
const char *codeSeedStr = getenv("PPSSPP_CPUFUZZ_CODE_SEED");
|
||||
const u64 codeSeed = codeSeedStr ? strtoull(codeSeedStr, nullptr, 10) : 0;
|
||||
const std::vector<u32> noOverride;
|
||||
auto overrideFor = [&](u64 seed) -> const std::vector<u32> & {
|
||||
return codeSeed == 0 || codeSeed == seed ? codeOverride : noOverride;
|
||||
};
|
||||
if (const char *codeStr = getenv("PPSSPP_CPUFUZZ_CODE")) {
|
||||
char *end = nullptr;
|
||||
for (const char *p = codeStr; *p; p = end) {
|
||||
codeOverride.push_back((u32)strtoul(p, &end, 16));
|
||||
if (end == p) {
|
||||
break;
|
||||
}
|
||||
while (*end == ',' || *end == ' ') {
|
||||
end++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static const CPUCore cores[] = {
|
||||
CPUCore::INTERPRETER,
|
||||
CPUCore::IR_INTERPRETER,
|
||||
CPUCore::JIT,
|
||||
#if !PPSSPP_PLATFORM(MAC)
|
||||
CPUCore::JIT_IR,
|
||||
#endif
|
||||
};
|
||||
const int numCores = (int)ARRAY_SIZE(cores);
|
||||
|
||||
std::vector<FuzzState> results((size_t)count * numCores);
|
||||
for (int c = 0; c < numCores; ++c) {
|
||||
if (c > 0 && skipCores && strchr(skipCores, '0' + c)) {
|
||||
for (int i = 0; i < count; ++i) {
|
||||
results[(size_t)i * numCores + c] = results[(size_t)i * numCores];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
mipsr4k.UpdateCore(cores[c]);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
FuzzCase fc = GenerateFuzzCase(firstSeed + i, classes, overrideFor(firstSeed + i));
|
||||
if (verbose) {
|
||||
printf("%s seed %llu\n", CoreName(cores[c]), (unsigned long long)(firstSeed + i));
|
||||
PrintFuzzCode(fc.code);
|
||||
fflush(stdout);
|
||||
}
|
||||
RunFuzzCase(fc.code, fc.init, &results[(size_t)i * numCores + c]);
|
||||
}
|
||||
}
|
||||
|
||||
if (dumpPath) {
|
||||
FILE *f = fopen(dumpPath, "w");
|
||||
if (f) {
|
||||
for (int i = 0; i < count; ++i) {
|
||||
DumpFuzzState(f, firstSeed + i, results[(size_t)i * numCores]);
|
||||
}
|
||||
fclose(f);
|
||||
} else {
|
||||
printf("Couldn't write %s\n", dumpPath);
|
||||
}
|
||||
}
|
||||
|
||||
int failures[ARRAY_SIZE(cores)]{};
|
||||
const char *reportsStr = getenv("PPSSPP_CPUFUZZ_REPORTS");
|
||||
const int maxReports = reportsStr ? atoi(reportsStr) : 20;
|
||||
// Minimized differences by the name of their last instruction, which is usually the culprit.
|
||||
std::map<std::string, int> culprits;
|
||||
int reports = 0;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const FuzzState &ref = results[(size_t)i * numCores];
|
||||
for (int c = 1; c < numCores; ++c) {
|
||||
if (DiffFuzzStates(ref, results[(size_t)i * numCores + c], nullptr) == 0) {
|
||||
continue;
|
||||
}
|
||||
++failures[c];
|
||||
if (reports >= maxReports) {
|
||||
continue;
|
||||
}
|
||||
++reports;
|
||||
FuzzCase fc = GenerateFuzzCase(firstSeed + i, classes, overrideFor(firstSeed + i));
|
||||
std::vector<u32> code = MinimizeFuzzCase(cores[c], fc.code, fc.init);
|
||||
FuzzState a, b;
|
||||
mipsr4k.UpdateCore(CPUCore::INTERPRETER);
|
||||
RunFuzzCase(code, fc.init, &a);
|
||||
mipsr4k.UpdateCore(cores[c]);
|
||||
RunFuzzCase(code, fc.init, &b);
|
||||
std::string diff;
|
||||
DiffFuzzStates(a, b, &diff);
|
||||
culprits[std::string(CoreName(cores[c])) + ": " + MIPSGetName(MIPSOpcode(code.back()))]++;
|
||||
printf("Seed %llu: %s differs from the interpreter (fcr31 %08x, cc %02x):\n", (unsigned long long)(firstSeed + i), CoreName(cores[c]), fc.init.fcr31, fc.init.vfpuCtrl[VFPU_CTRL_CC]);
|
||||
PrintFuzzCode(code);
|
||||
PrintFuzzInputs(code, fc.init);
|
||||
printf("%s", diff.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (!culprits.empty()) {
|
||||
std::vector<std::pair<int, std::string>> sorted;
|
||||
for (const auto &it : culprits) {
|
||||
sorted.emplace_back(it.second, it.first);
|
||||
}
|
||||
std::sort(sorted.rbegin(), sorted.rend());
|
||||
printf("Reported differences by last instruction:\n");
|
||||
for (const auto &it : sorted) {
|
||||
printf(" %4d %s\n", it.first, it.second.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
bool success = true;
|
||||
for (int c = 1; c < numCores; ++c) {
|
||||
printf("%s: %d of %d cases differ from the interpreter\n", CoreName(cores[c]), failures[c], count);
|
||||
if (failures[c]) {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
// Known differences remain (see docs/cpu-equivalence.md), so only crashes fail the test by default.
|
||||
if (!success && !getenv("PPSSPP_CPUFUZZ_STRICT")) {
|
||||
printf("Not failing, set PPSSPP_CPUFUZZ_STRICT to fail on differences.\n");
|
||||
success = true;
|
||||
}
|
||||
|
||||
mipsr4k.UpdateCore(CPUCore::INTERPRETER);
|
||||
DestroyJitHarness();
|
||||
g_Config.bSkipDeadbeefFilling = oldSkipDeadbeef;
|
||||
return success;
|
||||
}
|
||||
@@ -19,3 +19,4 @@
|
||||
|
||||
bool TestJit();
|
||||
bool TestJitReplacementHooks();
|
||||
bool TestCPUEquivalence();
|
||||
@@ -3446,6 +3446,7 @@ TestItem availableTests[] = {
|
||||
TEST_ITEM(IRPassSimplify),
|
||||
TEST_ITEM(Jit),
|
||||
TEST_ITEM(JitReplacementHooks),
|
||||
TEST_ITEM(CPUEquivalence),
|
||||
TEST_ITEM(VFPUMatrixTranspose),
|
||||
TEST_ITEM(ParseLBN),
|
||||
TEST_ITEM(CachingFileLoader),
|
||||
|
||||
Reference in new issue
Block a user