// Copyright 2026 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include #include #include #include #include #include "Common/MsgHandler.h" #include "Core/Core.h" #include "Core/HW/Memmap.h" #include "Core/HW/WII_IPC.h" #include "Core/IOS/Starlet/ARMCore.h" #include "Core/IOS/Starlet/BootstrapELF.h" #include "Core/IOS/Starlet/StarletMemory.h" #include "Core/System.h" namespace { enum class ExpectedOutcome { Idle, Panic, FatalData, UndefinedFault, Phase2 }; // Exit status of a run stopped by a Dolphin alert (1: failed expectation, 2: usage). constexpr int ALERT_EXIT_STATUS = 3; // Dolphin's default handler shows an alert in a modal MessageBox on Windows, which blocks the // probe until someone closes it. Report the alert on stderr and fail the run at once instead. bool FailOnAlert(const char* caption, const char* text, bool, Common::MsgType) { std::cout.flush(); std::cerr << "Dolphin alert (" << caption << "): " << text << std::endl; std::_Exit(ALERT_EXIT_STATUS); } bool Run(const std::string& path, bool jit, bool mmu_faults, ExpectedOutcome expected) { std::ifstream file(path, std::ios::binary | std::ios::ate); if (!file || file.tellg() <= 0 || file.tellg() > 16 * 1024 * 1024) { std::cerr << "Expected a readable Starlet ELF of at most 16 MiB: " << path << '\n'; return false; } const size_t size = static_cast(file.tellg()); std::vector image(size); file.seekg(0); if (!file.read(reinterpret_cast(image.data()), size)) { std::cerr << "Could not read Starlet ELF: " << path << '\n'; return false; } Core::DeclareAsCPUThread(); auto& system = Core::System::GetInstance(); system.GetWiiIPC().Reset(); IOS::LLE::StarletMemory memory(system); memory.PrepareBootstrap(); IOS::LLE::BootstrapELFInfo info; std::string error; if (!IOS::LLE::LoadBootstrapELF(image, memory, &info, &error)) { std::cerr << error << '\n'; return false; } IOS::LLE::ARMCore core(memory); core.SetJitEnabled(jit); core.Reset(info.entry); core.SetBigEndian(true); // StarOS MMU-001: translation, domain, permission and alignment aborts (off by default). core.SetMMUFaultsEnabled(mmu_faults); constexpr u32 TRACE_MAGIC = 0x52455631; constexpr u32 TRACE_CAPACITY = 1024; for (int slice = 0; slice < 512; ++slice) { core.SetIRQLine(system.GetWiiIPC().IsStarletIRQAsserted()); core.SetFIQLine(system.GetWiiIPC().IsStarletFIQAsserted()); core.RunCycles(4096); const u32 fault_code = memory.Read32(info.fault_code); if (fault_code != 0) { std::cerr << "Starlet exception " << fault_code << " LR=" << std::hex << memory.Read32(info.fault_lr) << " SPSR=" << memory.Read32(info.fault_spsr) << " PC=" << core.GetRegister(15) << std::dec << '\n'; return expected == ExpectedOutcome::UndefinedFault && fault_code == 1 && memory.Read32(info.fault_lr) != 0 && memory.Read32(info.fault_spsr) != 0; } if (memory.Read32(info.trace) != TRACE_MAGIC) continue; const u32 length = memory.Read32(info.trace + 4); if (length > TRACE_CAPACITY) { std::cerr << "Invalid REV_TRACE length: " << length << '\n'; return false; } std::string trace(length, '\0'); for (u32 index = 0; index < length; ++index) trace[index] = static_cast(memory.Read8(info.trace + 8 + index)); if (trace.find("REV:BOOT:IDLE\n") == std::string::npos && trace.find("REV:PANIC") == std::string::npos && trace.find("REV:FATAL") == std::string::npos) { continue; } std::cout << trace; std::cout << "PC=0x" << std::hex << core.GetRegister(15) << std::dec << " instructions=" << core.GetExecutedInstructions() << '\n'; const bool early_startup_ok = trace.find("REV:BOOT:ENTRY\n") != std::string::npos && trace.find("REV:BOOT:BSS_OK\n") != std::string::npos; if (expected == ExpectedOutcome::FatalData) { return early_startup_ok && trace.find("REV:FATAL:DATA\n") != std::string::npos && trace.find("REV:BOOT:DATA_OK\n") == std::string::npos; } const bool startup_ok = early_startup_ok && trace.find("REV:BOOT:DATA_OK\n") != std::string::npos && trace.find("REV:BOOT:STACK_OK\n") != std::string::npos && trace.find("REV:BOOT:RUST\n") != std::string::npos; if (expected == ExpectedOutcome::Phase2) { const std::string_view required[] = { "REV:P2:START\n", "ARCH PASS\n", "CRITICAL PASS\n", "TIMER PASS\n", "MONOTONIC PASS\n", "MEM PASS\n", "IRQ PASS\n", "BARRIER PASS\n", "LOGGING PASS\n", "REV:P2:PASS\n", "REV:P2:KERNEL_READY\n", "REV:BOOT:IDLE\n"}; bool phase2_ok = true; for (const std::string_view marker : required) phase2_ok &= trace.find(marker) != std::string::npos; return startup_ok && phase2_ok && trace.find(" FAIL\n") == std::string::npos; } const bool outcome_ok = expected == ExpectedOutcome::Panic ? trace.find("REV:PANIC") != std::string::npos : expected == ExpectedOutcome::Idle && trace.find("REV:BOOT:IDLE\n") != std::string::npos; return startup_ok && outcome_ok; } std::cerr << "No terminal bootstrap marker after 2097152 ARM cycles, PC=0x" << std::hex << core.GetRegister(15) << std::dec << '\n'; return false; } } // namespace int main(int argc, char** argv) { Common::RegisterMsgAlertHandler(FailOnAlert); if (argc < 2 || argc > 5) { std::cerr << "Usage: StarletBootstrapProbe [--jit] [--mmu-faults] " "[--expect-panic|--expect-fatal-data|--expect-undefined-fault|--expect-phase2]\n"; return 2; } bool jit = false; bool mmu_faults = false; ExpectedOutcome expected = ExpectedOutcome::Idle; bool outcome_selected = false; for (int index = 2; index < argc; ++index) { const std::string_view option = argv[index]; if (option == "--jit") jit = true; else if (option == "--mmu-faults") mmu_faults = true; else if (option == "--expect-panic" || option == "--expect-fatal-data" || option == "--expect-undefined-fault" || option == "--expect-phase2") { if (outcome_selected) { std::cerr << "Select only one expected outcome\n"; return 2; } outcome_selected = true; expected = option == "--expect-panic" ? ExpectedOutcome::Panic : option == "--expect-fatal-data" ? ExpectedOutcome::FatalData : option == "--expect-undefined-fault" ? ExpectedOutcome::UndefinedFault : ExpectedOutcome::Phase2; } else { std::cerr << "Unknown option: " << option << '\n'; return 2; } } // The Starlet shares MEM1 and MEM2 with Broadway. Back them as on a retail Wii (24 and 64 MiB, // zeroed): without them, any Starlet access there, such as StarOS reading its SBR1 record // header at 0x00200000, is an access to unmapped memory and raises an alert. auto& system = Core::System::GetInstance(); system.SetIsWii(true); system.GetMemory().Init(); const bool passed = Run(argv[1], jit, mmu_faults, expected); system.GetMemory().Shutdown(); return passed ? 0 : 1; }