StarletBootstrapProbe: --ipc-script

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]>
This commit is contained in:
Yaya48andClaude Opus 5.5 committed 2026-11-03 00:55:58 +01:00
1 parent 9d66381b3d
commit a709fddfa8
7 files changed
+650 -6

No files matched your search

+2 -1
View File
@@ -141,6 +141,7 @@ void WiiIPC::Reset()
{
INFO_LOG_FMT(WII_IPC, "Resetting ...");
InitState();
m_starlet_attached = false;
}
void WiiIPC::Shutdown()
@@ -478,7 +479,7 @@ void WiiIPC::UpdateInterrupts()
if ((m_ctrl.X1 & m_ctrl.IX1) || (m_ctrl.X2 & m_ctrl.IX2))
{
if (m_system.GetStarlet())
if (m_system.GetStarlet() || m_starlet_attached)
m_arm_irq_flags |= INT_CAUSE_IPC_STARLET;
else
m_ppc_irq_flags |= INT_CAUSE_IPC_STARLET;
+4
View File
@@ -164,6 +164,9 @@ public:
bool IsStarletIRQAsserted() const;
bool IsStarletFIQAsserted() const;
void SetStarletInterrupt(u32 cause, bool asserted);
// Set by a tool that runs an ARMCore with no IOS::LLE::Starlet object in the System, for
// UpdateInterrupts to raise the Starlet's IPC cause all the same. Reset clears it.
void SetStarletAttached(bool attached) { m_starlet_attached = attached; }
Common::Flags<GPIO> GetGPIOOutFlags() const { return m_gpio_out; }
@@ -189,6 +192,7 @@ private:
u32 m_resets = 0;
bool m_broadway_in_reset = false;
bool m_starlet_attached = false;
CoreTiming::EventType* m_event_type_update_interrupts = nullptr;
+4
View File
@@ -28,6 +28,10 @@ target_include_directories(StarletProbeRingTest PRIVATE ${CMAKE_SOURCE_DIR}/Tool
# StarletBootstrapProbe's --stage-spk checks (Tools/StarletBootstrapProbe/ProbeStaging.h).
add_dolphin_test(StarletProbeStagingTest IOS/Starlet/ProbeStagingTest.cpp)
target_include_directories(StarletProbeStagingTest PRIVATE ${CMAKE_SOURCE_DIR}/Tools/StarletBootstrapProbe)
# StarletBootstrapProbe's --ipc-script (Tools/StarletBootstrapProbe/ProbeScript.h) and its flag.
add_dolphin_test(StarletProbeScriptTest IOS/Starlet/ProbeScriptTest.cpp)
target_include_directories(StarletProbeScriptTest PRIVATE ${CMAKE_SOURCE_DIR}/Tools/StarletBootstrapProbe)
add_dolphin_test(StarletProbeInterruptTest IOS/Starlet/ProbeInterruptTest.cpp)
add_dolphin_test(StarletDMATest IOS/Starlet/DMATest.cpp)
add_dolphin_test(StarletSRAMReadTest IOS/Starlet/SRAMReadTest.cpp)
add_dolphin_test(StarletSRAMWriteTest IOS/Starlet/SRAMWriteTest.cpp)
@@ -0,0 +1,68 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <gtest/gtest.h>
#include "Core/Core.h"
#include "Core/HW/WII_IPC.h"
#include "Core/System.h"
namespace IOS
{
namespace
{
constexpr u32 PPCCTRL = 0x04;
constexpr u32 ARMCTRL = 0x0c;
constexpr u32 ARM_IRQMASK = 0x3c;
constexpr u32 IX1 = 0x10;
constexpr u32 X1 = 0x01;
WiiIPC& Fresh(bool attached)
{
Core::DeclareAsCPUThread();
auto& ipc = Core::System::GetInstance().GetWiiIPC();
ipc.Reset();
ipc.SetStarletAttached(attached);
ipc.WriteStarletRegister(ARM_IRQMASK, INT_CAUSE_IPC_STARLET);
return ipc;
}
TEST(StarletProbeInterrupt, WithoutTheFlagX1NeverReachesTheStarlet)
{
// master's behaviour, with no Starlet object in the system.
auto& ipc = Fresh(false);
ipc.WriteStarletRegister(ARMCTRL, IX1);
ipc.WriteStarletRegister(PPCCTRL, X1);
EXPECT_FALSE(ipc.IsStarletIRQAsserted());
}
TEST(StarletProbeInterrupt, WithTheFlagTheModelRaisesItOnX1AndIX1)
{
auto& ipc = Fresh(true);
ipc.WriteStarletRegister(ARMCTRL, IX1);
EXPECT_FALSE(ipc.IsStarletIRQAsserted());
ipc.WriteStarletRegister(PPCCTRL, X1);
EXPECT_TRUE(ipc.IsStarletIRQAsserted());
}
TEST(StarletProbeInterrupt, WithoutIX1TheCauseWaitsForIt)
{
auto& ipc = Fresh(true);
ipc.WriteStarletRegister(PPCCTRL, X1);
EXPECT_FALSE(ipc.IsStarletIRQAsserted());
// The same model line, on the Starlet's first ARMCTRL write.
ipc.WriteStarletRegister(ARMCTRL, IX1);
EXPECT_TRUE(ipc.IsStarletIRQAsserted());
}
TEST(StarletProbeInterrupt, ResetClearsTheFlag)
{
auto& ipc = Fresh(true);
ipc.Reset();
ipc.WriteStarletRegister(ARM_IRQMASK, INT_CAUSE_IPC_STARLET);
ipc.WriteStarletRegister(ARMCTRL, IX1);
ipc.WriteStarletRegister(PPCCTRL, X1);
EXPECT_FALSE(ipc.IsStarletIRQAsserted());
}
} // namespace
} // namespace IOS
@@ -0,0 +1,213 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <map>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include <gtest/gtest.h>
#include "ProbeScript.h"
namespace StarletBootstrapProbe
{
namespace
{
// The script's error, or "" when it parses.
std::string Refusal(std::string_view script)
{
std::string error;
return ParseScript(script, &error) ? "" : error;
}
std::vector<ScriptStep> Steps(std::string_view script)
{
std::string error;
const auto steps = ParseScript(script, &error);
EXPECT_TRUE(steps.has_value()) << error;
return steps.value_or(std::vector<ScriptStep>{});
}
class FakeBus final : public ScriptBus
{
public:
void StoreWord(u32 address, u32 word) override
{
log.push_back("store");
words[address] = word;
}
u32 LoadWord(u32 address) override { return words[address]; }
void WriteRegister(u32 offset, u32 value) override
{
log.push_back(offset == IPC_PPCMSG ? "ppcmsg" : "ppcctrl");
registers[offset] = value;
}
u32 ReadRegister(u32 offset) override { return registers[offset]; }
std::map<u32, u32> words;
std::map<u32, u32> registers;
std::vector<std::string> log;
};
TEST(StarletProbeScript, TheRangeIsTheContracts)
{
EXPECT_EQ(IPC_RANGE_BASE, u32{0x135E0000});
EXPECT_EQ(IPC_RANGE_END, u32{0x13600000});
EXPECT_TRUE(InIPCRange(0x135E0000));
EXPECT_TRUE(InIPCRange(0x135FFFFC));
EXPECT_FALSE(InIPCRange(0x135DFFFC));
EXPECT_FALSE(InIPCRange(0x13600000));
EXPECT_FALSE(InIPCRange(0x135E0002));
EXPECT_FALSE(InIPCRange(0x135FFFFE));
// The package window and the kernel's memory lie above the boundary.
EXPECT_FALSE(InIPCRange(0x13720000));
}
TEST(StarletProbeScript, ParsesEveryStepCommentsAndBlankLines)
{
const auto steps = Steps("# a comment\n"
"\n"
"store 0x135E0000 1 # trailing\n"
" ppcmsg\t0x135E0000\r\n"
"ppcctrl 0x31\n"
"wait 0x02 0x02 4096\n"
"expect-armmsg 0x135E0000\n"
"expect 0x135E0004 0xFFFFFFFA\n"
"run 8192");
ASSERT_EQ(steps.size(), size_t{7});
EXPECT_EQ(steps[0].kind, StepKind::Store);
EXPECT_EQ(steps[0].line, 3);
EXPECT_EQ(steps[0].first, u32{0x135E0000});
EXPECT_EQ(steps[3].kind, StepKind::Wait);
EXPECT_EQ(steps[3].cycles, u64{4096});
EXPECT_EQ(steps[5].second, u32{0xFFFFFFFA});
EXPECT_EQ(steps[6].kind, StepKind::Run);
}
TEST(StarletProbeScript, APpcMsgMayNameAnAddressOutOfTheRange)
{
// A register's value: the negative scenario's, an envelope the kernel must refuse.
EXPECT_EQ(Refusal("ppcmsg 0x00001000\n"), "");
}
TEST(StarletProbeScript, RefusesWhatIsNotTheFormat)
{
EXPECT_EQ(Refusal("nop\n"), "line 1: unknown step: nop");
EXPECT_EQ(Refusal("\n\nstore 1\n"), "line 3: takes an address and a word");
EXPECT_EQ(Refusal("store 0x135E0000 1 2\n"), "line 1: takes an address and a word");
EXPECT_EQ(Refusal("store 0x135E0000 0x100000000\n"), "line 1: takes an address and a word");
EXPECT_EQ(Refusal("store 0x135E0000 x\n"), "line 1: not a number: x");
EXPECT_EQ(Refusal("store 0x 1\n"), "line 1: not a number: 0x");
EXPECT_EQ(Refusal("store -4 1\n"), "line 1: not a number: -4");
EXPECT_EQ(Refusal("ppcmsg\n"), "line 1: takes one word");
EXPECT_EQ(Refusal("ppcctrl 0x40\n"), "line 1: takes PPCCTRL's six bits");
EXPECT_EQ(Refusal("wait 1 1\n"), "line 1: takes a mask, a value and a bound in cycles");
EXPECT_EQ(Refusal("wait 1 1 0\n"), "line 1: takes a mask, a value and a bound in cycles");
EXPECT_EQ(Refusal("run 0\n"), "line 1: takes a number of cycles");
EXPECT_EQ(Refusal("run 1073741825\n"), "line 1: takes a number of cycles");
}
TEST(StarletProbeScript, RefusesAStoreOrAnExpectOutOfTheRange)
{
const std::string_view why = "line 1: the address is not a whole word of the range";
EXPECT_EQ(Refusal("store 0x135DFFFC 1\n"), why);
EXPECT_EQ(Refusal("store 0x13600000 1\n"), why);
EXPECT_EQ(Refusal("store 0x135E0002 1\n"), why);
EXPECT_EQ(Refusal("store 0x13720000 1\n"), why);
EXPECT_EQ(Refusal("expect 0x00200000 1\n"), why);
}
TEST(StarletProbeScript, RefusesTooManySteps)
{
std::string script;
for (size_t index = 0; index < SCRIPT_MAX_STEPS; ++index)
script += "run 4096\n";
EXPECT_EQ(Refusal(script), "");
script += "run 4096\n";
EXPECT_EQ(Refusal(script), "line 257: more than 256 steps");
}
TEST(StarletProbeScript, StepsRunInOrderUpToOneThatWaits)
{
FakeBus bus;
ScriptRunner runner(Steps("store 0x135E0000 7\n"
"ppcmsg 0x135E0000\n"
"ppcctrl 0x31\n"
"wait 0x02 0x02 8192\n"
"ppcctrl 0x32\n"));
runner.Advance(bus, 0);
EXPECT_EQ(bus.log, (std::vector<std::string>{"store", "ppcmsg", "ppcctrl"}));
EXPECT_EQ(bus.words[0x135E0000], u32{7});
EXPECT_EQ(bus.registers[IPC_PPCMSG], u32{0x135E0000});
EXPECT_FALSE(runner.Done());
EXPECT_EQ(runner.Line(), 4);
// Y2 raised by the code under test: the wait ends, the next step runs.
runner.Advance(bus, 4096);
EXPECT_FALSE(runner.Done());
bus.registers[IPC_PPCCTRL] = 0x33;
runner.Advance(bus, 4096);
EXPECT_TRUE(runner.Done());
EXPECT_EQ(bus.registers[IPC_PPCCTRL], u32{0x32});
EXPECT_FALSE(runner.Failed());
}
TEST(StarletProbeScript, AWaitThatRunsOutOfItsBoundFailsItsLine)
{
FakeBus bus;
ScriptRunner runner(Steps("\nwait 0x04 0x04 8192\n"));
runner.Advance(bus, 0);
runner.Advance(bus, 4096);
EXPECT_FALSE(runner.Failed());
bus.registers[IPC_PPCCTRL] = 0x30;
runner.Advance(bus, 4096);
ASSERT_TRUE(runner.Failed());
EXPECT_EQ(runner.Failure(), "line 2: PPCCTRL reads 0x30 after 8192 cycles");
EXPECT_FALSE(runner.Done());
// A failed script runs nothing more.
bus.registers[IPC_PPCCTRL] = 0x34;
runner.Advance(bus, 4096);
EXPECT_FALSE(runner.Done());
}
TEST(StarletProbeScript, AnExpectationNotMetFailsItsLine)
{
FakeBus bus;
ScriptRunner armmsg(Steps("expect-armmsg 0x135E0000\n"));
armmsg.Advance(bus, 0);
EXPECT_EQ(armmsg.Failure(), "line 1: ARMMSG reads 0x0, not 0x135e0000");
ScriptRunner word(Steps("expect 0x135E0004 1\n"));
word.Advance(bus, 0);
EXPECT_EQ(word.Failure(), "line 1: the word at 0x135e0004 reads 0x0, not 0x1");
bus.words[0x135E0004] = 1;
ScriptRunner met(Steps("expect 0x135E0004 1\n"));
met.Advance(bus, 0);
EXPECT_TRUE(met.Done());
EXPECT_FALSE(met.Failed());
}
TEST(StarletProbeScript, RunWaitsItsCycles)
{
FakeBus bus;
ScriptRunner runner(Steps("run 8192\nppcctrl 1\n"));
runner.Advance(bus, 0);
runner.Advance(bus, 4096);
EXPECT_TRUE(bus.log.empty());
runner.Advance(bus, 4096);
EXPECT_EQ(bus.log, (std::vector<std::string>{"ppcctrl"}));
EXPECT_TRUE(runner.Done());
}
TEST(StarletProbeScript, AnEmptyScriptIsDone)
{
FakeBus bus;
ScriptRunner runner(Steps("# nothing\n"));
runner.Advance(bus, 0);
EXPECT_TRUE(runner.Done());
}
} // namespace
} // namespace StarletBootstrapProbe
+273
View File
@@ -0,0 +1,273 @@
// 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
@@ -20,6 +20,7 @@
#include "Core/System.h"
#include "ProbeBudget.h"
#include "ProbeRing.h"
#include "ProbeScript.h"
#include "ProbeStaging.h"
#include "StarOSPanicCheck.h"
@@ -82,8 +83,56 @@ std::optional<std::vector<u8>> ReadPackage(const std::string& path)
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;
};
bool Run(const std::string& path, bool jit, bool mmu_faults, SBR1Mode sbr1,
ExpectedOutcome expected, u64 cycles, const std::vector<u8>* package)
ExpectedOutcome expected, u64 cycles, const std::vector<u8>* package, ScriptRunner* script)
{
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file || file.tellg() <= 0 || file.tellg() > 16 * 1024 * 1024)
@@ -158,9 +207,22 @@ bool Run(const std::string& path, bool jit, bool mmu_faults, SBR1Mode sbr1,
std::cerr << "Starlet panic state " << memory.Read32(info.panic_state) << " code "
<< memory.Read32(info.panic_code) << '\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;
}
}
core.SetIRQLine(system.GetWiiIPC().IsStarletIRQAsserted());
core.SetFIQLine(system.GetWiiIPC().IsStarletFIQAsserted());
core.RunCycles(SLICE_CYCLES);
@@ -213,6 +275,11 @@ bool Run(const std::string& path, bool jit, bool mmu_faults, SBR1Mode sbr1,
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)
@@ -260,12 +327,12 @@ bool Run(const std::string& path, bool jit, bool mmu_faults, SBR1Mode sbr1,
int main(int argc, char** argv)
{
Common::RegisterMsgAlertHandler(FailOnAlert);
if (argc < 2 || argc > 10)
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>]\n";
"[--cycles <n>] [--stage-spk <file>] [--ipc-script <file>]\n";
return 2;
}
bool jit = false;
@@ -275,10 +342,21 @@ int main(int argc, char** argv)
bool outcome_selected = false;
std::optional<u64> 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 == "--stage-spk")
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)
@@ -338,6 +416,9 @@ int main(int argc, char** argv)
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
@@ -358,7 +439,7 @@ int main(int argc, char** argv)
memory.Write_U32(SBR1_MAGIC, SBR1_BASE);
}
const bool passed = Run(argv[1], jit, mmu_faults, sbr1, expected, cycles.value_or(DEFAULT_CYCLES),
package ? &*package : nullptr);
package ? &*package : nullptr, script ? &*script : nullptr);
if (sbr1 != SBR1Mode::None)
{
// The record's summary after the run; with --sbr1-absent every word must still be zero (an