// 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/. #include #include #include #include #include #include #include "ppsspp_config.h" #include "Common/CPUDetect.h" #include "Common/System/NativeApp.h" #include "Common/System/System.h" #include "Common/TimeUtil.h" #include "Core/ConfigValues.h" #include "Core/Debugger/SymbolMap.h" #include "Core/MIPS/JitCommon/JitCommon.h" #include "Core/MIPS/JitCommon/JitBlockCache.h" #include "Core/MIPS/MIPSCodeUtils.h" #include "Core/MIPS/MIPSDebugInterface.h" #include "Core/MIPS/MIPSAsm.h" #include "Core/MIPS/MIPSTables.h" #include "Core/MIPS/MIPSVFPUUtils.h" #include "Core/MemMap.h" #include "Core/Core.h" #include "Core/System.h" #include "Core/CoreTiming.h" #include "Core/Config.h" #include "Core/HLE/HLE.h" #include "Core/HLE/ReplaceTables.h" #include "Core/MIPS/MIPSAnalyst.h" #include "Core/MIPS/JitCommon/JitState.h" #if PPSSPP_ARCH(RISCV64) #include "Core/MIPS/RiscV/RiscVVfpuDot.h" #elif PPSSPP_ARCH(LOONGARCH64) #include "Core/MIPS/LoongArch64/LoongArch64VfpuDot.h" #endif #include "unittest/UnitTest.h" // The clock as the terminating syscall sees it, so the cores can be compared on cycles too. static u64 g_terminatorTicks; void UnitTestTerminator() { g_terminatorTicks = CoreTiming::GetTicks(currentMIPS); // Bails out of jit so we can time things. coreState = CORE_POWERDOWN; hleSkipDeadbeef(); } // What the host FPU does in HLE code: 1/3 (0x3EAAAAAB when rounding to nearest) and FLT_MIN / 2 // (0x00400000 unless flushed to zero). HLE must always see the host's own mode. static std::vector g_roundingProbes; void UnitTestRoundingProbe() { volatile float one = 1.0f, three = 3.0f, fltMin = FLT_MIN, half = 0.5f; float third = one / three; float denormal = fltMin * half; u32 bits; memcpy(&bits, &third, 4); g_roundingProbes.push_back(bits); memcpy(&bits, &denormal, 4); g_roundingProbes.push_back(bits); hleSkipDeadbeef(); } HLEFunction UnitTestFakeSyscalls[] = { {0x1234BEEF, &UnitTestTerminator, "UnitTestTerminator"}, {0x1234BEF0, &UnitTestRoundingProbe, "UnitTestRoundingProbe"}, }; double ExecCPUTest(bool clearCache = true) { int blockTicks = 1000000; if (MIPSComp::jit) { currentMIPS->pc = PSP_GetUserMemoryBase(); MIPSComp::JitAt(currentMIPS); } const double callsPerSecond = CallsPerSecond([&] { currentMIPS->pc = PSP_GetUserMemoryBase(); coreState = CORE_RUNNING_CPU; while (coreState == CORE_RUNNING_CPU) { mipsr4k.RunLoopUntil(blockTicks); } }, 0.5, 1000); if (MIPSComp::jit) { JitBlockCacheDebugInterface *cache = MIPSComp::jit->GetBlockCacheDebugInterface(); if (cache) { JitBlockDebugInfo block = cache->GetBlockDebugInfo(0); WARN_LOG(Log::JIT, "Executed %d target instrs, %d IR, for %d orig", (int)block.targetDisasm.size(), (int)block.irDisasm.size(), (int)block.origDisasm.size()); } if (clearCache) MIPSComp::jit->ClearCache(); } return callsPerSecond; } static void SetupJitHarness() { // We register a syscall so we have an easy way to finish the test. RegisterHLEModule("UnitTestFakeSyscalls", ARRAY_SIZE(UnitTestFakeSyscalls), UnitTestFakeSyscalls); // This is pretty much the bare minimum required to setup jit. coreState = CORE_RUNNING_CPU; currentMIPS = &mipsr4k; g_symbolMap = new SymbolMap(); Memory::g_MemorySize = Memory::RAM_NORMAL_SIZE; PSP_CoreParameter().cpuCore = CPUCore::INTERPRETER; PSP_CoreParameter().fastForward = true; Memory::Init(Memory::MemMapSetupFlags::Default); mipsr4k.Reset(); CoreTiming::Init(currentMIPS); } static void DestroyJitHarness() { // Clear our custom module out to be safe. HLEShutdown(); CoreTiming::Shutdown(); mipsr4k.Shutdown(); Memory::Shutdown(); coreState = CORE_POWERDOWN; currentMIPS = nullptr; delete g_symbolMap; } bool TestJit() { SetupJitHarness(); g_Config.bFastMemory = true; currentMIPS->pc = PSP_GetUserMemoryBase(); u32 *p = (u32 *)Memory::GetPointerOrException(currentMIPS->pc); // TODO: Smarter way of seeding in the code sequence. static const char *lines[] = { //"vcrsp.t C000, C100, C200", //"vdot.q C000, C100, C200", //"vmmul.q M000, M100, M200", "lui r1, 0x8910", "vmmul.q M000, M100, M200", "sv.q C000, 0(r1)", "sv.q C000, 16(r1)", "sv.q C000, 32(r1)", "sv.q C000, 48(r1)", /* "abs.s f1, f1", "cvt.w.s f1, f1", "cvt.w.s f3, f1", "cvt.w.s f0, f2", "cvt.w.s f5, f1", "cvt.w.s f6, f5", */ }; bool compileSuccess = true; u32 addr = currentMIPS->pc; DebugInterface *dbg = currentDebugMIPS; for (int i = 0; i < 100; ++i) { std::string error; for (size_t j = 0; j < ARRAY_SIZE(lines); ++j) { p++; if (!MipsAssembleOpcode(lines[j], currentDebugMIPS, addr, &error)) { printf("ERROR: %s\n", error.c_str()); compileSuccess = false; } addr += 4; } } *p++ = MIPS_MAKE_SYSCALL("UnitTestFakeSyscalls", "UnitTestTerminator"); *p++ = MIPS_MAKE_BREAK(1); *p++ = MIPS_MAKE_JR_RA(); // The cores keep their block lookup and hooks out of PSP memory, so the code has to read back // as assembled while its blocks are live. const u8 *code = Memory::GetPointerOrException(currentMIPS->pc); const std::vector assembled(code, (const u8 *)p); bool memoryClean = true; auto checkMemoryClean = [&](const char *core) { if (memcmp(code, assembled.data(), assembled.size()) != 0) { printf("%s modified the code in PSP memory\n", core); memoryClean = false; } if (MIPSComp::jit && Memory::ReadBlockShadow(PSP_GetUserMemoryBase()) == 0) { printf("%s didn't enter its block in the block shadow\n", core); memoryClean = false; } }; // Dogfood. addr = currentMIPS->pc; for (size_t j = 0; j < ARRAY_SIZE(lines); ++j) { char line[512]; MIPSDisAsm(Memory::Read_Instruction(addr), addr, line, sizeof(line), true); addr += 4; printf("%s\n", line); } printf("\n"); double jit_speed = 0.0, ir_speed = 0.0, interp_speed = 0.0; if (compileSuccess) { interp_speed = ExecCPUTest(); checkMemoryClean("Interpreter"); mipsr4k.UpdateCore(CPUCore::IR_INTERPRETER); ir_speed = ExecCPUTest(false); checkMemoryClean("IR interpreter"); MIPSComp::jit->ClearCache(); mipsr4k.UpdateCore(CPUCore::JIT); jit_speed = ExecCPUTest(false); // not clearing, so the below can do things. checkMemoryClean("JIT"); // Disassemble JitBlockCacheDebugInterface *cache = MIPSComp::jit->GetBlockCacheDebugInterface(); if (cache) { JitBlockDebugInfo block = cache->GetBlockDebugInfo(0); // Should only be one block. std::vector &lines = block.targetDisasm; // Cut off at 25 due to the repetition above. Might need tweaking for large instructions. const int cutoff = 50; for (int i = 0; i < std::min((int)lines.size(), cutoff); i++) { printf("%s\n", lines[i].c_str()); } if (lines.size() > cutoff) printf("...\n"); } printf("Jit was %fx faster than interp, IR was %fx faster.\n\n", jit_speed / interp_speed, ir_speed / interp_speed); } printf("\n"); DestroyJitHarness(); return memoryClean && jit_speed >= interp_speed; } // Times the JIT on the VFPU dot product shapes games use most, for working on their routines. Prints // nanoseconds per dot product; the operands are ordinary floats. Each group stores its results, or // the IR would drop all but the last as dead. bool TestJitDotBench() { SetupJitHarness(); g_Config.bFastMemory = true; const bool oldAccurateVdot = g_Config.bAccurateVdot; g_Config.bAccurateVdot = true; struct Sequence { const char *name; int dots; std::vector lines; }; const Sequence sequences[] = { { "vdot.q alone", 4, { "vdot.q S000, C100, C200", "vdot.q S001, C110, C210", "vdot.q S002, C120, C220", "vdot.q S003, C130, C230", "sv.q C000, 0(a0)" } }, { "vtfm4.q", 4, { "vtfm4.q C000, M100, C200", "sv.q C000, 0(a0)" } }, { "vmmul.q", 16, { "vmmul.q M000, M100, M200", "sv.q C000, 0(a0)", "sv.q C010, 16(a0)", "sv.q C020, 32(a0)", "sv.q C030, 48(a0)" } }, { "vdot.t run", 4, { "vdot.t S300, C400, C000", "vdot.t S301, C400, C010", "vdot.t S302, C400, C020", "vdot.t S303, C400, C030", "sv.q C300, 0(a0)" } }, { "vdot.q rows", 3, { "vdot.q S200, R100, C000", "vdot.q S201, R101, C000", "vdot.q S202, R102, C000", "sv.q C200, 0(a0)" } }, }; bool ok = true; for (const Sequence &seq : sequences) { currentMIPS->pc = PSP_GetUserMemoryBase(); currentMIPS->r[MIPS_REG_A0] = PSP_GetUserMemoryBase() + 0x100000; u32 addr = currentMIPS->pc; const int groups = 50; for (int i = 0; i < groups && ok; ++i) { for (const char *line : seq.lines) { std::string error; if (!MipsAssembleOpcode(line, currentDebugMIPS, addr, &error)) { printf("ERROR: %s: %s\n", line, error.c_str()); ok = false; } 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); for (int i = 0; i < 128; ++i) currentMIPS->v[i] = 0.75f + (float)((i * 37) % 101) * 0.37f * ((i & 1) ? -1.0f : 1.0f); mipsr4k.UpdateCore(CPUCore::JIT); const double callsPerSecond = ExecCPUTest(false); if (getenv("PPSSPP_JITBENCH_DISASM")) { JitBlockDebugInfo block = MIPSComp::jit->GetBlockCacheDebugInterface()->GetBlockDebugInfo(0); for (size_t i = 0; i < block.targetDisasm.size() && i < 80; i++) printf(" %s\n", block.targetDisasm[i].c_str()); } MIPSComp::jit->ClearCache(); printf("%-14s %6.2f ns per dot\n", seq.name, 1e9 / (callsPerSecond * groups * seq.dots)); } g_Config.bAccurateVdot = oldAccurateVdot; DestroyJitHarness(); return ok; } // Follows a replacement hook's block shadow entry through compiling, invalidating, clearing and // switching cores. The hook is installed the normal way, by naming a function memcpy. static bool CheckReplacementHooks(u32 func, u32 size, const std::vector &code) { auto compile = [&]() { currentMIPS->pc = func; MIPSComp::JitAt(currentMIPS); }; auto codeIntact = [&]() { return memcmp(Memory::GetPointerOrException(func), code.data(), code.size()) == 0; }; EXPECT_TRUE(Replacement_IsHooked(func)); EXPECT_EQ_HEX(Memory::ReadBlockShadow(func), (u32)Memory::BLOCK_SHADOW_HOOK_NO_JIT); EXPECT_TRUE(codeIntact()); // The disassembly shows the hook, with the instruction under it. char disasm[256]; DisAsm(func, disasm, sizeof(disasm)); EXPECT_TRUE(strstr(disasm, "replacement") != nullptr && strstr(disasm, "addiu") != nullptr); DisAsm(func + 4, disasm, sizeof(disasm)); EXPECT_TRUE(strstr(disasm, "replacement") == nullptr); static const CPUCore cores[] = { CPUCore::JIT, CPUCore::IR_INTERPRETER, }; for (CPUCore core : cores) { mipsr4k.UpdateCore(core); const u32 hook = MIPSComp::jit->GetBlockShadowHookValue(); EXPECT_EQ_HEX(Memory::ReadBlockShadow(func), hook); // Compiling replaces the hook value with the block's entry. compile(); const u32 block = Memory::ReadBlockShadow(func); EXPECT_TRUE(block != 0 && block != hook); // Destroying the block puts it back, whichever way it goes. MIPSComp::jit->InvalidateCacheAt(func, 4); EXPECT_EQ_HEX(Memory::ReadBlockShadow(func), hook); compile(); MIPSComp::jit->ClearCache(); EXPECT_EQ_HEX(Memory::ReadBlockShadow(func), MIPSComp::jit->GetBlockShadowHookValue()); // Leave a block for the core switch to destroy. compile(); EXPECT_TRUE(codeIntact()); } mipsr4k.UpdateCore(CPUCore::INTERPRETER); EXPECT_EQ_HEX(Memory::ReadBlockShadow(func), (u32)Memory::BLOCK_SHADOW_HOOK_NO_JIT); // Removing the hook while a block holds the entry clears it once the block is destroyed. mipsr4k.UpdateCore(CPUCore::JIT); compile(); RestoreReplacedInstructions(func, func + size); EXPECT_FALSE(Replacement_IsHooked(func)); currentMIPS->ProcessPendingInvalidates(); EXPECT_EQ_HEX(Memory::ReadBlockShadow(func), 0); // Rewriting the rest of the function drops the hook once the game invalidates the icache over it, // with the cache instruction (which the JITs leave to the interpreter). MIPSAnalyst::ReplaceFunctions(); const u32 bodyOp = Memory::ReadUnchecked_U32(func + 8); Memory::WriteUnchecked_U32(MIPS_MAKE_NOP(), func + 8); EXPECT_TRUE(Replacement_IsHooked(func)); currentMIPS->r[MIPS_REG_A0] = func + 8; MIPSInterpret(currentMIPS, MIPSOpcode((0x2F << 26) | (MIPS_REG_A0 << 21) | (8 << 16))); // cache 0x08, 0(a0) EXPECT_FALSE(Replacement_IsHooked(func)); EXPECT_EQ_HEX(Memory::ReadBlockShadow(func), 0); Memory::WriteUnchecked_U32(bodyOp, func + 8); // Cheats and the debugger queue the invalidation before writing. The check happens when it's processed. MIPSAnalyst::ReplaceFunctions(); currentMIPS->InvalidateICacheRangeDeferred(func + 8, 4); Memory::WriteUnchecked_U32(MIPS_MAKE_NOP(), func + 8); EXPECT_TRUE(Replacement_IsHooked(func)); currentMIPS->ProcessPendingInvalidates(); EXPECT_FALSE(Replacement_IsHooked(func)); Memory::WriteUnchecked_U32(bodyOp, func + 8); // Changed immediates (relocations) keep the hook, as they don't stop the function from matching. MIPSAnalyst::ReplaceFunctions(); Memory::WriteUnchecked_U32(bodyOp + 1, func + 8); currentMIPS->InvalidateICacheRangeDeferred(func + 8, 4); currentMIPS->ProcessPendingInvalidates(); EXPECT_TRUE(Replacement_IsHooked(func)); Memory::WriteUnchecked_U32(bodyOp, func + 8); // The game overwriting the hooked instruction drops the hook. MIPSAnalyst::ReplaceFunctions(); EXPECT_EQ_HEX(Memory::ReadBlockShadow(func), MIPSComp::jit->GetBlockShadowHookValue()); Memory::WriteUnchecked_U32(MIPS_MAKE_NOP(), func); EXPECT_EQ_HEX(ReadExecutedOp(func).encoding, MIPS_MAKE_NOP()); EXPECT_FALSE(Replacement_IsHooked(func)); EXPECT_EQ_HEX(Memory::ReadBlockShadow(func), 0); return true; } bool TestJitReplacementHooks() { SetupJitHarness(); Replacement_Init(); const u32 func = PSP_GetUserMemoryBase() + 0x1000; static const char *lines[] = { "addiu v0, a0, 0", "addiu t0, a1, 0", "addiu t1, a2, 0", "addiu t2, zero, 1", "addiu t3, zero, 2", "jr ra", "nop", }; u32 addr = func; for (const char *line : lines) { std::string error; if (!MipsAssembleOpcode(line, currentDebugMIPS, addr, &error)) { printf("ERROR: %s\n", error.c_str()); return false; } addr += 4; } const u32 size = addr - func; const u8 *codePtr = Memory::GetPointerOrException(func); const std::vector code(codePtr, codePtr + size); g_symbolMap->AddFunction("memcpy", func, size); MIPSAnalyst::RegisterFunction(func, size, "memcpy"); MIPSAnalyst::UpdateHashMap(); MIPSAnalyst::ReplaceFunctions(); const bool success = CheckReplacementHooks(func, size, code); RestoreReplacedInstructions(func, func + size); Replacement_Shutdown(); MIPSAnalyst::Reset(); 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 every core'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_HOSTEXT + or -: turn the host's optional extensions on or off for the JIT // (RISC-V Zba/Zbb/Zbs/Zcb/Zfa/Zfh/Zicond, and V with -, LoongArch // LSX/LASX). VFPUDotJit follows it too. // PPSSPP_CPUFUZZ_UNKNOWNPREFIX compile as after a block left a prefix uneaten. The first case // that leaves one does this for the rest of the run, so this reproduces // a later case on its own. // PPSSPP_CPUFUZZ_STRICT fail the test on any difference, not only on a crash. // PPSSPP_CPUFUZZ_BLOCKS print the IR and host code of every block the JITs ran, after each case. // PPSSPP_CPUFUZZ_ACCURATE_VDOT 0 to run without AccurateVdot (the VFPU's own dot products), which is on by default. // PPSSPP_CPUFUZZ_OPS comma-separated instruction name prefixes to generate only, for fast // runs on the instructions being worked on. "dot" is the dot products // and the prefixes. Without PPSSPP_CPUFUZZ_CLASSES, the class is "v" // ("vr" for "dot": r is a run of vdots the IR batches). b adds forward // conditional branches. 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]; u32 cycles; }; // 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); } // From PPSSPP_CPUFUZZ_OPS. Empty means every instruction. static std::vector g_fuzzOps; static bool MatchesFuzzOps(const char *name) { if (g_fuzzOps.empty()) { return true; } for (const std::string &prefix : g_fuzzOps) { if (!strncmp(name, prefix.c_str(), prefix.size())) { return true; } } return false; } 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': case 'r': 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))) || !MatchesFuzzOps(MIPSGetName(MIPSOpcode(op)))) { 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; }; // Turns some instructions into conditional branches forward, to a later instruction or the end, so // every case still finishes. Never two in a row, so each delay slot is an ordinary instruction. static void AddFuzzBranches(FuzzRng &rng, std::vector &code) { const int n = (int)code.size(); for (int i = 0; i + 1 < n; i++) { if (!rng.Chance(20)) continue; const int target = i + 2 + (int)rng.Below(n - i - 1); // up to n, the terminator const u32 offset = (u32)(target - (i + 1)) & 0xFFFF; const u32 rs = 1 + rng.Below(31), rt = rng.Chance(30) ? 0 : 1 + rng.Below(31); u32 op; switch (rng.Below(6)) { case 0: op = (0x04 << 26) | (rs << 21) | (rt << 16); break; // beq case 1: op = (0x05 << 26) | (rs << 21) | (rt << 16); break; // bne case 2: op = ((rng.Chance(50) ? 0x06 : 0x07) << 26) | (rs << 21); break; // blez, bgtz case 3: op = (0x01 << 26) | (rs << 21) | (rng.Below(2) << 16); break; // bltz, bgez case 4: op = ((rng.Chance(50) ? 0x14 : 0x15) << 26) | (rs << 21) | (rt << 16); break; // beql, bnel default: op = (0x01 << 26) | (rs << 21) | ((rng.Chance(50) ? 0x10 : 0x11) << 16); break; // bltzal, bgezal } code[i] = op | offset; // The delay slot mustn't be a branch. if (MIPSGetInfo(MIPSOpcode(code[i + 1])) & (IS_CONDBRANCH | IS_JUMP)) code[i + 1] = 0; i++; } } // A run of 2-4 vdots of one size against one vector, the others rows or columns of one matrix: the // shape the IR frontend turns into a Vec4VfpuMultiDot. The results go to lanes of one vector or // anywhere, sometimes over the operands. static void AppendVdotRun(FuzzRng &rng, std::vector &code) { static const VectorSize sizes[] = { V_Pair, V_Triple, V_Quad }; static const u32 sizeBits[] = { 0x0080, 0x8000, 0x8080 }; const int si = rng.Below(3); const VectorSize sz = sizes[si]; const int n = GetNumVectorElements(sz); auto regsOf = [&](int field, VectorSize s, u8 regs[4]) { GetVectorRegs(regs, s, field); }; auto singleField = [&](int voffset) { for (int f = 0; f < 128; f++) { u8 r[4]; regsOf(f, V_Single, r); if (r[0] == voffset) return f; } return (int)rng.Below(128); }; const int shared = rng.Below(128); const bool transposed = rng.Chance(50); const int base = 16 * rng.Below(8); const int count = 2 + rng.Below(3); const bool laneDests = rng.Chance(50); const int destBase = 4 * rng.Below(32); const bool sharedIsT = rng.Chance(50); int lanesUsed = 0; for (int k = 0; k < count; k++) { // A free lane, and a vector field reading the block's row or Vec4 at that lane. int lane = rng.Below(4); while (lanesUsed & (1 << lane)) lane = (lane + 1) & 3; lanesUsed |= 1 << lane; int field = -1; for (int f = 0; f < 128 && field < 0; f++) { u8 r[4]; regsOf(f, sz, r); bool ok = true; for (int i = 0; i < n; i++) ok = ok && r[i] == (transposed ? base + 4 * lane + i : base + 4 * i + lane); if (ok) field = f; } if (field < 0) field = rng.Below(128); const int dest = laneDests ? singleField(destBase + lane) : (int)rng.Below(128); const int vs = sharedIsT ? field : shared; const int vt = sharedIsT ? shared : field; code.push_back(0x64800000 | sizeBits[si] | (vt << 16) | (vs << 8) | dest); } } 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); const bool runs = strchr(classes, 'r') != nullptr; while ((int)c.code.size() < count) { if (runs && rng.Chance(25) && (int)c.code.size() + 4 <= FUZZ_MAX_INSTRS) AppendVdotRun(rng, c.code); else c.code.push_back(GenerateFuzzOp(rng, classes)); } if (strchr(classes, 'b')) AddFuzzBranches(rng, c.code); if (!codeOverride.empty()) { c.code = codeOverride; } return c; } // keepCache reruns blocks compiled by an earlier run of the same code. // From PPSSPP_CPUFUZZ_BLOCKS. static bool g_fuzzPrintBlocks = false; static void RunFuzzCase(const std::vector &code, const FuzzState &init, FuzzState *out, bool keepCache = false) { 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 && !keepCache) { 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; // As a thread switch does, or a JIT keeps converting in the previous case's rounding mode. if (MIPSComp::jit) { MIPSComp::jit->UpdateFCR31(); } 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; const u64 startTicks = CoreTiming::GetTicks(mips); while (coreState == CORE_RUNNING_CPU) { mips->RunLoopUntil(CoreTiming::GetTicks(mips) + 1000000); } out->cycles = (u32)(g_terminatorTicks - startTicks); if (g_fuzzPrintBlocks && MIPSComp::jit) { JitBlockCacheDebugInterface *blocks = MIPSComp::jit->GetBlockCacheDebugInterface(); for (int i = 0; i < blocks->GetNumBlocks(); i++) { if (!blocks->IsValidBlock(i)) continue; JitBlockDebugInfo info = blocks->GetBlockDebugInfo(i); printf("Block %08x:\n", info.originalAddress); for (const std::string &line : info.irDisasm) printf(" %s\n", line.c_str()); for (const std::string &line : info.targetDisasm) printf(" %s\n", line.c_str()); } } 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); } if (ref.cycles != other.cycles) { report("cycles", ref.cycles, other.cycles); } 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, const char *core, 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, "%s %llu r", core, (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"); } // Nops are left out, as the minimizer leaves a lot of them in code with branches. static void PrintFuzzCode(const std::vector &code) { u32 addr = PSP_GetUserMemoryBase() + FUZZ_CODE_OFFSET; for (u32 op : code) { if (op != 0) { 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 g_fuzzUnknownPrefix = false; // With PPSSPP_CPUFUZZ_UNKNOWNPREFIX, a block first leaves a prefix uneaten, after which the IR // frontend stops assuming that blocks start with the default prefixes (startDefaultPrefix). static void SwitchFuzzCore(CPUCore core) { mipsr4k.UpdateCore(core); if (g_fuzzUnknownPrefix && core != CPUCore::INTERPRETER) { FuzzState init{}, out; // vpfxd with lane 0 saturated, then the terminator. RunFuzzCase({ 0xDE000001 }, init, &out); } } static bool CoreMismatches(CPUCore core, const std::vector &code, const FuzzState &init) { FuzzState ref, other; mipsr4k.UpdateCore(CPUCore::INTERPRETER); RunFuzzCase(code, init, &ref); SwitchFuzzCore(core); RunFuzzCase(code, init, &other); return DiffFuzzStates(ref, other, nullptr) != 0; } // Drops instructions while the core still disagrees with the interpreter. With branches in the // code, they become nops instead, so no branch changes its target or lands in a delay slot. static std::vector MinimizeFuzzCase(CPUCore core, std::vector code, const FuzzState &init) { bool branches = false; for (u32 op : code) branches = branches || (MIPSGetInfo(MIPSOpcode(op)) & (IS_CONDBRANCH | IS_JUMP)) != 0; for (size_t i = 0; i < code.size(); ) { std::vector shorter = code; if (branches && code[i] == 0) { ++i; continue; } if (branches) shorter[i] = 0; else shorter.erase(shorter.begin() + i); if (!shorter.empty() && CoreMismatches(core, shorter, init)) { code = shorter; } else { ++i; } } return code; } } // namespace // Host extensions the JIT may use, from PPSSPP_CPUFUZZ_HOSTEXT: + turns on the ones detection can't // see (RISC-V only reports them for a few known SoCs), - turns them off to test the fallbacks. static void ApplyFuzzHostExtensions() { const char *ext = getenv("PPSSPP_CPUFUZZ_HOSTEXT"); if (ext && (ext[0] == '+' || ext[0] == '-')) { const bool on = ext[0] == '+'; #if PPSSPP_ARCH(RISCV64) cpu_info.RiscV_Zba = on; cpu_info.RiscV_Zbb = on; cpu_info.RiscV_Zbs = on; cpu_info.RiscV_Zcb = on; cpu_info.RiscV_Zfa = on; cpu_info.RiscV_Zfh = on; cpu_info.RiscV_Zfhmin = on; cpu_info.RiscV_Zicond = on; // V is detected everywhere, but can be turned off. if (!on) cpu_info.RiscV_V = false; #elif PPSSPP_ARCH(LOONGARCH64) if (!on) { cpu_info.LOONGARCH_LSX = false; cpu_info.LOONGARCH_LASX = false; } #endif (void)on; } } 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 bool oldAccurateVdot = g_Config.bAccurateVdot; const char *accurateVdot = getenv("PPSSPP_CPUFUZZ_ACCURATE_VDOT"); g_Config.bAccurateVdot = !accurateVdot || strcmp(accurateVdot, "0") != 0; 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"); g_fuzzUnknownPrefix = getenv("PPSSPP_CPUFUZZ_UNKNOWNPREFIX") != nullptr; g_fuzzPrintBlocks = getenv("PPSSPP_CPUFUZZ_BLOCKS") != nullptr; const int count = countStr ? atoi(countStr) : 2000; const u64 firstSeed = seedStr ? strtoull(seedStr, nullptr, 10) : 1; g_fuzzOps.clear(); if (const char *ops = getenv("PPSSPP_CPUFUZZ_OPS")) { std::string list = !strcmp(ops, "dot") ? "vdot,vhdp,vfad,vavg,vdet,vcrs,vmmul,v(h)tfm,vpfx" : ops; size_t start = 0; while (start <= list.size()) { size_t end = list.find(',', start); if (end == std::string::npos) { end = list.size(); } if (end > start) { g_fuzzOps.push_back(list.substr(start, end - start)); } start = end + 1; } } if (!classes || !classes[0]) { classes = g_fuzzOps.empty() ? "aaiffvvvmmr" : (!strcmp(getenv("PPSSPP_CPUFUZZ_OPS"), "dot") ? "vr" : "v"); } 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++; } } } ApplyFuzzHostExtensions(); static const CPUCore cores[] = { CPUCore::INTERPRETER, CPUCore::IR_INTERPRETER, CPUCore::JIT, }; 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; } SwitchFuzzCore(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 c = 0; c < numCores; ++c) { for (int i = 0; i < count; ++i) { DumpFuzzState(f, CoreName(cores[c]), firstSeed + i, results[(size_t)i * numCores + c]); } } 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); SwitchFuzzCore(cores[c]); RunFuzzCase(code, fc.init, &b); std::string diff; DiffFuzzStates(a, b, &diff); // The last instruction that isn't a nop left by minimizing. u32 last = code.back(); for (auto it = code.rbegin(); it != code.rend() && last == 0; ++it) last = *it; culprits[std::string(CoreName(cores[c])) + ": " + MIPSGetName(MIPSOpcode(last))]++; 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; g_Config.bAccurateVdot = oldAccurateVdot; return success; } static bool AssembleLines(const std::vector &lines, std::vector *out) { // Assembled in scratch memory past the fuzz code and data, then read back. const u32 base = PSP_GetUserMemoryBase() + 0x20000; u32 addr = base; for (const char *line : lines) { std::string error; if (!MipsAssembleOpcode(line, currentDebugMIPS, addr, &error)) { printf("Can't assemble '%s': %s\n", line, error.c_str()); return false; } out->push_back(Memory::ReadUnchecked_U32(addr)); addr += 4; } return true; } // The guest's rounding mode and flush-to-zero apply exactly while emulated code runs, on every // core, including ops a JIT leaves to the interpreter: HLE always sees the host's own mode, a ctc1 // takes effect at once, and a mode that arrives from outside (a savestate, a thread switch, a core // switch) also reaches blocks compiled before it. bool TestCPURoundingMode() { SetupJitHarness(); g_Config.bFastMemory = true; const bool oldSkipDeadbeef = g_Config.bSkipDeadbeefFilling; g_Config.bSkipDeadbeefFilling = true; const u32 oldJitDisable = g_Config.uJitDisableFlags; const u32 probe = MIPS_MAKE_SYSCALL("UnitTestFakeSyscalls", "UnitTestRoundingProbe"); // t0 holds the mode to set. f0/f1 = 1/3, f4 * f5 = FLT_MIN / 2, f7 = 1.5 converted by the mode // (cvt.w.s) and by round.w.s, which always rounds to nearest. std::vector setMode, preset; // The math comes right after each ctc1, before anything (like a syscall) could reapply the mode. bool success = AssembleLines({ "ctc1 t0, $31", "div.s f2, f0, f1", "mul.s f3, f4, f5", "cvt.w.s f6, f7", "round.w.s f8, f7", "nop", "ctc1 zero, $31", "div.s f9, f0, f1", "nop", }, &setMode) && AssembleLines({ "nop", "div.s f2, f0, f1", "mul.s f3, f4, f5", "cvt.w.s f6, f7", "round.w.s f8, f7", "nop", }, &preset); if (!success) { DestroyJitHarness(); return false; } // The probes go where the nops are. setMode[5] = probe; setMode[8] = probe; preset[0] = probe; preset[5] = probe; FuzzState init{}; auto setFloat = [&](int reg, float f) { memcpy(&init.f[reg], &f, 4); }; setFloat(0, 1.0f); setFloat(1, 3.0f); setFloat(4, FLT_MIN); setFloat(5, 0.5f); setFloat(7, 1.5f); static const u32 modes[] = { 0x00000001, 0x00000002, 0x00000003, 0x01000000, 0x01000003 }; static const CPUCore cores[] = { CPUCore::IR_INTERPRETER, CPUCore::JIT, }; // No flags, then ctc1 left to the interpreter, then everything the JITs can leave to it. static const u32 disableFlags[] = { 0, (u32)MIPSComp::JitDisable::FPU_XFER, 0xFFFF }; auto checkProbes = [&](const char *what) { for (size_t i = 0; i + 1 < g_roundingProbes.size(); i += 2) { if (g_roundingProbes[i] != 0x3EAAAAAB || g_roundingProbes[i + 1] != 0x00400000) { printf("%s: HLE ran in the guest's mode (probe %d: %08x %08x)\n", what, (int)(i / 2), g_roundingProbes[i], g_roundingProbes[i + 1]); success = false; } } g_roundingProbes.clear(); }; for (u32 mode : modes) { // The reference: the interpreter, which the other cores must match. mipsr4k.UpdateCore(CPUCore::INTERPRETER); FuzzState refSet, refPreset; init.r[MIPS_REG_T0] = mode; init.fcr31 = 0; RunFuzzCase(setMode, init, &refSet); checkProbes("Interpreter"); init.fcr31 = mode; RunFuzzCase(preset, init, &refPreset); checkProbes("Interpreter"); for (CPUCore core : cores) { for (u32 flags : disableFlags) { char what[128]; snprintf(what, sizeof(what), "%s, mode %08x, JitDisableFlags %08x", CoreName(core), mode, flags); g_Config.uJitDisableFlags = flags; // A fresh JIT, so the flags apply and no earlier mode has been seen. mipsr4k.UpdateCore(CPUCore::INTERPRETER); mipsr4k.UpdateCore(core); // Compiled in the default mode first, then rerun after the mode arrives from outside. FuzzState state; init.fcr31 = 0; RunFuzzCase(preset, init, &state); g_roundingProbes.clear(); init.fcr31 = mode; RunFuzzCase(preset, init, &state, true); std::string diff; if (DiffFuzzStates(refPreset, state, &diff) != 0) { printf("%s, mode set from outside:\n%s", what, diff.c_str()); success = false; } checkProbes(what); init.fcr31 = 0; RunFuzzCase(setMode, init, &state); diff.clear(); if (DiffFuzzStates(refSet, state, &diff) != 0) { printf("%s, mode set by ctc1:\n%s", what, diff.c_str()); success = false; } checkProbes(what); } } } mipsr4k.UpdateCore(CPUCore::INTERPRETER); g_Config.uJitDisableFlags = oldJitDisable; g_Config.bSkipDeadbeefFilling = oldSkipDeadbeef; DestroyJitHarness(); return success; } // The JITs' own VFPU dot product code against the reference, in every variant the host can run, // through a C-callable wrapper with the eight operands at the pointer passed. Inf and NaN only go // through the branch to the reference. bool TestVFPUDotJit() { typedef float (*DotFunc)(const float *operands); auto check = [](const char *name, DotFunc fn) { auto toBits = [](float f) { u32 bits; memcpy(&bits, &f, sizeof(bits)); return bits; }; printf("%s\n", name); return ForEachVFPUDotTestCase([&](const float a[4], const float b[4]) { float operands[8]; memcpy(operands, a, sizeof(float) * 4); memcpy(operands + 4, b, sizeof(float) * 4); const u32 expected = toBits(vfpu_dot_reference(a, b)); const u32 actual = toBits(fn(operands)); if (expected != actual) { printf("%s(%08x %08x %08x %08x, %08x %08x %08x %08x) = %08x, expected %08x\n", name, toBits(a[0]), toBits(a[1]), toBits(a[2]), toBits(a[3]), toBits(b[0]), toBits(b[1]), toBits(b[2]), toBits(b[3]), actual, expected); return false; } return true; }); }; (void)check; ApplyFuzzHostExtensions(); bool success = true; #if PPSSPP_ARCH(RISCV64) using namespace RiscVGen; using namespace MIPSComp; const bool hasExt = cpu_info.RiscV_Zbb && cpu_info.RiscV_Zicond; const bool oldZbb = cpu_info.RiscV_Zbb; const bool oldZicond = cpu_info.RiscV_Zicond; struct Variant { const char *name; bool vector; bool ext; }; const Variant variants[] = { { "scalar", false, false }, { "scalar Zbb Zicond", false, true }, { "vector", true, hasExt }, }; RiscVCodeBlock code; code.AllocCodeSpace(0x10000); // As in the JIT. code.SetAutoCompress(true); for (const Variant &v : variants) { if ((v.ext && !hasExt) || (v.vector && !cpu_info.RiscV_V)) continue; cpu_info.RiscV_Zbb = v.ext; cpu_info.RiscV_Zicond = v.ext; code.BeginWrite(0x1000); const u8 *start = code.AlignCode16(); code.MV(X12, X10); if (v.vector) { // Whatever the routine reads before writing is garbage. code.VSETIVLI(R_ZERO, 4, VType(VSew::E32, VLMul::M1, VTail::A, VMask::A)); for (int i = 0; i < 32; i++) code.VMV_VI((RiscVReg)(V0 + i), -1); } const RiscVVfpuDotRegs regs{ X12, 0, { X10, X11, X5, X6, X7, X28, X29, X30, X31, X16, X17 }, F10 }; FixupBranch special = EmitRiscVVfpuDot(&code, regs, v.vector); code.SetJumpTarget(special); code.MV(X10, X12); code.ADDI(X11, X12, 16); code.QuickJ(X5, (const u8 *)&vfpu_dot_reference); code.FlushIcache(); code.EndWrite(); if (!check(v.name, (DotFunc)start)) success = false; } code.FreeCodeSpace(); cpu_info.RiscV_Zbb = oldZbb; cpu_info.RiscV_Zicond = oldZicond; #elif PPSSPP_ARCH(LOONGARCH64) using namespace LoongArch64Gen; using namespace MIPSComp; LoongArch64CodeBlock code; code.AllocCodeSpace(0x10000); for (int lsx = 0; lsx < 2; lsx++) { if (lsx && !cpu_info.LOONGARCH_LSX) continue; code.BeginWrite(0x1000); const u8 *start = code.AlignCode16(); const LoongArch64VfpuDotRegs regs{ R4, 0, V0, V1, { V2, V3, V4, V5, V6, V7 }, { R12, R13, R14, R15, R16, R17, R18, R19, R20 }, F0, F1 }; if (lsx) { // Whatever the routine reads before writing is garbage. code.LI(R12, -1); for (int i = 0; i < 24; i++) code.VREPLGR2VR_W((LoongArch64Reg)(V0 + i), R12); code.VLD(regs.x, R4, 0); code.VLD(regs.y, R4, 16); } FixupBranch special = EmitLoongArch64VfpuDot(&code, regs, lsx != 0); code.SetJumpTarget(special); code.ADDI_D(R5, R4, 16); code.QuickJ(R12, (const u8 *)&vfpu_dot_reference); code.FlushIcache(); code.EndWrite(); if (!check(lsx ? "LSX" : "scalar", (DotFunc)start)) success = false; } code.FreeCodeSpace(); #endif return success; }