diff --git a/duckstation.sln b/duckstation.sln index a6098aa2a..d8634efc2 100644 --- a/duckstation.sln +++ b/duckstation.sln @@ -29,6 +29,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "googletest", "dep\googletes EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "common-tests", "src\common-tests\common-tests.vcxproj", "{EA2B9C7A-B8CC-42F9-879B-191A98680C10}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "core-tests", "src\core-tests\core-tests.vcxproj", "{93D9300C-B64A-4B21-A4B5-30173DE88747}" +EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "scmversion", "src\scmversion\scmversion.vcxproj", "{075CED82-6A20-46DF-94C7-9624AC9DDBEB}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "updater", "src\updater\updater.vcxproj", "{32EEAF44-57F8-4C6C-A6F0-DE5667123DD5}" @@ -523,6 +525,28 @@ Global {EA2B9C7A-B8CC-42F9-879B-191A98680C10}.ReleaseLTCG-Clang|x64.ActiveCfg = ReleaseLTCG-Clang|x64 {EA2B9C7A-B8CC-42F9-879B-191A98680C10}.ReleaseLTCG-Clang-SSE2|ARM64.ActiveCfg = ReleaseLTCG-Clang|ARM64 {EA2B9C7A-B8CC-42F9-879B-191A98680C10}.ReleaseLTCG-Clang-SSE2|x64.ActiveCfg = ReleaseLTCG-Clang-SSE2|x64 + {93D9300C-B64A-4B21-A4B5-30173DE88747}.Debug|ARM64.ActiveCfg = Debug-Clang|ARM64 + {93D9300C-B64A-4B21-A4B5-30173DE88747}.Debug|x64.ActiveCfg = Debug|x64 + {93D9300C-B64A-4B21-A4B5-30173DE88747}.Debug-Clang|ARM64.ActiveCfg = Debug-Clang|ARM64 + {93D9300C-B64A-4B21-A4B5-30173DE88747}.Debug-Clang|x64.ActiveCfg = Debug-Clang|x64 + {93D9300C-B64A-4B21-A4B5-30173DE88747}.Debug-Clang-SSE2|ARM64.ActiveCfg = Debug-Clang|ARM64 + {93D9300C-B64A-4B21-A4B5-30173DE88747}.Debug-Clang-SSE2|x64.ActiveCfg = Debug-Clang-SSE2|x64 + {93D9300C-B64A-4B21-A4B5-30173DE88747}.DebugFast|ARM64.ActiveCfg = DebugFast-Clang|ARM64 + {93D9300C-B64A-4B21-A4B5-30173DE88747}.DebugFast|x64.ActiveCfg = DebugFast|x64 + {93D9300C-B64A-4B21-A4B5-30173DE88747}.DebugFast-Clang|ARM64.ActiveCfg = DebugFast-Clang|ARM64 + {93D9300C-B64A-4B21-A4B5-30173DE88747}.DebugFast-Clang|x64.ActiveCfg = DebugFast-Clang|x64 + {93D9300C-B64A-4B21-A4B5-30173DE88747}.Devel-Clang|ARM64.ActiveCfg = Devel-Clang|ARM64 + {93D9300C-B64A-4B21-A4B5-30173DE88747}.Devel-Clang|x64.ActiveCfg = Devel-Clang|x64 + {93D9300C-B64A-4B21-A4B5-30173DE88747}.Release|ARM64.ActiveCfg = Release-Clang|ARM64 + {93D9300C-B64A-4B21-A4B5-30173DE88747}.Release|x64.ActiveCfg = Release|x64 + {93D9300C-B64A-4B21-A4B5-30173DE88747}.Release-Clang|ARM64.ActiveCfg = Release-Clang|ARM64 + {93D9300C-B64A-4B21-A4B5-30173DE88747}.Release-Clang|x64.ActiveCfg = Release-Clang|x64 + {93D9300C-B64A-4B21-A4B5-30173DE88747}.ReleaseLTCG|ARM64.ActiveCfg = ReleaseLTCG-Clang|ARM64 + {93D9300C-B64A-4B21-A4B5-30173DE88747}.ReleaseLTCG|x64.ActiveCfg = ReleaseLTCG|x64 + {93D9300C-B64A-4B21-A4B5-30173DE88747}.ReleaseLTCG-Clang|ARM64.ActiveCfg = ReleaseLTCG-Clang|ARM64 + {93D9300C-B64A-4B21-A4B5-30173DE88747}.ReleaseLTCG-Clang|x64.ActiveCfg = ReleaseLTCG-Clang|x64 + {93D9300C-B64A-4B21-A4B5-30173DE88747}.ReleaseLTCG-Clang-SSE2|ARM64.ActiveCfg = ReleaseLTCG-Clang|ARM64 + {93D9300C-B64A-4B21-A4B5-30173DE88747}.ReleaseLTCG-Clang-SSE2|x64.ActiveCfg = ReleaseLTCG-Clang-SSE2|x64 {075CED82-6A20-46DF-94C7-9624AC9DDBEB}.Debug|ARM64.ActiveCfg = Debug-Clang|ARM64 {075CED82-6A20-46DF-94C7-9624AC9DDBEB}.Debug|x64.ActiveCfg = Debug|x64 {075CED82-6A20-46DF-94C7-9624AC9DDBEB}.Debug|x64.Build.0 = Debug|x64 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f85b4d64c..77d4cff18 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -14,5 +14,6 @@ endif() if(BUILD_TESTS) add_subdirectory(common-tests EXCLUDE_FROM_ALL) + add_subdirectory(core-tests EXCLUDE_FROM_ALL) add_subdirectory(util-tests EXCLUDE_FROM_ALL) endif() diff --git a/src/core-tests/CMakeLists.txt b/src/core-tests/CMakeLists.txt new file mode 100644 index 000000000..13ca690c7 --- /dev/null +++ b/src/core-tests/CMakeLists.txt @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin +# SPDX-License-Identifier: CC-BY-NC-ND-4.0 + Packaging Restriction + +add_executable(core-tests + ../core/cpu_disasm.cpp + ../core/cpu_types.cpp + cpu_disasm_tests.cpp + stub_cpu.cpp +) + +target_include_directories(core-tests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/..") +target_link_libraries(core-tests PRIVATE common gtest gtest_main) diff --git a/src/core-tests/core-tests.vcxproj b/src/core-tests/core-tests.vcxproj new file mode 100644 index 000000000..3dc5235a5 --- /dev/null +++ b/src/core-tests/core-tests.vcxproj @@ -0,0 +1,33 @@ + + + + + + + + + + + + + {49953e1b-2ef7-46a4-b88b-1bf9e099093b} + + + {ee054e08-3799-4a59-a422-18259c105ffd} + + + + {93D9300C-B64A-4B21-A4B5-30173DE88747} + + + + + + %(AdditionalIncludeDirectories);$(SolutionDir)dep\googletest\include + + + Console + + + + diff --git a/src/core-tests/core-tests.vcxproj.filters b/src/core-tests/core-tests.vcxproj.filters new file mode 100644 index 000000000..92485331c --- /dev/null +++ b/src/core-tests/core-tests.vcxproj.filters @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/core-tests/cpu_disasm_tests.cpp b/src/core-tests/cpu_disasm_tests.cpp new file mode 100644 index 000000000..5858b66ab --- /dev/null +++ b/src/core-tests/cpu_disasm_tests.cpp @@ -0,0 +1,188 @@ +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#include "core/cpu_disasm.h" + +#include "common/error.h" +#include "common/small_string.h" + +#include +#include + +#include + +namespace { + +static std::string Disassemble(u32 pc, u32 bits) +{ + SmallString result; + CPU::DisassembleInstruction(&result, pc, bits); + return std::string(result.view()); +} + +static u32 Assemble(u32 pc, std::string_view text) +{ + u32 result = 0xDEADBEEF; + Error error; + EXPECT_TRUE(CPU::AssembleInstruction(&result, pc, text, &error)) << error.GetDescription(); + return result; +} + +struct AssemblyCase +{ + std::string_view text; + u32 bits; +}; + +TEST(CPUDisasm, Disassemble) +{ + EXPECT_EQ(Disassemble(0x1000, 0x00000000), "sll zero, zero, 0"); + EXPECT_EQ(Disassemble(0x1000, 0x012A4020), "add t0, t1, t2"); + EXPECT_EQ(Disassemble(0x1000, 0x01494007), "srav t0, t1, t2"); + EXPECT_EQ(Disassemble(0x1000, 0x0109001B), "divu t0, t1"); + EXPECT_EQ(Disassemble(0x1000, 0x2128FFFC), "addi t0, t1, -4"); + EXPECT_EQ(Disassemble(0x1000, 0x11000003), "beq t0, zero, 0x00001010"); + EXPECT_EQ(Disassemble(0x1000, 0x05110003), "bgezal t0, 0x00001010"); + EXPECT_EQ(Disassemble(0x1000, 0x8FA8FFF0), "lw t0, -0x10(sp)"); + EXPECT_EQ(Disassemble(0x1000, 0xCBA60010), "lwc2 rgbc, 0x10(sp)"); + EXPECT_EQ(Disassemble(0x1000, 0x40086000), "mfc0 t0, SR"); + EXPECT_EQ(Disassemble(0x1000, 0x48481800), "cfc2 t0, rt_3"); + EXPECT_EQ(Disassemble(0x1000, 0x42000010), "rfe"); + EXPECT_EQ(Disassemble(0x1000, 0x4A080401), "rtps sf lm"); + EXPECT_EQ(Disassemble(0x1000, 0x4A000006), "nclip"); + EXPECT_EQ(Disassemble(0x1000, 0x46123456), "cop1 0x00123456"); +} + +TEST(CPUDisasm, UnknownInstructions) +{ + EXPECT_EQ(Disassemble(0x1000, 0x60000000), "UNKNOWN"); + EXPECT_EQ(Disassemble(0x1000, 0x00000001), "UNKNOWN"); + EXPECT_EQ(Disassemble(0x1000, 0x4A000002), "UNKNOWN"); +} + +TEST(CPUDisasm, Assemble) +{ + static constexpr std::array cases = { + AssemblyCase{"nop", 0x00000000}, + AssemblyCase{"NOP", 0x00000000}, + AssemblyCase{"SLL $ZERO, $zero, 0", 0x00000000}, + AssemblyCase{"srl t0, t1, 4", 0x00094102}, + AssemblyCase{"sra t0, t1, 4", 0x00094103}, + AssemblyCase{"sllv t0, t1, t2", 0x01494004}, + AssemblyCase{"srlv t0, t1, t2", 0x01494006}, + AssemblyCase{"srav t0, t1, t2", 0x01494007}, + AssemblyCase{"jr t0", 0x01000008}, + AssemblyCase{"jalr ra, t0", 0x0100F809}, + AssemblyCase{"syscall", 0x0000000C}, + AssemblyCase{"break", 0x0000000D}, + AssemblyCase{"mfhi t0", 0x00004010}, + AssemblyCase{"mthi t0", 0x01000011}, + AssemblyCase{"mflo t0", 0x00004012}, + AssemblyCase{"mtlo t0", 0x01000013}, + AssemblyCase{"mult t0, t1", 0x01090018}, + AssemblyCase{"multu t0, t1", 0x01090019}, + AssemblyCase{"div t0, t1", 0x0109001A}, + AssemblyCase{"divu t0, t1", 0x0109001B}, + AssemblyCase{"add t0, $t1, T2", 0x012A4020}, + AssemblyCase{"addu t0, t1, t2", 0x012A4021}, + AssemblyCase{"sub t0, t1, t2", 0x012A4022}, + AssemblyCase{"subu t0, t1, t2", 0x012A4023}, + AssemblyCase{"and t0, t1, t2", 0x012A4024}, + AssemblyCase{"or t0, t1, t2", 0x012A4025}, + AssemblyCase{"xor t0, t1, t2", 0x012A4026}, + AssemblyCase{"nor t0, t1, t2", 0x012A4027}, + AssemblyCase{"slt t0, t1, t2", 0x012A402A}, + AssemblyCase{"sltu t0, t1, t2", 0x012A402B}, + AssemblyCase{"j 0x1000", 0x08000400}, + AssemblyCase{"jal 0x1000", 0x0C000400}, + AssemblyCase{"beq t1, t2, 0x1010", 0x112A0003}, + AssemblyCase{"bne t1, t2, 0x1010", 0x152A0003}, + AssemblyCase{"blez t1, 0x1010", 0x19200003}, + AssemblyCase{"bgtz t1, 0x1010", 0x1D200003}, + AssemblyCase{"bltz t0, 0x1010", 0x05000003}, + AssemblyCase{"bgez t0, 0x1010", 0x05010003}, + AssemblyCase{"bltzal t0, 0x1010", 0x05100003}, + AssemblyCase{"bgezal t0, 0x1010", 0x05110003}, + AssemblyCase{"addi t0,t1,-4", 0x2128FFFC}, + AssemblyCase{"addiu t0, t1, -4", 0x2528FFFC}, + AssemblyCase{"slti t0, t1, -4", 0x2928FFFC}, + AssemblyCase{"sltiu t0, t1, 291", 0x2D280123}, + AssemblyCase{"andi t0, t1, 0xabcd", 0x3128ABCD}, + AssemblyCase{"ori t0, t1, 0xabcd", 0x3528ABCD}, + AssemblyCase{"xori t0, t1, 0xabcd", 0x3928ABCD}, + AssemblyCase{"lui t0, 0xabcd", 0x3C08ABCD}, + AssemblyCase{"lb t0, -0x10(sp)", 0x83A8FFF0}, + AssemblyCase{"lh t0, -0x10(sp)", 0x87A8FFF0}, + AssemblyCase{"lwl t0, -0x10(sp)", 0x8BA8FFF0}, + AssemblyCase{"lw t0, -0x10($sp)", 0x8FA8FFF0}, + AssemblyCase{"lbu t0, -0x10(sp)", 0x93A8FFF0}, + AssemblyCase{"lhu t0, -0x10(sp)", 0x97A8FFF0}, + AssemblyCase{"lwr t0, -0x10(sp)", 0x9BA8FFF0}, + AssemblyCase{"sb t0, -0x10(sp)", 0xA3A8FFF0}, + AssemblyCase{"sh t0, -0x10(sp)", 0xA7A8FFF0}, + AssemblyCase{"swl t0, -0x10(sp)", 0xABA8FFF0}, + AssemblyCase{"sw t0, -0x10(sp)", 0xAFA8FFF0}, + AssemblyCase{"swr t0, -0x10(sp)", 0xBBA8FFF0}, + AssemblyCase{"lwc2 rgbc, 0x10(sp)", 0xCBA60010}, + AssemblyCase{"swc2 rgbc, 0x10(sp)", 0xEBA60010}, + AssemblyCase{"mfc0 t0, sr", 0x40086000}, + AssemblyCase{"cfc2 t0, rt_3", 0x48481800}, + AssemblyCase{"mtc1 t0, 7", 0x44883800}, + AssemblyCase{"ctc3 t0, 5", 0x4CC82800}, + AssemblyCase{"rfe", 0x42000010}, + AssemblyCase{"rtps SF lm", 0x4A080401}, + AssemblyCase{"nclip", 0x4A000006}, + AssemblyCase{"mvmva sf lm m=2 v=1 t=3", 0x4A0CE412}, + AssemblyCase{"cop1 0x123456", 0x46123456}, + }; + + for (const AssemblyCase& test : cases) + EXPECT_EQ(Assemble(0x1000, test.text), test.bits) << test.text; +} + +TEST(CPUDisasm, AssembleErrorsDoNotModifyDestination) +{ + static constexpr std::array cases = { + "", "unknown t0", "nop t0", "sll t0, t1, 32", + "addi t0, t1, 32768", "andi t0, t1, -1", "lw t0, 0", "lw bad, 0(sp)", + "beq t0, t1, 3", "beq t0, t1, 0x40000", "j 0x10000000", "mfc0 t0, nope", + "mvmva m=0 v=0", "rtps sf sf", + }; + + for (const std::string_view text : cases) + { + u32 result = 0xDEADBEEF; + Error error; + EXPECT_FALSE(CPU::AssembleInstruction(&result, 0x1000, text, &error)) << text; + EXPECT_EQ(result, 0xDEADBEEFu) << text; + EXPECT_TRUE(error.IsValid()) << text; + EXPECT_FALSE(error.GetDescription().empty()) << text; + } +} + +TEST(CPUDisasm, RoundTrip) +{ + static constexpr std::array instructions = { + 0x00000000, 0x00094102, 0x00094103, 0x01494004, 0x01494006, 0x01494007, + 0x01000008, 0x0100F809, 0x0000000C, 0x0000000D, 0x00004010, 0x01000011, + 0x00004012, 0x01000013, 0x01090018, 0x01090019, 0x0109001A, 0x0109001B, + 0x012A4020, 0x012A4021, 0x012A4022, 0x012A4023, 0x012A4024, 0x012A4025, + 0x012A4026, 0x012A4027, 0x012A402A, 0x012A402B, 0x08000400, 0x0C000400, + 0x112A0003, 0x152A0003, 0x19200003, 0x1D200003, 0x05000003, 0x05010003, + 0x05100003, 0x05110003, 0x2128FFFC, 0x2528FFFC, 0x2928FFFC, 0x2D280123, + 0x3128ABCD, 0x3528ABCD, 0x3928ABCD, 0x3C08ABCD, 0x83A8FFF0, 0x8FA8FFF0, + 0xBBA8FFF0, 0xCBA60010, 0xEBA60010, 0x40086000, 0x4A0CE412, 0x46123456, + }; + + for (const u32 bits : instructions) + { + const std::string text = Disassemble(0x1000, bits); + u32 assembled = 0; + Error error; + ASSERT_TRUE(CPU::AssembleInstruction(&assembled, 0x1000, text, &error)) + << fmt::format("0x{:08x}: {}: {}", bits, text, error.GetDescription()); + EXPECT_EQ(assembled, bits) << text; + } +} + +} // namespace diff --git a/src/core-tests/stub_cpu.cpp b/src/core-tests/stub_cpu.cpp new file mode 100644 index 000000000..c6684332c --- /dev/null +++ b/src/core-tests/stub_cpu.cpp @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#include "core/cpu_core.h" + +CPU::State CPU::g_state; + +bool CPU::SafeReadMemoryByte(VirtualMemoryAddress addr, u8* value) +{ + (void)addr; + *value = 0; + return false; +} + +bool CPU::SafeReadMemoryHalfWord(VirtualMemoryAddress addr, u16* value) +{ + (void)addr; + *value = 0; + return false; +} + +bool CPU::SafeReadMemoryWord(VirtualMemoryAddress addr, u32* value) +{ + (void)addr; + *value = 0; + return false; +} diff --git a/src/core/cpu_core.h b/src/core/cpu_core.h index de8e0880b..d9928f1ec 100644 --- a/src/core/cpu_core.h +++ b/src/core/cpu_core.h @@ -11,6 +11,7 @@ #include #include +#include #include #include diff --git a/src/core/cpu_disasm.cpp b/src/core/cpu_disasm.cpp index 8f9acc023..d8696524c 100644 --- a/src/core/cpu_disasm.cpp +++ b/src/core/cpu_disasm.cpp @@ -6,30 +6,21 @@ #include "cpu_types.h" #include "common/assert.h" +#include "common/error.h" #include "common/small_string.h" +#include "common/string_util.h" #include +#include namespace CPU { namespace { -enum Operand : u8 +enum class AssembleResult { - Operand_None, - i_rs, - i_rt, - i_imm, - j_target, - r_rs, - r_rt, - r_rd, - r_shamt, - r_funct -}; - -struct TableEntry -{ - const char* format; + NoMatch, + Success, + Error, }; struct GTEInstructionTable @@ -54,9 +45,24 @@ static void FormatCopComment(SmallStringBase* dest, u32 pc, const Instruction in static void FormatGTEInstruction(SmallStringBase* dest, u32 pc, const Instruction inst); +static std::string_view GetMnemonic(std::string_view text); +static std::string_view GetOperands(std::string_view text); +static std::string GetFormatMnemonic(const char* format, u32 cop_n); +static bool SplitOperands(std::string_view text, std::array* operands, size_t* count); +static bool ParseSignedValue(std::string_view text, s64* value); +static bool ParseUnsignedValue(std::string_view text, u64* value); +static bool ParseGPR(std::string_view text, u32* index); +static bool ParseNumericRegister(std::string_view text, u32* index); +static bool ParseCopRegister(std::string_view text, u32 cop_n, bool control, u32* index); +static bool SetField(u32* bits, u32 shift, u32 width, u32 value); +static bool AssembleFormat(u32* bits, u32 pc, const char* format, std::string_view text, Error* error); +static AssembleResult TryTableEntry(u32* dest, u32 pc, std::string_view text, const char* format, u32 base_bits, + Error* error); +static AssembleResult AssembleGTEInstruction(u32* dest, std::string_view text, Error* error); + static const std::array s_base_table = {{ "", // 0 - "UNKNOWN", // 1 + nullptr, // 1 "j $jt", // 2 "jal $jt", // 3 "beq $rs, $rt, $rel", // 4 @@ -71,22 +77,22 @@ static const std::array s_base_table = {{ "ori $rt, $rs, $immx", // 13 "xori $rt, $rs, $immx", // 14 "lui $rt, $immx", // 15 - "UNKNOWN", // 16 - "UNKNOWN", // 17 - "UNKNOWN", // 18 - "UNKNOWN", // 19 - "UNKNOWN", // 20 - "UNKNOWN", // 21 - "UNKNOWN", // 22 - "UNKNOWN", // 23 - "UNKNOWN", // 24 - "UNKNOWN", // 25 - "UNKNOWN", // 26 - "UNKNOWN", // 27 - "UNKNOWN", // 28 - "UNKNOWN", // 29 - "UNKNOWN", // 30 - "UNKNOWN", // 31 + nullptr, // 16 + nullptr, // 17 + nullptr, // 18 + nullptr, // 19 + nullptr, // 20 + nullptr, // 21 + nullptr, // 22 + nullptr, // 23 + nullptr, // 24 + nullptr, // 25 + nullptr, // 26 + nullptr, // 27 + nullptr, // 28 + nullptr, // 29 + nullptr, // 30 + nullptr, // 31 "lb $rt, $offsetrs", // 32 "lh $rt, $offsetrs", // 33 "lwl $rt, $offsetrs", // 34 @@ -94,66 +100,66 @@ static const std::array s_base_table = {{ "lbu $rt, $offsetrs", // 36 "lhu $rt, $offsetrs", // 37 "lwr $rt, $offsetrs", // 38 - "UNKNOWN", // 39 + nullptr, // 39 "sb $rt, $offsetrs", // 40 "sh $rt, $offsetrs", // 41 "swl $rt, $offsetrs", // 42 "sw $rt, $offsetrs", // 43 - "UNKNOWN", // 44 - "UNKNOWN", // 45 + nullptr, // 44 + nullptr, // 45 "swr $rt, $offsetrs", // 46 - "UNKNOWN", // 47 + nullptr, // 47 "lwc0 $coprt, $offsetrs", // 48 "lwc1 $coprt, $offsetrs", // 49 "lwc2 $coprt, $offsetrs", // 50 "lwc3 $coprt, $offsetrs", // 51 - "UNKNOWN", // 52 - "UNKNOWN", // 53 - "UNKNOWN", // 54 - "UNKNOWN", // 55 + nullptr, // 52 + nullptr, // 53 + nullptr, // 54 + nullptr, // 55 "swc0 $coprt, $offsetrs", // 56 "swc1 $coprt, $offsetrs", // 57 "swc2 $coprt, $offsetrs", // 58 "swc3 $coprt, $offsetrs", // 59 - "UNKNOWN", // 60 - "UNKNOWN", // 61 - "UNKNOWN", // 62 - "UNKNOWN" // 63 + nullptr, // 60 + nullptr, // 61 + nullptr, // 62 + nullptr // 63 }}; static const std::array s_special_table = {{ "sll $rd, $rt, $shamt", // 0 - "UNKNOWN", // 1 + nullptr, // 1 "srl $rd, $rt, $shamt", // 2 "sra $rd, $rt, $shamt", // 3 "sllv $rd, $rt, $rs", // 4 - "UNKNOWN", // 5 + nullptr, // 5 "srlv $rd, $rt, $rs", // 6 "srav $rd, $rt, $rs", // 7 "jr $rs", // 8 "jalr $rd, $rs", // 9 - "UNKNOWN", // 10 - "UNKNOWN", // 11 + nullptr, // 10 + nullptr, // 11 "syscall", // 12 "break", // 13 - "UNKNOWN", // 14 - "UNKNOWN", // 15 + nullptr, // 14 + nullptr, // 15 "mfhi $rd", // 16 "mthi $rs", // 17 "mflo $rd", // 18 "mtlo $rs", // 19 - "UNKNOWN", // 20 - "UNKNOWN", // 21 - "UNKNOWN", // 22 - "UNKNOWN", // 23 + nullptr, // 20 + nullptr, // 21 + nullptr, // 22 + nullptr, // 23 "mult $rs, $rt", // 24 "multu $rs, $rt", // 25 "div $rs, $rt", // 26 "divu $rs, $rt", // 27 - "UNKNOWN", // 28 - "UNKNOWN", // 29 - "UNKNOWN", // 30 - "UNKNOWN", // 31 + nullptr, // 28 + nullptr, // 29 + nullptr, // 30 + nullptr, // 31 "add $rd, $rs, $rt", // 32 "addu $rd, $rs, $rt", // 33 "sub $rd, $rs, $rt", // 34 @@ -162,30 +168,30 @@ static const std::array s_special_table = {{ "or $rd, $rs, $rt", // 37 "xor $rd, $rs, $rt", // 38 "nor $rd, $rs, $rt", // 39 - "UNKNOWN", // 40 - "UNKNOWN", // 41 + nullptr, // 40 + nullptr, // 41 "slt $rd, $rs, $rt", // 42 "sltu $rd, $rs, $rt", // 43 - "UNKNOWN", // 44 - "UNKNOWN", // 45 - "UNKNOWN", // 46 - "UNKNOWN", // 47 - "UNKNOWN", // 48 - "UNKNOWN", // 49 - "UNKNOWN", // 50 - "UNKNOWN", // 51 - "UNKNOWN", // 52 - "UNKNOWN", // 53 - "UNKNOWN", // 54 - "UNKNOWN", // 55 - "UNKNOWN", // 56 - "UNKNOWN", // 57 - "UNKNOWN", // 58 - "UNKNOWN", // 59 - "UNKNOWN", // 60 - "UNKNOWN", // 61 - "UNKNOWN", // 62 - "UNKNOWN" // 63 + nullptr, // 44 + nullptr, // 45 + nullptr, // 46 + nullptr, // 47 + nullptr, // 48 + nullptr, // 49 + nullptr, // 50 + nullptr, // 51 + nullptr, // 52 + nullptr, // 53 + nullptr, // 54 + nullptr, // 55 + nullptr, // 56 + nullptr, // 57 + nullptr, // 58 + nullptr, // 59 + nullptr, // 60 + nullptr, // 61 + nullptr, // 62 + nullptr // 63 }}; static const std::array, 4> s_cop_common_table = { @@ -209,70 +215,70 @@ static constexpr const std::array s_cop0_register_names = { "$22", "$23", "$24", "$25", "$26", "$27", "$28", "$29", "$30", "$31"}}; static constexpr const std::array s_gte_instructions = {{ - {"UNKNOWN", false, false, false}, // 0x00 - {"rtps", true, true, false}, // 0x01 - {"UNKNOWN", false, false, false}, // 0x02 - {"UNKNOWN", false, false, false}, // 0x03 - {"UNKNOWN", false, false, false}, // 0x04 - {"UNKNOWN", false, false, false}, // 0x05 - {"nclip", false, false, false}, // 0x06 - {"UNKNOWN", false, false, false}, // 0x07 - {"UNKNOWN", false, false, false}, // 0x08 - {"UNKNOWN", false, false, false}, // 0x09 - {"UNKNOWN", false, false, false}, // 0x0A - {"UNKNOWN", false, false, false}, // 0x0B - {"op", true, true, false}, // 0x0C - {"UNKNOWN", false, false, false}, // 0x0D - {"UNKNOWN", false, false, false}, // 0x0E - {"UNKNOWN", false, false, false}, // 0x0F - {"dpcs", true, true, false}, // 0x10 - {"intpl", true, true, false}, // 0x11 - {"mvmva", true, true, true}, // 0x12 - {"ncds", true, true, false}, // 0x13 - {"cdp", true, true, false}, // 0x14 - {"UNKNOWN", false, false, false}, // 0x15 - {"ncdt", true, true, false}, // 0x16 - {"UNKNOWN", false, false, false}, // 0x17 - {"UNKNOWN", false, false, false}, // 0x18 - {"UNKNOWN", false, false, false}, // 0x19 - {"UNKNOWN", false, false, false}, // 0x1A - {"nccs", true, true, false}, // 0x1B - {"cc", true, true, false}, // 0x1C - {"UNKNOWN", false, false, false}, // 0x1D - {"ncs", true, true, false}, // 0x1E - {"UNKNOWN", false, false, false}, // 0x1F - {"nct", true, true, false}, // 0x20 - {"UNKNOWN", false, false, false}, // 0x21 - {"UNKNOWN", false, false, false}, // 0x22 - {"UNKNOWN", false, false, false}, // 0x23 - {"UNKNOWN", false, false, false}, // 0x24 - {"UNKNOWN", false, false, false}, // 0x25 - {"UNKNOWN", false, false, false}, // 0x26 - {"UNKNOWN", false, false, false}, // 0x27 - {"sqr", true, true, false}, // 0x28 - {"dcpl", true, true, false}, // 0x29 - {"dpct", true, true, false}, // 0x2A - {"UNKNOWN", false, false, false}, // 0x2B - {"UNKNOWN", false, false, false}, // 0x2C - {"avsz3", false, false, false}, // 0x2D - {"avsz4", false, false, false}, // 0x2E - {"UNKNOWN", false, false, false}, // 0x2F - {"rtpt", true, true, false}, // 0x30 - {"UNKNOWN", false, false, false}, // 0x31 - {"UNKNOWN", false, false, false}, // 0x32 - {"UNKNOWN", false, false, false}, // 0x33 - {"UNKNOWN", false, false, false}, // 0x34 - {"UNKNOWN", false, false, false}, // 0x35 - {"UNKNOWN", false, false, false}, // 0x36 - {"UNKNOWN", false, false, false}, // 0x37 - {"UNKNOWN", false, false, false}, // 0x38 - {"UNKNOWN", false, false, false}, // 0x39 - {"UNKNOWN", false, false, false}, // 0x3A - {"UNKNOWN", false, false, false}, // 0x3B - {"UNKNOWN", false, false, false}, // 0x3C - {"gpf", true, true, false}, // 0x3D - {"gpl", true, true, false}, // 0x3E - {"ncct", true, true, false}, // 0x3F + {nullptr, false, false, false}, // 0x00 + {"rtps", true, true, false}, // 0x01 + {nullptr, false, false, false}, // 0x02 + {nullptr, false, false, false}, // 0x03 + {nullptr, false, false, false}, // 0x04 + {nullptr, false, false, false}, // 0x05 + {"nclip", false, false, false}, // 0x06 + {nullptr, false, false, false}, // 0x07 + {nullptr, false, false, false}, // 0x08 + {nullptr, false, false, false}, // 0x09 + {nullptr, false, false, false}, // 0x0A + {nullptr, false, false, false}, // 0x0B + {"op", true, true, false}, // 0x0C + {nullptr, false, false, false}, // 0x0D + {nullptr, false, false, false}, // 0x0E + {nullptr, false, false, false}, // 0x0F + {"dpcs", true, true, false}, // 0x10 + {"intpl", true, true, false}, // 0x11 + {"mvmva", true, true, true}, // 0x12 + {"ncds", true, true, false}, // 0x13 + {"cdp", true, true, false}, // 0x14 + {nullptr, false, false, false}, // 0x15 + {"ncdt", true, true, false}, // 0x16 + {nullptr, false, false, false}, // 0x17 + {nullptr, false, false, false}, // 0x18 + {nullptr, false, false, false}, // 0x19 + {nullptr, false, false, false}, // 0x1A + {"nccs", true, true, false}, // 0x1B + {"cc", true, true, false}, // 0x1C + {nullptr, false, false, false}, // 0x1D + {"ncs", true, true, false}, // 0x1E + {nullptr, false, false, false}, // 0x1F + {"nct", true, true, false}, // 0x20 + {nullptr, false, false, false}, // 0x21 + {nullptr, false, false, false}, // 0x22 + {nullptr, false, false, false}, // 0x23 + {nullptr, false, false, false}, // 0x24 + {nullptr, false, false, false}, // 0x25 + {nullptr, false, false, false}, // 0x26 + {nullptr, false, false, false}, // 0x27 + {"sqr", true, true, false}, // 0x28 + {"dcpl", true, true, false}, // 0x29 + {"dpct", true, true, false}, // 0x2A + {nullptr, false, false, false}, // 0x2B + {nullptr, false, false, false}, // 0x2C + {"avsz3", false, false, false}, // 0x2D + {"avsz4", false, false, false}, // 0x2E + {nullptr, false, false, false}, // 0x2F + {"rtpt", true, true, false}, // 0x30 + {nullptr, false, false, false}, // 0x31 + {nullptr, false, false, false}, // 0x32 + {nullptr, false, false, false}, // 0x33 + {nullptr, false, false, false}, // 0x34 + {nullptr, false, false, false}, // 0x35 + {nullptr, false, false, false}, // 0x36 + {nullptr, false, false, false}, // 0x37 + {nullptr, false, false, false}, // 0x38 + {nullptr, false, false, false}, // 0x39 + {nullptr, false, false, false}, // 0x3A + {nullptr, false, false, false}, // 0x3B + {nullptr, false, false, false}, // 0x3C + {"gpf", true, true, false}, // 0x3D + {"gpl", true, true, false}, // 0x3E + {"ncct", true, true, false}, // 0x3F }}; } // namespace CPU @@ -281,6 +287,12 @@ void CPU::FormatInstruction(SmallStringBase* dest, const Instruction inst, u32 p { dest->clear(); + if (!format) + { + dest->assign("UNKNOWN"); + return; + } + const char* str = format; while (*str != '\0') { @@ -397,6 +409,9 @@ void CPU::FormatInstruction(SmallStringBase* dest, const Instruction inst, u32 p void CPU::FormatComment(SmallStringBase* dest, const Instruction inst, u32 pc, const char* format) { + if (!format) + return; + const CPU::Registers* regs = &CPU::g_state.regs; const char* str = format; @@ -599,7 +614,7 @@ void CPU::FormatCopInstruction(SmallStringBase* dest, u32 pc, const Instruction } } - dest->format("", ZeroExtend32(inst.cop.cop_n.GetValue()), inst.cop.imm25.GetValue()); + dest->format("cop{} 0x{:08x}", ZeroExtend32(inst.cop.cop_n.GetValue()), inst.cop.imm25.GetValue()); } template @@ -620,6 +635,12 @@ void CPU::FormatGTEInstruction(SmallStringBase* dest, u32 pc, const Instruction { const GTE::Instruction gi{inst.bits}; const GTEInstructionTable& t = s_gte_instructions[gi.command]; + if (!t.name) + { + dest->assign("UNKNOWN"); + return; + } + dest->assign(t.name); if (t.sf && gi.sf) @@ -673,7 +694,7 @@ void CPU::DisassembleInstruction(SmallStringBase* dest, u32 pc, u32 bits) case InstructionOp::cop3: default: { - dest->format("", ZeroExtend32(inst.cop.cop_n.GetValue()), inst.cop.imm25.GetValue()); + dest->format("cop{} 0x{:08x}", ZeroExtend32(inst.cop.cop_n.GetValue()), inst.cop.imm25.GetValue()); } break; } @@ -736,7 +757,7 @@ void CPU::DisassembleInstructionComment(SmallStringBase* dest, u32 pc, u32 bits) case InstructionOp::cop3: default: { - dest->format("", ZeroExtend32(inst.cop.cop_n.GetValue()), inst.cop.imm25.GetValue()); + dest->format("cop{} 0x{:08x}", ZeroExtend32(inst.cop.cop_n.GetValue()), inst.cop.imm25.GetValue()); } break; } @@ -772,3 +793,532 @@ const char* CPU::GetCop0RegisterName(u32 index) { return (index < s_cop0_register_names.size()) ? s_cop0_register_names[index] : ""; } + +std::string_view CPU::GetMnemonic(std::string_view text) +{ + const size_t end = text.find_first_of(" \t\r\n"); + return text.substr(0, end); +} + +std::string_view CPU::GetOperands(std::string_view text) +{ + const size_t end = text.find_first_of(" \t\r\n"); + return (end == std::string_view::npos) ? std::string_view() : StringUtil::StripWhitespace(text.substr(end)); +} + +std::string CPU::GetFormatMnemonic(const char* format, u32 cop_n) +{ + std::string mnemonic; + for (const char* ptr = format; *ptr && !StringUtil::IsWhitespace(*ptr);) + { + if (std::strncmp(ptr, "$cop", 4) == 0) + { + mnemonic.push_back(static_cast('0' + cop_n)); + ptr += 4; + } + else + { + mnemonic.push_back(*ptr++); + } + } + return mnemonic; +} + +bool CPU::SplitOperands(std::string_view text, std::array* operands, size_t* count) +{ + *count = 0; + text = StringUtil::StripWhitespace(text); + if (text.empty()) + return true; + + size_t start = 0; + u32 parentheses = 0; + for (size_t i = 0; i <= text.size(); i++) + { + const char ch = (i < text.size()) ? text[i] : ','; + if (ch == '(') + parentheses++; + else if (ch == ')') + { + if (parentheses == 0) + return false; + parentheses--; + } + else if (ch == ',' && parentheses == 0) + { + if (*count == operands->size()) + return false; + + const std::string_view operand = StringUtil::StripWhitespace(text.substr(start, i - start)); + if (operand.empty()) + return false; + (*operands)[(*count)++] = operand; + start = i + 1; + } + } + + return (parentheses == 0); +} + +bool CPU::ParseSignedValue(std::string_view text, s64* value) +{ + text = StringUtil::StripWhitespace(text); + const bool negative = (!text.empty() && text.front() == '-'); + if (negative || (!text.empty() && text.front() == '+')) + text.remove_prefix(1); + if (text.empty()) + return false; + + std::string_view end; + const std::optional parsed = StringUtil::FromCharsWithOptionalBase(text, &end); + static constexpr u64 negative_limit = static_cast(std::numeric_limits::max()) + 1; + if (!parsed.has_value() || !end.empty() || + parsed.value() > (negative ? negative_limit : static_cast(std::numeric_limits::max()))) + { + return false; + } + + if (negative) + { + *value = (parsed.value() == negative_limit) ? std::numeric_limits::min() : -static_cast(parsed.value()); + } + else + { + *value = static_cast(parsed.value()); + } + return true; +} + +bool CPU::ParseUnsignedValue(std::string_view text, u64* value) +{ + text = StringUtil::StripWhitespace(text); + std::string_view end; + const std::optional parsed = StringUtil::FromCharsWithOptionalBase(text, &end); + if (!parsed.has_value() || !end.empty()) + return false; + *value = parsed.value(); + return true; +} + +bool CPU::ParseGPR(std::string_view text, u32* index) +{ + text = StringUtil::StripWhitespace(text); + if (!text.empty() && text.front() == '$') + text.remove_prefix(1); + + for (u32 i = 0; i < static_cast(Reg::count); i++) + { + if (StringUtil::EqualNoCase(text, GetRegName(static_cast(i)))) + { + *index = i; + return true; + } + } + return false; +} + +bool CPU::ParseNumericRegister(std::string_view text, u32* index) +{ + text = StringUtil::StripWhitespace(text); + if (!text.empty() && text.front() == '$') + text.remove_prefix(1); + + u64 value; + if (!ParseUnsignedValue(text, &value) || value >= 32) + return false; + *index = static_cast(value); + return true; +} + +bool CPU::ParseCopRegister(std::string_view text, u32 cop_n, bool control, u32* index) +{ + if (cop_n == 2) + { + const u32 start = control ? 32 : 0; + for (u32 i = 0; i < 32; i++) + { + if (StringUtil::EqualNoCase(text, s_gte_register_names[start + i])) + { + *index = i; + return true; + } + } + } + else if (cop_n == 0 && !control) + { + for (u32 i = 0; i < s_cop0_register_names.size(); i++) + { + if (StringUtil::EqualNoCase(text, s_cop0_register_names[i])) + { + *index = i; + return true; + } + } + } + + return ParseNumericRegister(text, index); +} + +bool CPU::SetField(u32* bits, u32 shift, u32 width, u32 value) +{ + const u32 mask = ((UINT32_C(1) << width) - 1) << shift; + *bits = (*bits & ~mask) | (value << shift); + return true; +} + +bool CPU::AssembleFormat(u32* bits, u32 pc, const char* format, std::string_view text, Error* error) +{ + const std::string_view format_view(format); + const size_t format_space = format_view.find_first_of(" \t\r\n"); + const std::string_view format_operands = (format_space == std::string_view::npos) ? + std::string_view() : + StringUtil::StripWhitespace(format_view.substr(format_space)); + + std::array expected; + std::array actual; + size_t expected_count; + size_t actual_count; + if (!SplitOperands(format_operands, &expected, &expected_count) || + !SplitOperands(GetOperands(text), &actual, &actual_count) || expected_count != actual_count) + { + Error::SetStringFmt(error, "Expected operands matching '{}'.", format); + return false; + } + + const u32 cop_n = (*bits >> 26) & 3; + for (size_t i = 0; i < expected_count; i++) + { + const std::string_view type = expected[i]; + const std::string_view operand = actual[i]; + u32 reg; + + if (type == "$rs" || type == "$rt" || type == "$rt_" || type == "$rd") + { + if (!ParseGPR(operand, ®)) + { + Error::SetStringFmt(error, "Invalid general-purpose register '{}'.", operand); + return false; + } + + const u32 shift = (type == "$rs") ? 21 : ((type == "$rd") ? 11 : 16); + SetField(bits, shift, 5, reg); + } + else if (type == "$coprd" || type == "$coprdc" || type == "$coprt") + { + const bool control = (type == "$coprdc"); + if (!ParseCopRegister(operand, cop_n, control, ®)) + { + Error::SetStringFmt(error, "Invalid coprocessor register '{}'.", operand); + return false; + } + SetField(bits, (type == "$coprt") ? 16 : 11, 5, reg); + } + else if (type == "$shamt") + { + u64 value; + if (!ParseUnsignedValue(operand, &value) || value >= 32) + { + Error::SetStringFmt(error, "Shift amount '{}' is outside the range 0..31.", operand); + return false; + } + SetField(bits, 6, 5, static_cast(value)); + } + else if (type == "$imm") + { + s64 value; + if (!ParseSignedValue(operand, &value) || value < std::numeric_limits::min() || + value > std::numeric_limits::max()) + { + Error::SetStringFmt(error, "Immediate '{}' does not fit in a signed 16-bit field.", operand); + return false; + } + SetField(bits, 0, 16, static_cast(value)); + } + else if (type == "$immu" || type == "$immx") + { + u64 value; + if (!ParseUnsignedValue(operand, &value) || value > std::numeric_limits::max()) + { + Error::SetStringFmt(error, "Immediate '{}' does not fit in an unsigned 16-bit field.", operand); + return false; + } + SetField(bits, 0, 16, static_cast(value)); + } + else if (type == "$rel") + { + u64 target_value; + if (!ParseUnsignedValue(operand, &target_value) || target_value > std::numeric_limits::max() || + (target_value & 3) != 0) + { + Error::SetStringFmt(error, "Branch target '{}' is not an aligned 32-bit address.", operand); + return false; + } + + const u32 delta = static_cast(target_value) - (pc + 4); + const s64 word_delta = static_cast(static_cast(delta)) / 4; + if ((delta & 3) != 0 || word_delta < std::numeric_limits::min() || + word_delta > std::numeric_limits::max()) + { + Error::SetStringFmt(error, "Branch target '{}' is out of range.", operand); + return false; + } + SetField(bits, 0, 16, static_cast(word_delta)); + } + else if (type == "$jt") + { + u64 target_value; + if (!ParseUnsignedValue(operand, &target_value) || target_value > std::numeric_limits::max() || + (target_value & 3) != 0) + { + Error::SetStringFmt(error, "Jump target '{}' is not an aligned 32-bit address.", operand); + return false; + } + + const u32 target = static_cast(target_value); + if ((target & 0xF0000000u) != ((pc + 4) & 0xF0000000u)) + { + Error::SetStringFmt(error, "Jump target '{}' is outside the current 256 MB region.", operand); + return false; + } + SetField(bits, 0, 26, target >> 2); + } + else if (type == "$offsetrs") + { + const size_t left_paren = operand.find('('); + const size_t right_paren = operand.rfind(')'); + if (left_paren == std::string_view::npos || right_paren != operand.size() - 1 || left_paren >= right_paren) + { + Error::SetStringFmt(error, "Invalid memory operand '{}'.", operand); + return false; + } + + s64 offset; + if (!ParseSignedValue(operand.substr(0, left_paren), &offset) || offset < std::numeric_limits::min() || + offset > std::numeric_limits::max()) + { + Error::SetStringFmt(error, "Memory offset in '{}' does not fit in a signed 16-bit field.", operand); + return false; + } + if (!ParseGPR(operand.substr(left_paren + 1, right_paren - left_paren - 1), ®)) + { + Error::SetStringFmt(error, "Invalid base register in '{}'.", operand); + return false; + } + SetField(bits, 21, 5, reg); + SetField(bits, 0, 16, static_cast(offset)); + } + else + { + Error::SetStringFmt(error, "Unsupported assembler operand type '{}'.", type); + return false; + } + } + + return true; +} + +CPU::AssembleResult CPU::TryTableEntry(u32* dest, u32 pc, std::string_view text, const char* format, u32 base_bits, + Error* error) +{ + if (!format) // Unknown + return AssembleResult::NoMatch; + + const u32 cop_n = (base_bits >> 26) & 3; + if (!StringUtil::EqualNoCase(GetMnemonic(text), GetFormatMnemonic(format, cop_n))) + return AssembleResult::NoMatch; + + u32 bits = base_bits; + if (!AssembleFormat(&bits, pc, format, text, error)) + return AssembleResult::Error; + + *dest = bits; + return AssembleResult::Success; +} + +CPU::AssembleResult CPU::AssembleGTEInstruction(u32* dest, std::string_view text, Error* error) +{ + const std::string_view mnemonic = GetMnemonic(text); + const GTEInstructionTable* entry = nullptr; + u32 command = 0; + for (u32 i = 0; i < s_gte_instructions.size(); i++) + { + if (s_gte_instructions[i].name && StringUtil::EqualNoCase(mnemonic, s_gte_instructions[i].name)) + { + entry = &s_gte_instructions[i]; + command = i; + break; + } + } + if (!entry) + return AssembleResult::NoMatch; + + u32 bits = (static_cast(InstructionOp::cop2) << 26) | (UINT32_C(1) << 25) | command; + std::string_view options = GetOperands(text); + bool seen_sf = false; + bool seen_lm = false; + bool seen_m = false; + bool seen_v = false; + bool seen_t = false; + while (!options.empty()) + { + const size_t end = options.find_first_of(" \t\r\n"); + const std::string_view option = options.substr(0, end); + options = (end == std::string_view::npos) ? std::string_view() : StringUtil::StripWhitespace(options.substr(end)); + + if (StringUtil::EqualNoCase(option, "sf")) + { + if (!entry->sf || seen_sf) + { + Error::SetStringFmt(error, "Invalid or duplicate GTE option '{}'.", option); + return AssembleResult::Error; + } + seen_sf = true; + bits |= UINT32_C(1) << 19; + } + else if (StringUtil::EqualNoCase(option, "lm")) + { + if (!entry->lm || seen_lm) + { + Error::SetStringFmt(error, "Invalid or duplicate GTE option '{}'.", option); + return AssembleResult::Error; + } + seen_lm = true; + bits |= UINT32_C(1) << 10; + } + else + { + const size_t equals = option.find('='); + if (!entry->mvmva || equals == std::string_view::npos || equals != 1) + { + Error::SetStringFmt(error, "Invalid GTE option '{}'.", option); + return AssembleResult::Error; + } + + u64 value; + if (!ParseUnsignedValue(option.substr(2), &value) || value >= 4) + { + Error::SetStringFmt(error, "GTE option '{}' must have a value from 0 to 3.", option); + return AssembleResult::Error; + } + + const char name = StringUtil::ToLower(option[0]); + bool* seen = (name == 'm') ? &seen_m : ((name == 'v') ? &seen_v : ((name == 't') ? &seen_t : nullptr)); + const u32 shift = (name == 'm') ? 17 : ((name == 'v') ? 15 : 13); + if (!seen || *seen) + { + Error::SetStringFmt(error, "Invalid or duplicate GTE option '{}'.", option); + return AssembleResult::Error; + } + *seen = true; + bits |= static_cast(value) << shift; + } + } + + if (entry->mvmva && (!seen_m || !seen_v || !seen_t)) + { + Error::SetStringView(error, "mvmva requires m=, v=, and t= options."); + return AssembleResult::Error; + } + + *dest = bits; + return AssembleResult::Success; +} + +bool CPU::AssembleInstruction(u32* dest, u32 pc, std::string_view text, Error* error) +{ + if (!dest) + { + Error::SetStringView(error, "Destination pointer is null."); + return false; + } + + text = StringUtil::StripWhitespace(text); + if (text.empty()) + { + Error::SetStringView(error, "Instruction is empty."); + return false; + } + + const std::string_view mnemonic = GetMnemonic(text); + if (StringUtil::EqualNoCase(mnemonic, "nop")) + { + const AssembleResult result = TryTableEntry(dest, pc, text, "nop", 0, error); + return (result == AssembleResult::Success); + } + + for (u32 op = 0; op < s_base_table.size(); op++) + { + if (op == static_cast(InstructionOp::funct) || op == static_cast(InstructionOp::b) || + (op >= static_cast(InstructionOp::cop0) && op <= static_cast(InstructionOp::cop3))) + { + continue; + } + + const AssembleResult result = TryTableEntry(dest, pc, text, s_base_table[op], op << 26, error); + if (result != AssembleResult::NoMatch) + return (result == AssembleResult::Success); + } + + for (u32 funct = 0; funct < s_special_table.size(); funct++) + { + const AssembleResult result = TryTableEntry(dest, pc, text, s_special_table[funct], funct, error); + if (result != AssembleResult::NoMatch) + return (result == AssembleResult::Success); + } + + static constexpr std::array, 4> branch_table = {{ + {0, "bltz $rs, $rel"}, + {1, "bgez $rs, $rel"}, + {16, "bltzal $rs, $rel"}, + {17, "bgezal $rs, $rel"}, + }}; + for (const auto& [rt, format] : branch_table) + { + const u32 base_bits = (static_cast(InstructionOp::b) << 26) | (rt << 16); + const AssembleResult result = TryTableEntry(dest, pc, text, format, base_bits, error); + if (result != AssembleResult::NoMatch) + return (result == AssembleResult::Success); + } + + for (u32 cop_n = 0; cop_n < 4; cop_n++) + { + for (const auto& [common_op, format] : s_cop_common_table) + { + const u32 base_bits = + ((static_cast(InstructionOp::cop0) + cop_n) << 26) | (static_cast(common_op) << 21); + const AssembleResult result = TryTableEntry(dest, pc, text, format, base_bits, error); + if (result != AssembleResult::NoMatch) + return (result == AssembleResult::Success); + } + } + + for (const auto& [cop0_op, format] : s_cop0_table) + { + const u32 base_bits = + (static_cast(InstructionOp::cop0) << 26) | (UINT32_C(1) << 25) | static_cast(cop0_op); + const AssembleResult result = TryTableEntry(dest, pc, text, format, base_bits, error); + if (result != AssembleResult::NoMatch) + return (result == AssembleResult::Success); + } + + const AssembleResult gte_result = AssembleGTEInstruction(dest, text, error); + if (gte_result != AssembleResult::NoMatch) + return (gte_result == AssembleResult::Success); + + if (mnemonic.size() == 4 && StringUtil::StartsWithNoCase(mnemonic, "cop") && mnemonic[3] >= '0' && mnemonic[3] <= '3') + { + u64 immediate; + if (!ParseUnsignedValue(GetOperands(text), &immediate) || immediate > 0x1FFFFFF) + { + Error::SetStringView(error, "Raw coprocessor instruction requires a 25-bit immediate."); + return false; + } + + const u32 cop_n = static_cast(mnemonic[3] - '0'); + *dest = ((static_cast(InstructionOp::cop0) + cop_n) << 26) | (UINT32_C(1) << 25) | static_cast(immediate); + return true; + } + + Error::SetStringFmt(error, "Unknown instruction mnemonic '{}'.", mnemonic); + return false; +} diff --git a/src/core/cpu_disasm.h b/src/core/cpu_disasm.h index c7f88c3bb..20faea011 100644 --- a/src/core/cpu_disasm.h +++ b/src/core/cpu_disasm.h @@ -4,10 +4,14 @@ #pragma once #include "cpu_types.h" +#include + +class Error; class SmallStringBase; namespace CPU { +bool AssembleInstruction(u32* dest, u32 pc, std::string_view text, Error* error = nullptr); void DisassembleInstruction(SmallStringBase* dest, u32 pc, u32 bits); void DisassembleInstructionComment(SmallStringBase* dest, u32 pc, u32 bits);