diff --git a/Tools/cpu_fuzz_reduce.py b/Tools/cpu_fuzz_reduce.py new file mode 100644 index 0000000000..11719e781f --- /dev/null +++ b/Tools/cpu_fuzz_reduce.py @@ -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()) diff --git a/unittest/JitHarness.cpp b/unittest/JitHarness.cpp index b890b09219..cb574b2359 100644 --- a/unittest/JitHarness.cpp +++ b/unittest/JitHarness.cpp @@ -16,6 +16,7 @@ // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. #include +#include #include #include @@ -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 code; + FuzzState init; +}; + +static FuzzCase GenerateFuzzCase(u64 seed, const char *classes, const std::vector &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 &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 &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 &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 &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 MinimizeFuzzCase(CPUCore core, std::vector code, const FuzzState &init) { + for (size_t i = 0; i < code.size(); ) { + std::vector 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 codeOverride; + const char *codeSeedStr = getenv("PPSSPP_CPUFUZZ_CODE_SEED"); + const u64 codeSeed = codeSeedStr ? strtoull(codeSeedStr, nullptr, 10) : 0; + const std::vector noOverride; + auto overrideFor = [&](u64 seed) -> const std::vector & { + 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 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 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 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> 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; +} diff --git a/unittest/JitHarness.h b/unittest/JitHarness.h index 2e6a53928d..8755656630 100644 --- a/unittest/JitHarness.h +++ b/unittest/JitHarness.h @@ -19,3 +19,4 @@ bool TestJit(); bool TestJitReplacementHooks(); +bool TestCPUEquivalence(); diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp index dcad902de4..3525372f2b 100644 --- a/unittest/UnitTest.cpp +++ b/unittest/UnitTest.cpp @@ -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),