The probe stands in for Broadway, so that a real request can enter StarOS's image through the Hollywood IPC registers and its reply be read back (StarOS docs/contracts/hollywood-ipc-injection.md, approved for development). A script is data: whole words stored in one range of PPC memory, 0x135E0000..0x13600000, PPCMSG and PPCCTRL written, PPCCTRL and ARMMSG read, every wait bounded in ARM cycles. Steps run between the probe's slices. The probe knows nothing of the code under test and answers nothing for it. A script refused when read ends the run with exit status 5, before StarOS starts. A step that fails, or a terminal trace line with steps left, fails the run with its line on stderr. The option given twice, or without its file, is a usage error. The one change in Source/Core: WiiIPC::SetStarletAttached, an opt-in flag UpdateInterrupts reads beside GetStarlet(), because the probe runs an ARMCore with no Starlet object in the System. The IPC cause is then raised by the model's own line, X1 and IX1 included, with no second model of it. Reset clears the flag, and nothing sets it without --ipc-script. ProbeScript.h holds the parser and the runner, tested by StarletProbeScriptTest over a fake bus; StarletProbeInterruptTest holds the flag's behaviour with and without it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
274 lines
8.6 KiB
C++
274 lines
8.6 KiB
C++
// Copyright 2026 Dolphin Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#pragma once
|
|
|
|
#include <charconv>
|
|
#include <cstddef>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include <fmt/format.h>
|
|
|
|
#include "Common/CommonTypes.h"
|
|
|
|
// StarletBootstrapProbe's --ipc-script <file>: 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<u64> 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<std::vector<ScriptStep>> 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<ScriptStep> 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<std::string_view> 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<u64> numbers;
|
|
for (size_t index = 1; index < words.size(); ++index)
|
|
{
|
|
const std::optional<u64> 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<u32>(numbers[0]);
|
|
step.second = static_cast<u32>(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<u32>(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<u32>(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<u32>(numbers[0]);
|
|
step.second = static_cast<u32>(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<ScriptStep> 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<ScriptStep> m_steps;
|
|
size_t m_next = 0;
|
|
u64 m_waited = 0;
|
|
std::string m_failure;
|
|
};
|
|
} // namespace StarletBootstrapProbe
|