Files
dolphin/Source/UnitTests/Core/IOS/Starlet/ProbeBroadwayTest.cpp
T
Yaya48andClaude Opus 5.5 9ad22f00bd StarletBootstrapProbe: --broadway-h1a's report and budget, from the final review
Two findings of the review of 2df9dfc909 (StarOS H-1A, D2):
- The report reads the bench window too (hw0c_report(): HW-M4-0D's
  area, and H-1A's record if it was never read), but a refused access
  there did not fail the run, as one during the run does. It does now:
  the failure on stderr, and the run fails.
- --cycles' bound of 2^34 was raised whenever "--broadway-h1a" appeared
  among the arguments, even as the value of --ipc-script or --stage-spk
  (a file of that name), the run then not in the client's mode.
  ClientRequested reads the options with their grammar: the word after
  --ipc-script, --stage-spk or --cycles is a value, never an option.

StarletProbeBroadwayTest: ClientRequested.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-04 21:59:34 +01:00

326 lines
12 KiB
C++

// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cstdio>
#include <cstdlib>
#include <string>
#include <string_view>
#include <vector>
#include <gtest/gtest.h>
#include "ProbeBroadway.h"
#include "ProbeBudget.h"
#include "staros_h1a.h"
#include "staros_hw0c.h"
namespace StarletBootstrapProbe
{
namespace
{
// StarOS boot/wii/loader/board.h's timebase ticks a millisecond.
constexpr u64 TICKS_PER_MS = 60750;
// PPC memory as the client sees it: the bench window's words, and a count of what fell outside.
class FakeMemory final : public BroadwayMemory
{
public:
u32 LoadWord(u32 address) override
{
if (InBenchWindow(address))
return words[(address - BENCH_WINDOW_BASE) / 4];
++outside;
return 0;
}
void StoreWord(u32 address, u32 word) override
{
if (InBenchWindow(address))
words[(address - BENCH_WINDOW_BASE) / 4] = word;
else
++outside;
}
u32& At(u32 offset) { return words[offset / 4]; }
std::vector<u32> words = std::vector<u32>((BENCH_WINDOW_END - BENCH_WINDOW_BASE) / 4);
int outside = 0;
};
TEST(StarletProbeBroadway, TheWindowIsTheContractsAndNothingElseIsReached)
{
EXPECT_EQ(BENCH_WINDOW_BASE, u32{STAROS_HW0C_BASE});
EXPECT_EQ(BENCH_WINDOW_END, u32{STAROS_HW0C_BASE + STAROS_HW0C_SIZE});
FakeMemory memory;
BroadwayHal hal(memory, 100);
hal.Write(BENCH_WINDOW_BASE, 7);
hal.Write(BENCH_WINDOW_END - 4, 9);
EXPECT_EQ(hal.Read(BENCH_WINDOW_BASE), u32{7});
EXPECT_EQ(hal.Read(BENCH_WINDOW_END - 4), u32{9});
hal.CacheLine(BENCH_WINDOW_BASE + 0x20);
EXPECT_FALSE(hal.Failed());
EXPECT_EQ(hal.Accesses(), u64{5});
// The first access outside fails the client, and is never made.
EXPECT_EQ(hal.Read(BENCH_WINDOW_END), u32{0});
EXPECT_TRUE(hal.Failed());
EXPECT_EQ(hal.Failure(), "a read at 0x13510000, outside the bench window 0x13500000..0x13510000");
hal.Write(BENCH_WINDOW_BASE - 4, 1);
hal.Write(BENCH_WINDOW_BASE + 2, 1);
EXPECT_EQ(memory.outside, 0);
EXPECT_EQ(hal.Failure(), "a read at 0x13510000, outside the bench window 0x13500000..0x13510000");
EXPECT_FALSE(InBenchWindow(BENCH_WINDOW_BASE + 2));
EXPECT_FALSE(InBenchWindow(0x135E0000));
EXPECT_FALSE(InBenchWindow(u64{BENCH_WINDOW_BASE} + 0x100000000));
}
TEST(StarletProbeBroadway, TheTimebaseIsAQuarterOfTheStarletsCycles)
{
FakeMemory memory;
BroadwayHal hal(memory, CLIENT_TIMEBASE_BASE);
hal.Elapse(0);
EXPECT_EQ(hal.Timebase(), CLIENT_TIMEBASE_BASE);
hal.Elapse(SLICE_CYCLES);
EXPECT_EQ(hal.Timebase(), CLIENT_TIMEBASE_BASE + 1024);
// 2 ms of Broadway's timebase (StarOS's TICKS_PER_MS, 60750) is 486,000 Starlet cycles.
hal.Elapse(486000);
EXPECT_EQ(hal.Timebase() - CLIENT_TIMEBASE_BASE - 1024, u64{121500});
}
TEST(StarletProbeBroadway, TheCaptureIsGeckosFormats)
{
FakeMemory memory;
BroadwayHal hal(memory, 0);
hal.Put("SBL:X ");
hal.Hex(0x1A);
hal.Put(" ");
hal.Dec(0);
hal.Put(" ");
hal.Dec(0xFFFFFFFF);
hal.Put(" ");
hal.Hex(0xDEADBEEF);
EXPECT_EQ(hal.Capture(), "SBL:X 0x0000001a 0 4294967295 0xdeadbeef");
}
TEST(StarletProbeBroadway, TheClientsBudgetIsBoundedAndOnlyItsOwn)
{
EXPECT_EQ(MAX_CLIENT_CYCLES, u64{1} << 34);
EXPECT_EQ(ParseCycles("17179869184", MAX_CLIENT_CYCLES), MAX_CLIENT_CYCLES);
EXPECT_FALSE(ParseCycles("17179873280", MAX_CLIENT_CYCLES));
EXPECT_FALSE(ParseCycles("17179869185", MAX_CLIENT_CYCLES));
// Without the client, 2^30 as before.
EXPECT_EQ(ParseCycles("1073741824"), MAX_CYCLES);
EXPECT_FALSE(ParseCycles("1073745920"));
EXPECT_FALSE(ParseCycles("17179869184"));
}
TEST(StarletProbeBroadway, OnlyTheOptionItselfRaisesTheBudget)
{
const auto requested = [](std::vector<const char*> argv) {
return ClientRequested(static_cast<int>(argv.size()), argv.data());
};
EXPECT_TRUE(requested({"probe", "image.elf", "--broadway-h1a"}));
EXPECT_TRUE(requested({"probe", "image.elf", "--jit", "--cycles", "4096", "--broadway-h1a"}));
EXPECT_TRUE(requested({"probe", "image.elf", "--stage-spk", "p.bin", "--broadway-h1a"}));
EXPECT_FALSE(requested({"probe", "image.elf"}));
// The image's own name, or another option's value, is no option.
EXPECT_FALSE(requested({"probe", "--broadway-h1a"}));
EXPECT_FALSE(requested({"probe", "image.elf", "--ipc-script", "--broadway-h1a"}));
EXPECT_FALSE(requested({"probe", "image.elf", "--stage-spk", "--broadway-h1a", "--jit"}));
EXPECT_FALSE(requested({"probe", "image.elf", "--cycles", "--broadway-h1a"}));
}
TEST(StarletProbeBroadway, TheStarletIsDoneAsTheLoadersMonitorSaysIt)
{
// StarOS boot/contract/staros_boot.h: RUNNING 2, IDLE 3, FATAL 4, PANIC 5, FAULT 6; EXI PPC 1.
EXPECT_EQ(SBR1_STATE_IDLE, u32{3});
EXPECT_TRUE(StarletDone(false, 3, SBR1_EXI_PPC));
EXPECT_TRUE(StarletDone(false, 6, SBR1_EXI_PPC));
EXPECT_FALSE(StarletDone(false, 2, SBR1_EXI_PPC));
EXPECT_FALSE(StarletDone(false, 3, SBR1_EXI_STARLET));
EXPECT_FALSE(StarletDone(false, 0, 0));
// A terminal trace line counts on its own.
EXPECT_TRUE(StarletDone(true, 0, 0));
}
TEST(StarletProbeBroadway, TheBuiltInSourcesAreTheNineFilesWithTheirHashes)
{
const std::string_view sources = BroadwayClient::Sources();
const char* names[] = {"hw0c.c", "hw0c.h", "hw0d.c", "hw0d.h", "h1a.c",
"h1a.h", "staros_hw0c.h", "staros_hw0d.h", "staros_h1a.h"};
size_t at = 0;
for (const char* name : names)
{
const size_t end = sources.find('\n', at);
ASSERT_NE(end, std::string_view::npos);
const std::string_view line = sources.substr(at, end - at);
EXPECT_TRUE(line.starts_with(std::string(name) + " ")) << line;
EXPECT_EQ(line.size(), std::string_view(name).size() + 1 + 64) << line;
at = end + 1;
}
EXPECT_EQ(at, sources.size());
}
// A scripted Starlet over the fake window, as StarOS's tests/hardware-wii/h1a benches script it:
// READY 100 ms after the first step (unless `ready` is false), then each round answered at least
// 40,960 timebase ticks after it was first seen posted, S2B's words first for a Starlet -> Broadway
// round; after the last round or Broadway's END, its record (GEN 2) and its END (unless `ends` is
// false).
struct Starlet
{
bool ready = true;
bool ends = true;
bool answered_ready = false;
bool ended = false;
u32 answered = 0;
bool seen = false;
u64 seen_at = 0;
static u32 Scenario(u32 round) { return round <= STAROS_H1A_S1_ROUNDS ? STAROS_H1A_S1 : STAROS_H1A_S3; }
void Tick(FakeMemory& memory, u64 now, u64 start)
{
const u32 broadway = STAROS_HW0C_BROADWAY, starlet = STAROS_HW0C_STARLET;
if (!answered_ready)
{
if (ready && now >= start + 100 * TICKS_PER_MS)
{
answered_ready = true;
memory.At(STAROS_HW0C_ENVIRONMENT + STAROS_HW0C_E_CONTROL) = 0x000520FB;
memory.At(STAROS_HW0C_ENVIRONMENT + STAROS_HW0C_E_DACR) = 1;
memory.At(STAROS_HW0C_ENVIRONMENT + STAROS_HW0C_E_DESCRIPTOR) = 0x13500552;
memory.At(STAROS_HW0C_ENVIRONMENT + STAROS_HW0C_E_SENTINEL_BAD) = 0;
memory.At(starlet + STAROS_HW0C_C_STATE) = STAROS_HW0C_STATE_READY;
}
return;
}
if (ended)
return;
if (answered == STAROS_H1A_ROUNDS ||
memory.At(broadway + STAROS_HW0C_C_STATE) == STAROS_HW0C_STATE_END)
{
if (!ends)
return;
for (u32 i = 0; i < STAROS_H1A_RESULTS_WORDS; ++i)
memory.At(STAROS_H1A_RESULTS + 4 * i) = 0x1000 + i;
memory.At(STAROS_H1A_RESULTS + 4 * STAROS_H1A_F_MAGIC) = STAROS_H1A_MAGIC;
memory.At(STAROS_H1A_RESULTS + 4 * STAROS_H1A_F_VERSION) = STAROS_H1A_VERSION;
memory.At(STAROS_H1A_RESULTS + 4 * STAROS_H1A_F_GEN) = STAROS_H1A_GEN;
memory.At(starlet + STAROS_HW0C_C_STATE) = STAROS_HW0C_STATE_END;
ended = true;
return;
}
const u32 round = answered + 1;
if (memory.At(broadway + STAROS_HW0C_C_STATE) != STAROS_HW0C_STATE_ROUND ||
memory.At(broadway + STAROS_HW0C_C_ROUND) != round ||
memory.At(broadway + STAROS_HW0C_C_SCENARIO) != Scenario(round))
{
return;
}
if (!seen)
{
seen = true;
seen_at = now;
}
if (now - seen_at < 40960)
return;
const bool s2b = round > STAROS_H1A_S1_ROUNDS && (round - STAROS_H1A_S1_ROUNDS) % 2 == 0;
if (s2b)
{
for (u32 w = 0; w < STAROS_H1A_AREA_WORDS; ++w)
{
const u32 offset = STAROS_H1A_S2B + 4 * w;
memory.At(offset) = STAROS_HW0C_PATTERN(STAROS_HW0C_TAG_S2B, round, offset / 4);
}
}
memory.At(starlet + STAROS_HW0C_C_SCENARIO) = Scenario(round);
memory.At(starlet + STAROS_HW0C_C_STATE) = STAROS_HW0C_STATE_ROUND;
memory.At(starlet + STAROS_HW0C_C_ROUND) = round;
answered = round;
seen = false;
}
};
// One whole run of the real loader code in this process (its state is global: each death test's
// child runs it alone). Its report goes to stderr; the child exits 0 only if the run ended (or not)
// as `done` says, reached nothing outside the window, held every line of `required` in its report,
// and a second client in the same process was refused. (Windows' gtest has only simple regular
// expressions, so the child checks the report itself.)
[[noreturn]] void RunClient(Starlet starlet, u64 slices, bool done,
std::vector<std::string_view> required)
{
FakeMemory memory;
BroadwayClient client;
if (!client.Start(memory))
std::exit(2);
const u64 start = client.Timebase();
for (u64 slice = 0; slice < slices && !client.Done() && !client.Failed(); ++slice)
{
starlet.Tick(memory, client.Timebase(), start);
client.Advance(slice == 0 ? 0 : SLICE_CYCLES);
}
const std::string report =
client.Report(client.Done() ? CLIENT_STATUS_DONE : CLIENT_STATUS_TIMEOUT);
std::fputs(report.c_str(), stderr);
int missing = 0;
for (const std::string_view line : required)
{
if (report.find(line) == std::string::npos)
{
std::fprintf(stderr, "MISSING %.*s\n", static_cast<int>(line.size()), line.data());
++missing;
}
}
BroadwayClient second;
FakeMemory other;
const bool refused = !second.Start(other) && second.Failed();
std::fprintf(stderr, "steps %llu outside %d failed %d done %d refused %d missing %d\n",
static_cast<unsigned long long>(client.Steps()), memory.outside,
static_cast<int>(client.Failed()), static_cast<int>(client.Done()),
static_cast<int>(refused), missing);
const bool passed = client.Done() == done && !client.Failed() && memory.outside == 0 &&
refused && missing == 0;
std::exit(passed ? 0 : 1);
}
// Enough slices for 3000 rounds of at least 2 ms each, and the waits: 2^34 cycles' worth.
constexpr u64 SLICES = MAX_CLIENT_CYCLES / SLICE_CYCLES;
TEST(StarletProbeBroadwayDeathTest, TheRealLoaderRunsAllRoundsAndReports)
{
EXPECT_EXIT(RunClient(Starlet{}, SLICES, true,
{"SBL:STARLET DONE\n", "SBL:HW0C S0 PASS\n",
"SBL:H1A B POSTS 3000 LAST 3000 ACKS 3000 TIMEOUT 0 ",
"SBL:H1A B CHECK SENTINEL 0 SCRUB 0 POOL 0\n",
"SBL:H1A S GEN 0x00000002 0x00000002 ATTEMPTS 1 COHERENT\n",
"SBL:H1A END CRC 0x", "\nSBL:END GECKO DROPPED AFTER JUMP 0 BEFORE 0\n"}),
::testing::ExitedWithCode(0), "");
}
TEST(StarletProbeBroadwayDeathTest, WithoutReadyTheBenchTimesOutAndNeverStartsH1A)
{
Starlet starlet;
starlet.ready = false;
EXPECT_EXIT(RunClient(starlet, SLICES, true,
{"SBL:HW0C S0 INCONCLUSIVE\n", "SBL:HW0C TIMEOUT S0 ROUND 0\n",
"SBL:H1A B POSTS 0 LAST 0 ACKS 0 ", "ATTEMPTS 3 INCOMPLETE\n"}),
::testing::ExitedWithCode(0), "");
}
TEST(StarletProbeBroadwayDeathTest, WithoutTheStarletsEndBroadwayStopsAtReadyPlus65s)
{
Starlet starlet;
starlet.ends = false;
EXPECT_EXIT(RunClient(starlet, SLICES, true,
{"SBL:HW0C S0 INCONCLUSIVE\n", "SBL:HW0C TIMEOUT S0 ROUND 0\n",
"SBL:H1A B POSTS 3000 LAST 3000 ", "ATTEMPTS 3 INCOMPLETE\n"}),
::testing::ExitedWithCode(0), "");
}
TEST(StarletProbeBroadwayDeathTest, ABudgetTooShortEndsUnderTimeout)
{
EXPECT_EXIT(RunClient(Starlet{}, 1000, false,
{"SBL:TIMEOUT\n", "SBL:END GECKO DROPPED AFTER JUMP 0 BEFORE 0\n"}),
::testing::ExitedWithCode(0), "");
}
} // namespace
} // namespace StarletBootstrapProbe