// Copyright 2026 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include #include #include #include #include #include #include #include #include "Common/CommonTypes.h" // StarletBootstrapProbe's --ipc-script : the probe stands in for Broadway (StarOS // docs/contracts/hollywood-ipc-injection.md). A script is data: whole words stored in one range // of PPC memory, the registers Broadway writes and reads, every wait bounded. It knows nothing of // the code under test and answers nothing for it: what it reads back, the ARM code wrote. namespace StarletBootstrapProbe { // The one range a script may store to or expect from: the 128 KiB below Broadway's boundary. constexpr u32 IPC_RANGE_BASE = 0x135E0000; // Exclusive. constexpr u32 IPC_RANGE_END = 0x13600000; constexpr u64 SCRIPT_MAX_BYTES = 64 * 1024; constexpr size_t SCRIPT_MAX_STEPS = 256; constexpr u64 SCRIPT_MAX_CYCLES = u64{1} << 30; // Exit status of a run whose script is refused before StarOS starts (1: failed expectation, // 2: usage, 3: Dolphin alert, 4: refused package file). constexpr int SCRIPT_EXIT_STATUS = 5; // The registers, as offsets from Hollywood's register page. constexpr u32 IPC_PPCMSG = 0x00; constexpr u32 IPC_PPCCTRL = 0x04; constexpr u32 IPC_ARMMSG = 0x08; // PPCCTRL's bits a write means: X1, Y2, Y1, X2, IY1, IY2. constexpr u32 IPC_PPCCTRL_BITS = 0x3F; enum class StepKind { Store, PpcMsg, PpcCtrl, Wait, ExpectArmMsg, Expect, Run }; struct ScriptStep { StepKind kind; int line; u32 first; u32 second; u64 cycles; }; // What a script reaches: PPC memory and the registers, nothing of the code under test. class ScriptBus { public: virtual ~ScriptBus() = default; virtual void StoreWord(u32 address, u32 word) = 0; virtual u32 LoadWord(u32 address) = 0; virtual void WriteRegister(u32 offset, u32 value) = 0; virtual u32 ReadRegister(u32 offset) = 0; }; // A number, hexadecimal with 0x or decimal, whole: nothing else. inline std::optional ParseScriptNumber(std::string_view text) { const bool hexadecimal = text.starts_with("0x"); if (hexadecimal) text.remove_prefix(2); if (text.empty()) return std::nullopt; u64 value = 0; const auto [end, status] = std::from_chars(text.data(), text.data() + text.size(), value, hexadecimal ? 16 : 10); if (status != std::errc{} || end != text.data() + text.size()) return std::nullopt; return value; } // Whether a whole word at `address` lies in the range. constexpr bool InIPCRange(u64 address) { return address >= IPC_RANGE_BASE && address + 4 <= IPC_RANGE_END && address % 4 == 0; } // The script's steps, or nullopt with the reason in `error`. Nothing is run. inline std::optional> ParseScript(std::string_view text, std::string* error) { const auto refuse = [error](int line, std::string_view why) { *error = fmt::format("line {}: {}", line, why); return std::nullopt; }; std::vector steps; int line_number = 0; while (!text.empty()) { ++line_number; const size_t end = text.find('\n'); std::string_view line = text.substr(0, end); text = end == std::string_view::npos ? std::string_view{} : text.substr(end + 1); line = line.substr(0, line.find('#')); std::vector words; while (!line.empty()) { const size_t start = line.find_first_not_of(" \t\r"); if (start == std::string_view::npos) break; line.remove_prefix(start); const size_t stop = line.find_first_of(" \t\r"); words.push_back(line.substr(0, stop)); line = stop == std::string_view::npos ? std::string_view{} : line.substr(stop); } if (words.empty()) continue; if (steps.size() == SCRIPT_MAX_STEPS) return refuse(line_number, fmt::format("more than {} steps", SCRIPT_MAX_STEPS)); std::vector numbers; for (size_t index = 1; index < words.size(); ++index) { const std::optional number = ParseScriptNumber(words[index]); if (!number) return refuse(line_number, fmt::format("not a number: {}", words[index])); numbers.push_back(*number); } const auto operands = [&numbers](size_t count) { return numbers.size() == count; }; const auto word = [&numbers](size_t index) { return numbers[index] <= 0xFFFFFFFF; }; const auto cycles = [&numbers](size_t index) { return numbers[index] != 0 && numbers[index] <= SCRIPT_MAX_CYCLES; }; const std::string_view name = words[0]; ScriptStep step{StepKind::Run, line_number, 0, 0, 0}; if (name == "store" || name == "expect") { if (!operands(2) || !word(1)) return refuse(line_number, "takes an address and a word"); if (!InIPCRange(numbers[0])) return refuse(line_number, "the address is not a whole word of the range"); step.kind = name == "store" ? StepKind::Store : StepKind::Expect; step.first = static_cast(numbers[0]); step.second = static_cast(numbers[1]); } else if (name == "ppcmsg" || name == "expect-armmsg") { // A register's value, in or out of the range: nothing is accessed through it here. if (!operands(1) || !word(0)) return refuse(line_number, "takes one word"); step.kind = name == "ppcmsg" ? StepKind::PpcMsg : StepKind::ExpectArmMsg; step.first = static_cast(numbers[0]); } else if (name == "ppcctrl") { if (!operands(1) || numbers[0] > IPC_PPCCTRL_BITS) return refuse(line_number, "takes PPCCTRL's six bits"); step.kind = StepKind::PpcCtrl; step.first = static_cast(numbers[0]); } else if (name == "wait") { if (!operands(3) || !word(0) || !word(1) || !cycles(2)) return refuse(line_number, "takes a mask, a value and a bound in cycles"); step.kind = StepKind::Wait; step.first = static_cast(numbers[0]); step.second = static_cast(numbers[1]); step.cycles = numbers[2]; } else if (name == "run") { if (!operands(1) || !cycles(0)) return refuse(line_number, "takes a number of cycles"); step.kind = StepKind::Run; step.cycles = numbers[0]; } else { return refuse(line_number, fmt::format("unknown step: {}", name)); } steps.push_back(step); } return steps; } // Runs a script's steps in order, between the probe's slices. class ScriptRunner { public: explicit ScriptRunner(std::vector steps) : m_steps(std::move(steps)) {} // `elapsed` ARM cycles went by: every step that can run now does, up to one that must wait. void Advance(ScriptBus& bus, u64 elapsed) { m_waited += elapsed; while (m_failure.empty() && m_next < m_steps.size()) { const ScriptStep& step = m_steps[m_next]; switch (step.kind) { case StepKind::Store: bus.StoreWord(step.first, step.second); break; case StepKind::PpcMsg: bus.WriteRegister(IPC_PPCMSG, step.first); break; case StepKind::PpcCtrl: bus.WriteRegister(IPC_PPCCTRL, step.first); break; case StepKind::Wait: { const u32 seen = bus.ReadRegister(IPC_PPCCTRL); if ((seen & step.first) != step.second) { if (m_waited >= step.cycles) Fail(step, fmt::format("PPCCTRL reads {:#x} after {} cycles", seen, m_waited)); return; } break; } case StepKind::ExpectArmMsg: { const u32 seen = bus.ReadRegister(IPC_ARMMSG); if (seen != step.first) return Fail(step, fmt::format("ARMMSG reads {:#x}, not {:#x}", seen, step.first)); break; } case StepKind::Expect: { const u32 seen = bus.LoadWord(step.first); if (seen != step.second) { return Fail(step, fmt::format("the word at {:#x} reads {:#x}, not {:#x}", step.first, seen, step.second)); } break; } case StepKind::Run: if (m_waited < step.cycles) return; break; } ++m_next; m_waited = 0; } } bool Done() const { return m_next == m_steps.size(); } bool Failed() const { return !m_failure.empty(); } const std::string& Failure() const { return m_failure; } // The line of the step it stands on. int Line() const { return Done() ? 0 : m_steps[m_next].line; } private: void Fail(const ScriptStep& step, const std::string& why) { m_failure = fmt::format("line {}: {}", step.line, why); } std::vector m_steps; size_t m_next = 0; u64 m_waited = 0; std::string m_failure; }; } // namespace StarletBootstrapProbe