diff --git a/Source/Core/Core/HW/WII_IPC.cpp b/Source/Core/Core/HW/WII_IPC.cpp index 4ddf3409f2..fd93fad176 100644 --- a/Source/Core/Core/HW/WII_IPC.cpp +++ b/Source/Core/Core/HW/WII_IPC.cpp @@ -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; diff --git a/Source/Core/Core/HW/WII_IPC.h b/Source/Core/Core/HW/WII_IPC.h index d02ef89bd7..a3ce66743e 100644 --- a/Source/Core/Core/HW/WII_IPC.h +++ b/Source/Core/Core/HW/WII_IPC.h @@ -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 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; diff --git a/Source/UnitTests/Core/CMakeLists.txt b/Source/UnitTests/Core/CMakeLists.txt index 833426f78d..54d23e3213 100644 --- a/Source/UnitTests/Core/CMakeLists.txt +++ b/Source/UnitTests/Core/CMakeLists.txt @@ -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) diff --git a/Source/UnitTests/Core/IOS/Starlet/ProbeInterruptTest.cpp b/Source/UnitTests/Core/IOS/Starlet/ProbeInterruptTest.cpp new file mode 100644 index 0000000000..fcdcd278e8 --- /dev/null +++ b/Source/UnitTests/Core/IOS/Starlet/ProbeInterruptTest.cpp @@ -0,0 +1,68 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#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 diff --git a/Source/UnitTests/Core/IOS/Starlet/ProbeScriptTest.cpp b/Source/UnitTests/Core/IOS/Starlet/ProbeScriptTest.cpp new file mode 100644 index 0000000000..b9545590e3 --- /dev/null +++ b/Source/UnitTests/Core/IOS/Starlet/ProbeScriptTest.cpp @@ -0,0 +1,213 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include + +#include + +#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 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{}); +} + +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 words; + std::map registers; + std::vector 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{"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{"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 diff --git a/Tools/StarletBootstrapProbe/ProbeScript.h b/Tools/StarletBootstrapProbe/ProbeScript.h new file mode 100644 index 0000000000..6eec6d1f23 --- /dev/null +++ b/Tools/StarletBootstrapProbe/ProbeScript.h @@ -0,0 +1,273 @@ +// 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 diff --git a/Tools/StarletBootstrapProbe/StarletBootstrapProbe.cpp b/Tools/StarletBootstrapProbe/StarletBootstrapProbe.cpp index 98201fc45e..c8d12007f9 100644 --- a/Tools/StarletBootstrapProbe/StarletBootstrapProbe.cpp +++ b/Tools/StarletBootstrapProbe/StarletBootstrapProbe.cpp @@ -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> 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 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(end) > SCRIPT_MAX_BYTES) + { + std::cerr << "--ipc-script: refused " << path << ": over " << SCRIPT_MAX_BYTES << " bytes\n"; + return std::nullopt; + } + std::string text(static_cast(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* package) + ExpectedOutcome expected, u64 cycles, const std::vector* 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 [--jit] [--mmu-faults] " "[--sbr1-record|--sbr1-absent] " "[--expect-panic|--expect-fatal-data|--expect-undefined-fault|--expect-phase2] " - "[--cycles ] [--stage-spk ]\n"; + "[--cycles ] [--stage-spk ] [--ipc-script ]\n"; return 2; } bool jit = false; @@ -275,10 +342,21 @@ int main(int argc, char** argv) bool outcome_selected = false; std::optional cycles; std::optional package_path; + std::optional 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> package; if (package_path && !(package = ReadPackage(*package_path))) return STAGING_EXIT_STATUS; + std::optional 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