Files
dolphin/Tools/StarletBootstrapProbe/StarletBootstrapProbe.cpp
T
Yaya48andClaude Opus 5.5 2df9dfc909 StarletBootstrapProbe: read --cycles where it stands, as before
--broadway-h1a had moved --cycles' reading after the options, which
changed the usage errors' text for a missing value or a second --cycles.
The bound is now known before the options are read (whether
--broadway-h1a is among them), and --cycles is read in place, with
master's messages: without --broadway-h1a, every run and every refusal
is master's, byte for byte.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-04 21:03:10 +01:00

588 lines
23 KiB
C++

// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#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"
#include "ProbeBroadway.h"
#include "ProbeBudget.h"
#include "ProbeRing.h"
#include "ProbeScript.h"
#include "ProbeStaging.h"
#include "StarOSPanicCheck.h"
namespace
{
using namespace StarletBootstrapProbe;
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;
// Slices run once a StarOS panic path looks finished, to see that it stays so.
constexpr int PANIC_SETTLE_SLICES = 2;
// 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);
}
// Reads the package file for --stage-spk: its bytes, or nullopt once the reason is on stderr (a
// missing or unreadable file; a length StagingRefusal refuses).
std::optional<std::vector<u8>> ReadPackage(const std::string& path)
{
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file)
{
std::cerr << "--stage-spk: cannot open the package file: " << path << '\n';
return std::nullopt;
}
const std::streamoff end = file.tellg();
if (end < 0)
{
std::cerr << "--stage-spk: cannot size the package file: " << path << '\n';
return std::nullopt;
}
if (const auto refusal = StagingRefusal(static_cast<u64>(end)))
{
std::cerr << "--stage-spk: refused " << path << " (" << end << " bytes): " << *refusal << '\n';
return std::nullopt;
}
std::vector<u8> package(static_cast<size_t>(end));
file.seekg(0);
if (!file.read(reinterpret_cast<char*>(package.data()), end))
{
std::cerr << "--stage-spk: cannot read the package file: " << path << '\n';
return std::nullopt;
}
return package;
}
// Reads and parses the file of --ipc-script: its runner, or nullopt once the reason is on stderr.
std::optional<ScriptRunner> ReadScript(const std::string& path)
{
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file)
{
std::cerr << "--ipc-script: cannot open the script: " << path << '\n';
return std::nullopt;
}
const std::streamoff end = file.tellg();
if (end < 0 || static_cast<u64>(end) > SCRIPT_MAX_BYTES)
{
std::cerr << "--ipc-script: refused " << path << ": over " << SCRIPT_MAX_BYTES << " bytes\n";
return std::nullopt;
}
std::string text(static_cast<size_t>(end), '\0');
file.seekg(0);
if (!file.read(text.data(), end))
{
std::cerr << "--ipc-script: cannot read the script: " << path << '\n';
return std::nullopt;
}
std::string error;
auto steps = ParseScript(text, &error);
if (!steps)
{
std::cerr << "--ipc-script: refused " << path << ": " << error << '\n';
return std::nullopt;
}
return ScriptRunner(std::move(*steps));
}
// What a script reaches: PPC memory and the registers, as Broadway does.
class ProbeBus final : public ScriptBus
{
public:
ProbeBus(Memory::MemoryManager& memory, IOS::WiiIPC& ipc) : m_memory(memory), m_ipc(ipc) {}
void StoreWord(u32 address, u32 word) override { m_memory.Write_U32(word, address); }
u32 LoadWord(u32 address) override { return m_memory.Read_U32(address); }
void WriteRegister(u32 offset, u32 value) override { m_ipc.WriteStarletRegister(offset, value); }
u32 ReadRegister(u32 offset) override { return m_ipc.ReadStarletRegister(offset); }
private:
Memory::MemoryManager& m_memory;
IOS::WiiIPC& m_ipc;
};
// What the Broadway client reaches: PPC memory, by whole words (ProbeBroadway.h bounds it to the
// bench window).
class ProbeMemory final : public BroadwayMemory
{
public:
explicit ProbeMemory(Memory::MemoryManager& memory) : m_memory(memory) {}
u32 LoadWord(u32 address) override { return m_memory.Read_U32(address); }
void StoreWord(u32 address, u32 word) override { m_memory.Write_U32(word, address); }
private:
Memory::MemoryManager& m_memory;
};
bool Run(const std::string& path, bool jit, bool mmu_faults, SBR1Mode sbr1,
ExpectedOutcome expected, u64 cycles, const std::vector<u8>* package, ScriptRunner* script,
BroadwayClient* client)
{
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<size_t>(file.tellg());
std::vector<u8> image(size);
file.seekg(0);
if (!file.read(reinterpret_cast<char*>(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;
}
if (package)
{
// The image loaded, then the package at the window's base, its bytes as the file holds them
// (big-endian words, as the Starlet reads them); read back before the first instruction.
auto& mem2 = system.GetMemory();
mem2.CopyToEmu(SPK_WINDOW_BASE, package->data(), package->size());
std::vector<u8> staged(package->size());
mem2.CopyFromEmu(staged.data(), SPK_WINDOW_BASE, staged.size());
if (staged != *package)
{
std::cerr << "--stage-spk: the package window does not read back as staged\n";
return false;
}
std::cerr << "Staged SPK package: " << package->size() << " bytes at 0x" << std::hex
<< SPK_WINDOW_BASE << std::dec << '\n';
}
// --broadway-h1a: StarOS's loader sets the bench window up before the jump; so does the client,
// before the Starlet's first instruction.
ProbeMemory client_memory(system.GetMemory());
if (client)
{
std::cerr << "--broadway-h1a: StarOS loader sources built in:\n" << BroadwayClient::Sources();
if (!client->Start(client_memory) || client->Failed())
{
std::cerr << "--broadway-h1a: " << client->Failure() << '\n';
return false;
}
}
IOS::LLE::ARMCore core(memory);
// Declared after memory, so destroyed first.
memory.AttachInterruptLines(&core);
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;
auto& mem1 = system.GetMemory();
const auto read_mem1 = [&mem1](u32 address) { return mem1.Read_U32(address); };
const auto panic_unmet = [&] {
return UnmetPanicCondition(memory.Read32(info.panic_state), memory.Read32(info.panic_code),
sbr1, read_mem1);
};
// StarOS's panic path writes no trace line. Print the trace published so far, for the
// scenario's ordered checks, and the panic words.
const auto report_panic = [&] {
const u32 length = memory.Read32(info.trace) == TRACE_MAGIC ?
std::min(memory.Read32(info.trace + 4), TRACE_CAPACITY) :
0;
for (u32 index = 0; index < length; ++index)
std::cout << static_cast<char>(memory.Read8(info.trace + 8 + index));
std::cout << "PC=0x" << std::hex << core.GetRegister(15) << std::dec
<< " instructions=" << core.GetExecutedInstructions() << '\n';
std::cerr << "Starlet panic state " << memory.Read32(info.panic_state) << " code "
<< memory.Read32(info.panic_code) << '\n';
};
// The client's capture on stdout, after the trace, and its counts on stderr.
const auto report_client = [&](std::string_view status) {
std::cout << client->Report(status);
std::cerr << "--broadway-h1a: " << client->Steps() << " steps, timebase 0x" << std::hex
<< client->Timebase() << std::dec << ", done " << client->Done() << '\n';
};
ProbeBus script_bus(mem1, system.GetWiiIPC());
// The model's own line raises the IPC cause: the probe has no Starlet object (ProbeScript.h).
if (script)
system.GetWiiIPC().SetStarletAttached(true);
int panic_settled = 0;
for (u64 slice = 0; slice < cycles / SLICE_CYCLES; ++slice)
{
if (script)
{
script->Advance(script_bus, slice == 0 ? 0 : SLICE_CYCLES);
if (script->Failed())
{
std::cerr << "--ipc-script: " << script->Failure() << '\n';
return false;
}
}
if (client)
{
// As the loader's monitor: one step of the bench a turn, the first at the jump.
client->Advance(slice == 0 ? 0 : SLICE_CYCLES);
if (client->Failed())
{
std::cerr << "--broadway-h1a: " << client->Failure() << '\n';
return false;
}
}
core.SetIRQLine(system.GetWiiIPC().IsStarletIRQAsserted());
core.SetFIQLine(system.GetWiiIPC().IsStarletFIQAsserted());
core.RunCycles(SLICE_CYCLES);
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;
}
// StarOS's panic path sets its guard word first, then its code, then publishes the SBR1
// record when there is one, and halts. An expected panic passes once all of that holds
// (UnmetPanicCondition) and still holds PANIC_SETTLE_SLICES slices later.
if (info.panic_state != 0 && memory.Read32(info.panic_state) != 0)
{
if (expected != ExpectedOutcome::Panic)
{
report_panic();
return false;
}
if (!panic_unmet().empty())
{
panic_settled = 0;
continue;
}
if (++panic_settled <= PANIC_SETTLE_SLICES)
continue;
report_panic();
return true;
}
if (client)
{
// --broadway-h1a: the loader's monitor reports once the SBR1 record says the Starlet is done
// (StarletDone) and its bench has ended; the trace, possibly full, is printed as it stands.
const u32 length = memory.Read32(info.trace) == TRACE_MAGIC ?
std::min(memory.Read32(info.trace + 4), TRACE_CAPACITY) :
0;
std::string trace(length, '\0');
for (u32 index = 0; index < length; ++index)
trace[index] = static_cast<char>(memory.Read8(info.trace + 8 + index));
const bool trace_terminal = trace.find("REV:BOOT:IDLE\n") != std::string::npos ||
trace.find("REV:PANIC") != std::string::npos ||
trace.find("REV:FATAL") != std::string::npos;
const u32 state = mem1.Read_U32(SBR1_BASE + SBR1_STARLET_STATE);
if (!StarletDone(trace_terminal, state, mem1.Read_U32(SBR1_BASE + SBR1_EXI_OWNER)) ||
!client->Done())
{
continue;
}
std::cout << trace;
std::cout << "PC=0x" << std::hex << core.GetRegister(15) << std::dec
<< " instructions=" << core.GetExecutedInstructions() << '\n';
if (length == TRACE_CAPACITY)
std::cerr << "--broadway-h1a: the trace is full: its last lines are in the SBR1 ring\n";
report_client(CLIENT_STATUS_DONE);
const bool startup_ok = trace.find("REV:BOOT:ENTRY\n") != std::string::npos &&
trace.find("REV:BOOT:BSS_OK\n") != std::string::npos &&
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;
const bool idle =
state == SBR1_STATE_IDLE || trace.find("REV:BOOT:IDLE\n") != std::string::npos;
return startup_ok && idle && trace.find("REV:PANIC") == std::string::npos &&
trace.find("REV:FATAL") == std::string::npos;
}
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<char>(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';
if (script && !script->Done())
{
std::cerr << "--ipc-script: script unfinished at line " << script->Line() << '\n';
return false;
}
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;
}
if (info.panic_state != 0 && memory.Read32(info.panic_state) != 0)
{
report_panic();
const std::string unmet = panic_unmet();
std::cerr << "StarOS panic path unfinished after " << cycles
<< " ARM cycles: " << (unmet.empty() ? "it ended inside the settle slices" : unmet)
<< '\n';
return false;
}
std::cerr << "No terminal bootstrap marker after " << cycles << " ARM cycles, PC=0x" << std::hex
<< core.GetRegister(15) << std::dec << '\n';
// As main.c's monitor at its own timeout: the report anyway, under SBL:TIMEOUT.
if (client)
report_client(CLIENT_STATUS_TIMEOUT);
return false;
}
} // namespace
int main(int argc, char** argv)
{
Common::RegisterMsgAlertHandler(FailOnAlert);
if (argc < 2 || argc > 12)
{
std::cerr << "Usage: StarletBootstrapProbe <ELF> [--jit] [--mmu-faults] "
"[--sbr1-record|--sbr1-absent] "
"[--expect-panic|--expect-fatal-data|--expect-undefined-fault|--expect-phase2] "
"[--cycles <n>] [--stage-spk <file>] [--ipc-script <file>|--broadway-h1a]\n";
return 2;
}
bool jit = false;
bool mmu_faults = false;
SBR1Mode sbr1 = SBR1Mode::None;
ExpectedOutcome expected = ExpectedOutcome::Idle;
bool outcome_selected = false;
std::optional<u64> cycles;
bool broadway_h1a = false;
// --cycles' bound: MAX_CYCLES, or MAX_CLIENT_CYCLES when --broadway-h1a is among the options
// (ProbeBudget.h), known before they are read so that --cycles is read where it stands.
const auto is_client = [](const char* argument) {
return std::string_view(argument) == "--broadway-h1a";
};
const u64 max_cycles =
std::any_of(argv + 2, argv + argc, is_client) ? MAX_CLIENT_CYCLES : MAX_CYCLES;
std::optional<std::string> package_path;
std::optional<std::string> script_path;
for (int index = 2; index < argc; ++index)
{
const std::string_view option = argv[index];
if (option == "--ipc-script")
{
// One script, given once (ProbeScript.h).
if (script_path || index + 1 == argc)
{
std::cerr << "--ipc-script takes one file, once\n";
return 2;
}
script_path = argv[++index];
}
else if (option == "--stage-spk")
{
// One package file, given once (ProbeStaging.h).
if (package_path || index + 1 == argc)
{
std::cerr << "--stage-spk takes one package file, once\n";
return 2;
}
package_path = argv[++index];
}
else if (option == "--broadway-h1a")
{
// StarOS's H-1A bench loader as Broadway (ProbeBroadway.h), once.
if (broadway_h1a)
{
std::cerr << "--broadway-h1a is given once\n";
return 2;
}
broadway_h1a = true;
}
else if (option == "--cycles")
{
// A whole number of slices, at most max_cycles (ProbeBudget.h); given once.
if (cycles || index + 1 == argc || !(cycles = ParseCycles(argv[index + 1], max_cycles)))
{
std::cerr << "--cycles takes one decimal multiple of " << SLICE_CYCLES << ", at most "
<< max_cycles << ", once\n";
return 2;
}
++index;
}
else if (option == "--jit")
jit = true;
else if (option == "--mmu-faults")
mmu_faults = true;
else if (option == "--sbr1-record" || option == "--sbr1-absent")
{
if (sbr1 != SBR1Mode::None)
{
std::cerr << "Select only one SBR1 mode\n";
return 2;
}
sbr1 = option == "--sbr1-record" ? SBR1Mode::Record : SBR1Mode::Absent;
}
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 client drives Broadway's side alone: no script, no staged package with it; and it reads
// the SBR1 record, as the loader's monitor does, so the record must be there.
if (broadway_h1a && (script_path || package_path))
{
std::cerr << "--broadway-h1a takes neither --ipc-script nor --stage-spk\n";
return 2;
}
if (broadway_h1a && sbr1 != SBR1Mode::Record)
{
std::cerr << "--broadway-h1a takes --sbr1-record: the loader's monitor reads the record\n";
return 2;
}
// The package file read and checked before anything is set up: a refused file ends the run
// before StarOS starts.
std::optional<std::vector<u8>> package;
if (package_path && !(package = ReadPackage(*package_path)))
return STAGING_EXIT_STATUS;
std::optional<ScriptRunner> script;
if (script_path && !(script = ReadScript(*script_path)))
return SCRIPT_EXIT_STATUS;
// 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();
auto& memory = system.GetMemory();
if (sbr1 == SBR1Mode::Record)
{
// As the Broadway loader and the ARM relay leave StarOS's SBR1 boot record before the image
// runs (StarOS docs/contracts/wii-dev-boot.md): header, the relay at its last step, EXI
// channel 1 given to the Starlet, the magic written last.
memory.Write_U32(SBR1_VERSION, SBR1_BASE + 0x04);
memory.Write_U32(SBR1_SIZE, SBR1_BASE + 0x08);
memory.Write_U32(SBR1_EXI_STARLET, SBR1_BASE + SBR1_EXI_OWNER);
memory.Write_U32(SBR1_RELAY_JUMPING, SBR1_BASE + SBR1_RELAY_STATUS);
memory.Write_U32(SBR1_MAGIC, SBR1_BASE);
}
BroadwayClient client;
const bool passed = Run(argv[1], jit, mmu_faults, sbr1, expected, cycles.value_or(DEFAULT_CYCLES),
package ? &*package : nullptr, script ? &*script : nullptr,
broadway_h1a ? &client : nullptr);
if (sbr1 != SBR1Mode::None)
{
// The record's summary after the run; with --sbr1-absent every word must still be zero (an
// expected panic fails otherwise).
std::cerr << std::hex << "SBR1 magic 0x" << memory.Read_U32(SBR1_BASE) << std::dec
<< " state " << memory.Read_U32(SBR1_BASE + SBR1_STARLET_STATE) << " reason "
<< memory.Read_U32(SBR1_BASE + SBR1_FATAL_REASON) << " owner "
<< memory.Read_U32(SBR1_BASE + SBR1_EXI_OWNER) << " fault "
<< memory.Read_U32(SBR1_BASE + SBR1_FAULT_CODE) << " ring "
<< memory.Read_U32(SBR1_BASE + SBR1_RING_LENGTH) << " dropped "
<< memory.Read_U32(SBR1_BASE + SBR1_RING_DROPPED);
u32 written = 0;
for (u32 offset = 0; offset < SBR1_SIZE; offset += 4)
written += memory.Read_U32(SBR1_BASE + offset) != 0;
std::cerr << " nonzero_words " << written << '\n';
if (sbr1 == SBR1Mode::Record)
{
// The ring's text, as StarOS's Broadway loader prints it over the Gecko on the console;
// END starts its own line even when the text does not end with one.
const std::string ring =
RingText([&memory](u32 offset) { return memory.Read_U8(SBR1_BASE + offset); },
memory.Read_U32(SBR1_BASE + SBR1_RING_LENGTH));
std::cerr << "SBR1 RING BEGIN\n"
<< ring << (ring.empty() || ring.back() == '\n' ? "" : "\n") << "SBR1 RING END\n";
}
}
system.GetMemory().Shutdown();
return passed ? 0 : 1;
}