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]>
214 lines
6.9 KiB
C++
214 lines
6.9 KiB
C++
// 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
|