diff --git a/src/core-tests/CMakeLists.txt b/src/core-tests/CMakeLists.txt index 1cd4406a6..afc5d8102 100644 --- a/src/core-tests/CMakeLists.txt +++ b/src/core-tests/CMakeLists.txt @@ -4,6 +4,7 @@ add_executable(core-tests cheats_tests.cpp cpu_disasm_tests.cpp + gte_tests.cpp lightgun_controller_tests.cpp negcon_base_tests.cpp spu_tests.cpp diff --git a/src/core-tests/core-tests.vcxproj b/src/core-tests/core-tests.vcxproj index 0828eeccf..f22fc016e 100644 --- a/src/core-tests/core-tests.vcxproj +++ b/src/core-tests/core-tests.vcxproj @@ -5,6 +5,7 @@ + diff --git a/src/core-tests/core-tests.vcxproj.filters b/src/core-tests/core-tests.vcxproj.filters index f3c8cf4cb..c7f5a2c9f 100644 --- a/src/core-tests/core-tests.vcxproj.filters +++ b/src/core-tests/core-tests.vcxproj.filters @@ -4,6 +4,7 @@ + diff --git a/src/core-tests/gte_tests.cpp b/src/core-tests/gte_tests.cpp new file mode 100644 index 000000000..85e1feaf1 --- /dev/null +++ b/src/core-tests/gte_tests.cpp @@ -0,0 +1,402 @@ +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#include "core/cpu_core.h" +#include "core/gte.h" +#include "core/gte_types.h" +#include "core/types.h" + +#include "common/types.h" +#include "common/xorshift_prng.h" + +#include + +#include +#include +#include + +namespace { + +// Register indices, control registers are offset by 32. +enum : u32 +{ + REG_VXY0 = 0, + REG_VZ0 = 1, + REG_RGBC = 6, + REG_IR0 = 8, + REG_IR1 = 9, + REG_IR2 = 10, + REG_IR3 = 11, + REG_SXY0 = 12, + REG_SXY1 = 13, + REG_SXY2 = 14, + REG_RGB2 = 22, + REG_MAC0 = 24, + REG_MAC1 = 25, + REG_MAC2 = 26, + REG_MAC3 = 27, + REG_IRGB = 28, + REG_ORGB = 29, + REG_RT11RT12 = 32, + REG_TRX = 37, + REG_FLAG = 63, +}; + +enum : u32 +{ + CMD_RTPS = 0x01, + CMD_NCLIP = 0x06, + CMD_MVMVA = 0x12, + CMD_RTPT = 0x30, + CMD_GPF = 0x3D, + + INST_SF = (1u << 19), + INST_LM = (1u << 10), +}; + +using RegisterState = std::array; + +using Random = XorShift128PlusPlus; + +} // namespace + +static u32 Next32(Random& rng) +{ + return static_cast(rng.Next() >> 32); +} + +static u32 NextIndex(Random& rng, u32 count) +{ + return static_cast(rng.NextRange(static_cast(count))); +} + +static u16 NextEdge16(Random& rng) +{ + static constexpr std::array values = { + {0x0000, 0x0001, 0xFFFF, 0x7FFF, 0x8000, 0x8001, 0x1000, 0xF000, 0x00FF, 0x0100, 0x4000, 0xC000}}; + return values[NextIndex(rng, static_cast(values.size()))]; +} + +static u32 NextEdge32(Random& rng) +{ + static constexpr std::array values = {{0x00000000u, 0x00000001u, 0xFFFFFFFFu, 0x7FFFFFFFu, 0x80000000u, + 0x80000001u, 0x0000FFFFu, 0x00010000u, 0x7FFF0000u, 0xFFFF8000u}}; + return values[NextIndex(rng, static_cast(values.size()))]; +} + +static u16 NextSmall16(Random& rng) +{ + // 4.12 fixed point values around +/-1.0, as used for most matrices/vectors. + return static_cast(static_cast(NextIndex(rng, 0x2001)) - 0x1000); +} + +static u32 NextRegisterValue(Random& rng, bool typical_values) +{ + switch (NextIndex(rng, typical_values ? 2 : 5)) + { + case 0: + return ZeroExtend32(NextSmall16(rng)) | (ZeroExtend32(NextSmall16(rng)) << 16); + + case 1: + { + // sign-extended value of a random width + const u32 shift = 32 - (1 + NextIndex(rng, 24)); + return static_cast(static_cast(Next32(rng) << shift) >> shift); + } + + case 2: + return NextEdge32(rng); + + case 3: + return ZeroExtend32(NextEdge16(rng)) | (ZeroExtend32(NextEdge16(rng)) << 16); + + default: + return Next32(rng); + } +} + +static RegisterState NextRegisterState(Random& rng) +{ + // Half of the states use values which mostly stay in range, the other half hammer the overflow/saturation paths. + const bool typical_values = (NextIndex(rng, 2) == 0); + + RegisterState state; + for (u32& value : state) + value = NextRegisterValue(rng, typical_values); + + return state; +} + +static void LoadRegisterState(const RegisterState& state) +{ + GTE::Reset(); + for (u32 i = 0; i < GTE::NUM_REGS; i++) + GTE::WriteRegister(i, state[i]); +} + +static RegisterState SaveRegisterState() +{ + RegisterState state; + std::memcpy(state.data(), CPU::g_state.gte_regs.r32, sizeof(state)); + return state; +} + +static u64 HashValue(u64 hash, u32 value) +{ + // FNV-1a + for (u32 i = 0; i < 4; i++) + { + hash ^= (value >> (i * 8)) & 0xFFu; + hash *= UINT64_C(0x100000001B3); + } + return hash; +} + +/// Executes random instructions of the specified command on random register states, through both the interpreter and +/// recompiler entry points, and returns a hash of the resulting registers. +static u64 RunRandomInstructions(u32 command, u32 count, u64 seed) +{ + Random rng(seed); + u64 hash = UINT64_C(0); + + for (u32 i = 0; i < count; i++) + { + const RegisterState input = NextRegisterState(rng); + + // cop2 imm25 with random sf/lm/mvmva fields, including the unused bits. + const u32 inst = UINT32_C(0x4A000000) | ((Next32(rng) & UINT32_C(0x01FFFFC0)) | command); + + LoadRegisterState(input); + GTE::ExecuteInstruction(inst); + const RegisterState interpreter_output = SaveRegisterState(); + const u32 interpreter_orgb = GTE::ReadRegister(REG_ORGB); + + LoadRegisterState(input); + TickCount ticks = 0; + const GTE::InstructionImpl impl = GTE::GetInstructionImpl(inst, &ticks); + impl(GTE::Instruction{inst & GTE::Instruction::REQUIRED_BITS_MASK}); + const RegisterState recompiler_output = SaveRegisterState(); + + if (interpreter_output != recompiler_output) + { + ADD_FAILURE() << "Interpreter/recompiler mismatch for instruction 0x" << std::hex << inst << " at iteration " + << std::dec << i; + return 0; + } + + for (const u32 value : interpreter_output) + hash = HashValue(hash, value); + hash = HashValue(hash, interpreter_orgb); + } + + return hash; +} + +static u32 PackXY(s16 x, s16 y) +{ + return ZeroExtend32(static_cast(x)) | (ZeroExtend32(static_cast(y)) << 16); +} + +TEST(GTE, GPFSaturatesIRAndColor) +{ + const auto run = [](bool lm) { + GTE::Reset(); + GTE::WriteRegister(REG_RGBC, 0xAB000000u); + GTE::WriteRegister(REG_IR0, 0x2000); + GTE::WriteRegister(REG_IR1, static_cast(-5)); + GTE::WriteRegister(REG_IR2, 0x7FFF); + GTE::WriteRegister(REG_IR3, 100); + GTE::ExecuteInstruction(UINT32_C(0x4A000000) | INST_SF | (lm ? INST_LM : 0u) | CMD_GPF); + }; + + // MAC = (IR * IR0) >> 12 + run(false); + EXPECT_EQ(GTE::ReadRegister(REG_MAC1), static_cast(-10)); + EXPECT_EQ(GTE::ReadRegister(REG_MAC2), 0xFFFEu); + EXPECT_EQ(GTE::ReadRegister(REG_MAC3), 200u); + EXPECT_EQ(GTE::ReadRegister(REG_IR1), static_cast(-10)); + EXPECT_EQ(GTE::ReadRegister(REG_IR2), 0x7FFFu); + EXPECT_EQ(GTE::ReadRegister(REG_IR3), 200u); + + // R underflows, G overflows, B = 200 >> 4. + EXPECT_EQ(GTE::ReadRegister(REG_RGB2), 0xAB0CFF00u); + + // IR2 saturated (also sets the error bit), R/G saturated. + EXPECT_EQ(GTE::ReadRegister(REG_FLAG), 0x80B00000u); + + // With lm set, negative values are clamped to zero. + run(true); + EXPECT_EQ(GTE::ReadRegister(REG_MAC1), static_cast(-10)); + EXPECT_EQ(GTE::ReadRegister(REG_IR1), 0u); + EXPECT_EQ(GTE::ReadRegister(REG_IR2), 0x7FFFu); + EXPECT_EQ(GTE::ReadRegister(REG_IR3), 200u); + EXPECT_EQ(GTE::ReadRegister(REG_RGB2), 0xAB0CFF00u); + EXPECT_EQ(GTE::ReadRegister(REG_FLAG), 0x81B00000u); +} + +TEST(GTE, MVMVAOverflowWrapsTo44Bits) +{ + // MAC1 = (TRX * 0x1000 + RT11 * VX0) >> 12, where the intermediate sum overflows 44 bits and wraps around. + GTE::Reset(); + GTE::WriteRegister(REG_TRX, 0x7FFFFFFFu); + GTE::WriteRegister(REG_RT11RT12, 0x7FFFu); + GTE::WriteRegister(REG_VXY0, 0x7FFFu); + GTE::ExecuteInstruction(UINT32_C(0x4A000000) | INST_SF | CMD_MVMVA); + + const s64 sum = (INT64_C(0x7FFFFFFF) << 12) + (INT64_C(0x7FFF) * INT64_C(0x7FFF)); + ASSERT_GT(sum, (INT64_C(1) << 43) - 1); + const s64 wrapped = sum - (INT64_C(1) << 44); + + EXPECT_EQ(GTE::ReadRegister(REG_MAC1), static_cast(wrapped >> 12)); + EXPECT_EQ(GTE::ReadRegister(REG_MAC2), 0u); + EXPECT_EQ(GTE::ReadRegister(REG_MAC3), 0u); + EXPECT_EQ(GTE::ReadRegister(REG_IR1), static_cast(-0x8000)); + EXPECT_EQ(GTE::ReadRegister(REG_IR2), 0u); + EXPECT_EQ(GTE::ReadRegister(REG_IR3), 0u); + + // MAC1 positive overflow, IR1 saturated, error. + EXPECT_EQ(GTE::ReadRegister(REG_FLAG), 0xC1000000u); +} + +TEST(GTE, NCLIPMatchesReference) +{ + Random rng(0x4E434C4950ull); + + for (u32 i = 0; i < 100000; i++) + { + std::array v; + for (s16& value : v) + value = static_cast((NextIndex(rng, 4) == 0) ? NextEdge16(rng) : static_cast(Next32(rng))); + + GTE::Reset(); + GTE::WriteRegister(REG_SXY0, PackXY(v[0], v[1])); + GTE::WriteRegister(REG_SXY1, PackXY(v[2], v[3])); + GTE::WriteRegister(REG_SXY2, PackXY(v[4], v[5])); + GTE::ExecuteInstruction(UINT32_C(0x4A000000) | CMD_NCLIP); + + // MAC0 = SX0*SY1 + SX1*SY2 + SX2*SY0 - SX0*SY2 - SX1*SY0 - SX2*SY1 + const s64 expected = s64(v[0]) * s64(v[3]) + s64(v[2]) * s64(v[5]) + s64(v[4]) * s64(v[1]) - s64(v[0]) * s64(v[5]) - + s64(v[2]) * s64(v[1]) - s64(v[4]) * s64(v[3]); + const u32 expected_flag = + (expected > INT64_C(0x7FFFFFFF)) ? 0x80010000u : ((expected < -INT64_C(0x80000000)) ? 0x80008000u : 0u); + + ASSERT_EQ(GTE::ReadRegister(REG_MAC0), static_cast(expected)) << "iteration " << i; + ASSERT_EQ(GTE::ReadRegister(REG_FLAG), expected_flag) << "iteration " << i; + } +} + +TEST(GTE, NCLIPOverflow) +{ + // (-32768 * -32768) * 2 == 0x80000000, one past the largest positive MAC0. + GTE::Reset(); + GTE::WriteRegister(REG_SXY0, PackXY(-32768, 0)); + GTE::WriteRegister(REG_SXY1, PackXY(0, -32768)); + GTE::WriteRegister(REG_SXY2, PackXY(-32768, 32767)); + GTE::ExecuteInstruction(UINT32_C(0x4A000000) | CMD_NCLIP); + + const s64 expected = s64(-32768) * s64(-32768) + s64(-32768) * s64(0) - s64(-32768) * s64(32767) - + s64(-32768) * s64(-32768) + s64(0) * s64(32767) - s64(0) * s64(0); + EXPECT_EQ(GTE::ReadRegister(REG_MAC0), static_cast(expected)); + EXPECT_EQ(GTE::ReadRegister(REG_FLAG), 0u); +} + +TEST(GTE, ORGBMatchesReference) +{ + GTE::Reset(); + + for (s32 ir = -0x8000; ir <= 0x7FFF; ir++) + { + GTE::WriteRegister(REG_IR1, static_cast(ir)); + GTE::WriteRegister(REG_IR2, static_cast(-ir)); + GTE::WriteRegister(REG_IR3, static_cast(ir ^ 0x5555)); + + const s32 ir1 = static_cast(ir); + const s32 ir2 = static_cast(-ir); + const s32 ir3 = static_cast(ir ^ 0x5555); + const u32 expected = static_cast(std::clamp(ir1 / 0x80, 0x00, 0x1F)) | + (static_cast(std::clamp(ir2 / 0x80, 0x00, 0x1F)) << 5) | + (static_cast(std::clamp(ir3 / 0x80, 0x00, 0x1F)) << 10); + + ASSERT_EQ(GTE::ReadRegister(REG_ORGB), expected) << "IR " << ir; + ASSERT_EQ(GTE::ReadRegister(REG_IRGB), expected) << "IR " << ir; + } +} + +TEST(GTE, FlagWriteUpdatesErrorBit) +{ + GTE::Reset(); + + // Low 12 bits and the error bit are not writable, error is computed from bits 30..23 and 18..13. + GTE::WriteRegister(REG_FLAG, 0xFFFFFFFFu); + EXPECT_EQ(GTE::ReadRegister(REG_FLAG), 0xFFFFF000u); + GTE::WriteRegister(REG_FLAG, 0x80000000u); + EXPECT_EQ(GTE::ReadRegister(REG_FLAG), 0u); + GTE::WriteRegister(REG_FLAG, 0x00781000u); + EXPECT_EQ(GTE::ReadRegister(REG_FLAG), 0x00781000u); + GTE::WriteRegister(REG_FLAG, 0x00002000u); + EXPECT_EQ(GTE::ReadRegister(REG_FLAG), 0x80002000u); + GTE::WriteRegister(REG_FLAG, 0x00800000u); + EXPECT_EQ(GTE::ReadRegister(REG_FLAG), 0x80800000u); +} + +// The hashes below were generated from the reference implementation, and cover every data/control register, including +// FLAG. Any change to the GTE which alters them is visible to the guest. +TEST(GTE, RandomInstructionsMatchReference) +{ + struct Expected + { + u32 command; + const char* name; + u64 hash; + }; + static constexpr Expected expected[] = { + {0x01, "RTPS", UINT64_C(0x67DB10006F92A79A)}, {0x06, "NCLIP", UINT64_C(0x6FF888C2E9723670)}, + {0x0C, "OP", UINT64_C(0x0198396CA9B93119)}, {0x10, "DPCS", UINT64_C(0x06F2D283E74AE1CD)}, + {0x11, "INTPL", UINT64_C(0x6EF2EC0137B1B261)}, {0x12, "MVMVA", UINT64_C(0x52EE5E2C045F3B93)}, + {0x13, "NCDS", UINT64_C(0x4EE2E9B367B7A687)}, {0x14, "CDP", UINT64_C(0x5FCCBD6B3CAD4E02)}, + {0x16, "NCDT", UINT64_C(0xB348FE7BB72B5E61)}, {0x1B, "NCCS", UINT64_C(0xD7C544FE788657E0)}, + {0x1C, "CC", UINT64_C(0x9D5E7A94079B7A1B)}, {0x1E, "NCS", UINT64_C(0xC4CD369E7EE8C71B)}, + {0x20, "NCT", UINT64_C(0x6397E7FFC162420C)}, {0x28, "SQR", UINT64_C(0xECAF13972B0704E2)}, + {0x29, "DCPL", UINT64_C(0x37FCCC7EFFC1F34F)}, {0x2A, "DPCT", UINT64_C(0x8CAD4B6FEA89BD86)}, + {0x2D, "AVSZ3", UINT64_C(0x2BB924F2315C3943)}, {0x2E, "AVSZ4", UINT64_C(0xBCB5C07569392954)}, + {0x30, "RTPT", UINT64_C(0x7B5F2BE827D9D2F4)}, {0x3D, "GPF", UINT64_C(0xF2962F44DE3C7551)}, + {0x3E, "GPL", UINT64_C(0x4B553A1C4CD9DE0A)}, {0x3F, "NCCT", UINT64_C(0xD9C9884142F6EF5F)}, + }; + + for (const Expected& e : expected) + { + const u64 hash = RunRandomInstructions(e.command, 20000, UINT64_C(0x475445) + e.command); + EXPECT_EQ(hash, e.hash) << e.name << " hash is 0x" << std::hex << hash; + } +} + +// Widescreen rendering changes the screen coordinates produced by RTPS/RTPT, but should do so consistently. +TEST(GTE, RandomWidescreenProjectionMatchesReference) +{ + struct Expected + { + DisplayAspectRatio aspect_ratio; + u64 rtps_hash; + u64 rtpt_hash; + }; + static constexpr Expected expected[] = { + {DisplayAspectRatio{16, 9}, UINT64_C(0xE1872C09241C0916), UINT64_C(0xC2B954F4B811C16F)}, + {DisplayAspectRatio{19, 9}, UINT64_C(0x9D6959FBF368F868), UINT64_C(0x8FDFCA41229E8E17)}, + {DisplayAspectRatio{20, 9}, UINT64_C(0x5D3D669C26417415), UINT64_C(0x8E0DBEF4343A23F4)}, + {DisplayAspectRatio{21, 9}, UINT64_C(0x5F6A3CE96DF5F276), UINT64_C(0x8C89A270E1782513)}, + }; + + for (const Expected& e : expected) + { + GTE::SetAspectRatio(e.aspect_ratio); + + const u64 rtps_hash = RunRandomInstructions(CMD_RTPS, 10000, UINT64_C(0x57494445) + e.aspect_ratio.numerator); + const u64 rtpt_hash = RunRandomInstructions(CMD_RTPT, 10000, UINT64_C(0x57494445) + e.aspect_ratio.numerator); + EXPECT_EQ(rtps_hash, e.rtps_hash) << e.aspect_ratio.numerator << ":" << e.aspect_ratio.denominator + << " RTPS hash is 0x" << std::hex << rtps_hash; + EXPECT_EQ(rtpt_hash, e.rtpt_hash) << e.aspect_ratio.numerator << ":" << e.aspect_ratio.denominator + << " RTPT hash is 0x" << std::hex << rtpt_hash; + } + + GTE::SetAspectRatio(DisplayAspectRatio::Auto()); +}