diff --git a/README.md b/README.md index 75ec6049fd..75ef81d3d8 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,11 @@ See the [Ghidra Workflow](ps2xAnalyzer/Readme.md#3-ghidra-integration-for-retail Then build generated output and link with `ps2xRuntime`. +The recompiler exits with a nonzero status when its report contains errors, +including unhandled instructions found during C++ generation. Generated files +are retained for investigation, but their presence alone does not mean that +translation succeeded. Warnings alone do not cause a nonzero exit status. + ### Configuration Main fields in `config.toml`: @@ -199,4 +204,4 @@ Other fields (Wikidata item, target platforms, maintainers, links, what help is * Uses ELFIO for ELF parsing * Uses toml11 for TOML parsing * Uses fmt for string formatting -* Reference for runtime PCSX2 \ No newline at end of file +* Reference for runtime PCSX2 diff --git a/ps2xRecomp/include/ps2recomp/ps2_recompiler.h b/ps2xRecomp/include/ps2recomp/ps2_recompiler.h index dd622753fe..276141598b 100644 --- a/ps2xRecomp/include/ps2recomp/ps2_recompiler.h +++ b/ps2xRecomp/include/ps2recomp/ps2_recompiler.h @@ -34,6 +34,7 @@ namespace ps2recomp bool recompile(); void generateOutput(); void printReport() const; + bool hasErrors() const { return m_reporter.hasErrors(); } const RecompilerReporter::Counters &reportCounters() const { return m_reporter.counters(); } static StubTarget resolveStubTarget(const std::string& name); diff --git a/ps2xRecomp/src/runner/main.cpp b/ps2xRecomp/src/runner/main.cpp index 64c77973d8..d1d4c4b782 100644 --- a/ps2xRecomp/src/runner/main.cpp +++ b/ps2xRecomp/src/runner/main.cpp @@ -42,6 +42,12 @@ int main(int argc, char *argv[]) recompiler.generateOutput(); recompiler.printReport(); + if (recompiler.hasErrors()) + { + std::cerr << "Recompilation output contains errors; see report above\n"; + return 1; + } + std::cout << "Recompilation completed successfully\n"; return 0; } @@ -50,4 +56,4 @@ int main(int argc, char *argv[]) std::cerr << "Error: " << e.what() << std::endl; return 1; } -} \ No newline at end of file +} diff --git a/ps2xRuntime/CMakeLists.txt b/ps2xRuntime/CMakeLists.txt index 589d76850a..f45773d070 100644 --- a/ps2xRuntime/CMakeLists.txt +++ b/ps2xRuntime/CMakeLists.txt @@ -353,7 +353,7 @@ else() endif() function(ps2x_stage_ffmpeg_runtime_dlls target_name) - if(NOT WIN32) + if(NOT WIN32 OR NOT PS2X_ENABLE_FFMPEG) return() endif() diff --git a/ps2xTest/src/ps2_recompiler_tests.cpp b/ps2xTest/src/ps2_recompiler_tests.cpp index 201b71e8c3..99d4ca4390 100644 --- a/ps2xTest/src/ps2_recompiler_tests.cpp +++ b/ps2xTest/src/ps2_recompiler_tests.cpp @@ -62,7 +62,8 @@ static Function makeFunction(const std::string &name, uint32_t start, uint32_t e return fn; } -static bool writeMinimalMipsElfWithCodeAndDataFunctionSymbols(const std::filesystem::path &elfPath) +static bool writeMinimalMipsElfWithCodeAndDataFunctionSymbols(const std::filesystem::path &elfPath, + uint32_t firstWord = 0x03E00008u) { ELFIO::elfio writer; writer.create(ELFIO::ELFCLASS32, ELFIO::ELFDATA2LSB); @@ -76,8 +77,8 @@ static bool writeMinimalMipsElfWithCodeAndDataFunctionSymbols(const std::filesys text->set_flags(ELFIO::SHF_ALLOC | ELFIO::SHF_EXECINSTR); text->set_addr_align(4); text->set_address(0x00100000u); - const char textBytes[] = {0x08, 0x00, static_cast(0xE0), 0x03, 0x00, 0x00, 0x00, 0x00}; - text->set_data(textBytes, sizeof(textBytes)); + const std::array textWords{firstWord, 0u}; + text->set_data(reinterpret_cast(textWords.data()), sizeof(textWords)); ELFIO::section *data = writer.sections.add(".data"); data->set_type(ELFIO::SHT_PROGBITS); @@ -807,6 +808,52 @@ void register_ps2_recompiler_tests() { MiniTest::Case("PS2Recompiler", [](TestCase &tc) { + tc.Run("output diagnostics distinguish supported code from unhandled instructions", [](TestCase &t) { + const auto uniqueSuffix = std::to_string( + std::chrono::steady_clock::now().time_since_epoch().count()); + const std::filesystem::path tempRoot = + std::filesystem::temp_directory_path() / ("ps2recomp-output-errors-" + uniqueSuffix); + std::filesystem::create_directories(tempRoot); + + // COP3 is not an R5900 instruction. Decoding it still succeeds, and + // the error is reported only when the C++ throw is emitted. + for (const uint32_t firstWord : {0x03E00008u, 0x4C000000u}) + { + const bool unsupported = firstWord == 0x4C000000u; + const auto elfPath = tempRoot / (unsupported ? "unsupported.elf" : "supported.elf"); + const auto configPath = tempRoot / (unsupported ? "unsupported.toml" : "supported.toml"); + const auto outputPath = tempRoot / (unsupported ? "unsupported" : "supported"); + const bool inputsWritten = + writeMinimalMipsElfWithCodeAndDataFunctionSymbols(elfPath, firstWord) && + writeRecompilerTestConfig(configPath, elfPath, outputPath, {}); + t.IsTrue(inputsWritten, "synthetic output diagnostic inputs should be generated"); + if (!inputsWritten) + continue; + + PS2Recompiler recompiler(configPath.string()); + const bool initialized = recompiler.initialize(); + t.IsTrue(initialized, "synthetic output diagnostic config should initialize"); + if (!initialized) + continue; + const bool recompiled = recompiler.recompile(); + t.IsTrue(recompiled, "both input opcodes should reach code generation"); + if (!recompiled) + continue; + t.IsFalse(recompiler.hasErrors(), "decode alone should not report the codegen error"); + recompiler.generateOutput(); + t.Equals(recompiler.hasErrors(), unsupported, + "codegen errors must remain observable after output generation"); + t.Equals(recompiler.reportCounters().unhandledInstructions, + static_cast(unsupported ? 1u : 0u), + "only the unsupported opcode should count as unhandled"); + t.IsTrue(std::filesystem::exists(outputPath / "register_functions.cpp"), + "diagnostic output should remain available for investigation"); + } + + std::error_code removeError; + std::filesystem::remove_all(tempRoot, removeError); + }); + tc.Run("game helpers are not classified as runtime stubs", [](TestCase &t) { t.IsFalse(ps2_runtime_calls::isStubName("Pad_init"), "Pad_init should be recompiled as game code");