Compare commits

...
2 Commits
Author SHA1 Message Date
Yaya48andClaude Opus 5.5 bef2c8cf02 StarletBootstrapProbe: print the SBR1 ring with --sbr1-record
The probe printed the ring's length only. StarOS's EXP-003 images put
lines in the ring that REV_TRACE has no room for (the MEM2 monitor, the
measurements), so under Dolphin nothing showed them. After the record's
summary, --sbr1-record now prints the ring's text between "SBR1 RING
BEGIN" and "SBR1 RING END" on stderr, as StarOS's Broadway loader prints
it over the Gecko on the console: at most the ring's 4032 bytes, a byte
outside printable ASCII and newline, and the backslash, written \xHH.

The formatting is in ProbeRing.h, tested by StarletProbeRingTest.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 17:50:01 +02:00
Yaya48andClaude Opus 5.5 b457eb18c3 StarletBootstrapProbe: --cycles
The probe ran every image for a fixed 512 slices of 4096 ARM cycles
(2097152). StarOS's EXP-003 images hash MEM2 and time series of
operations, which need more (StarOS experiments/proposals/mmu-console.md,
P-7 and P-8). --cycles <n> sets the budget: a decimal number of cycles,
a whole number of slices, from one slice to 2^30, given once; anything
else is a usage error (exit 2), never rounded. Without it the probe runs
2097152 cycles, as before, and the timeout messages name the budget.

The parser is in ProbeBudget.h, tested by StarletProbeBudgetTest.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 17:34:06 +02:00
6 changed files with 229 additions and 10 deletions

No files matched your search

+6
View File
@@ -19,6 +19,12 @@ add_dolphin_test(StarletBootstrapELFTest IOS/Starlet/BootstrapELFTest.cpp)
# StarletBootstrapProbe's StarOS panic check (Tools/StarletBootstrapProbe/StarOSPanicCheck.h).
add_dolphin_test(StarletProbePanicCheckTest IOS/Starlet/ProbePanicCheckTest.cpp)
target_include_directories(StarletProbePanicCheckTest PRIVATE ${CMAKE_SOURCE_DIR}/Tools/StarletBootstrapProbe)
# StarletBootstrapProbe's --cycles budget (Tools/StarletBootstrapProbe/ProbeBudget.h).
add_dolphin_test(StarletProbeBudgetTest IOS/Starlet/ProbeBudgetTest.cpp)
target_include_directories(StarletProbeBudgetTest PRIVATE ${CMAKE_SOURCE_DIR}/Tools/StarletBootstrapProbe)
# StarletBootstrapProbe's SBR1 ring text (Tools/StarletBootstrapProbe/ProbeRing.h).
add_dolphin_test(StarletProbeRingTest IOS/Starlet/ProbeRingTest.cpp)
target_include_directories(StarletProbeRingTest PRIVATE ${CMAKE_SOURCE_DIR}/Tools/StarletBootstrapProbe)
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,46 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <string_view>
#include <gtest/gtest.h>
#include "ProbeBudget.h"
namespace StarletBootstrapProbe
{
namespace
{
// The parsed budget, or 0 when refused.
u64 Parsed(std::string_view text)
{
return ParseCycles(text).value_or(0);
}
TEST(StarletProbeBudget, DefaultIsTheFormerFixedBudget)
{
EXPECT_EQ(DEFAULT_CYCLES, u64{2097152});
EXPECT_EQ(DEFAULT_CYCLES % SLICE_CYCLES, u64{0});
EXPECT_EQ(Parsed("2097152"), DEFAULT_CYCLES);
}
TEST(StarletProbeBudget, AcceptsWholeSlicesUpToTheMaximum)
{
EXPECT_EQ(Parsed("4096"), u64{4096});
EXPECT_EQ(Parsed("16777216"), u64{16777216});
EXPECT_EQ(Parsed("1073741824"), MAX_CYCLES);
// Leading zeros are still a decimal number.
EXPECT_EQ(Parsed("0008192"), u64{8192});
}
TEST(StarletProbeBudget, RefusesEverythingElse)
{
for (const std::string_view text :
{"", "0", "4095", "4097", "6144", "1073745920", "999999999999", "1234567890123",
"99999999999999999999", "-4096", "+4096", "0x1000", "4096 ", " 4096", "4k", "4096.0"})
{
EXPECT_FALSE(ParseCycles(text).has_value()) << '"' << text << '"';
}
}
} // namespace
} // namespace StarletBootstrapProbe
@@ -0,0 +1,58 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <map>
#include <string>
#include <gtest/gtest.h>
#include "ProbeRing.h"
namespace StarletBootstrapProbe
{
namespace
{
// Record bytes by offset; absent bytes read as zero.
class FakeRecord
{
public:
std::map<u32, u8> bytes;
void Put(const std::string& text)
{
for (size_t index = 0; index < text.size(); ++index)
bytes[SBR1_RING + static_cast<u32>(index)] = static_cast<u8>(text[index]);
}
u8 operator()(u32 offset) const
{
const auto it = bytes.find(offset);
return it == bytes.end() ? 0 : it->second;
}
};
TEST(StarletProbeRing, PrintsTheTextUpToItsLength)
{
FakeRecord record;
record.Put("REV:BOOT:ENTRY\nEXP3 OWN 23 PAGES 0 CHANGED\nleft over");
EXPECT_EQ(RingText(record, 0), "");
EXPECT_EQ(RingText(record, 15), "REV:BOOT:ENTRY\n");
EXPECT_EQ(RingText(record, 43), "REV:BOOT:ENTRY\nEXP3 OWN 23 PAGES 0 CHANGED\n");
}
TEST(StarletProbeRing, LengthIsClampedToTheRing)
{
EXPECT_EQ(SBR1_RING_BYTES, 4032u);
FakeRecord record;
record.Put(std::string(SBR1_RING_BYTES, 'x'));
record.bytes[SBR1_SIZE] = 'y';
EXPECT_EQ(RingText(record, 0xffffffff), std::string(SBR1_RING_BYTES, 'x'));
}
TEST(StarletProbeRing, EscapesWhatIsNotText)
{
FakeRecord record;
// a, NUL, b, CR, tab, backslash, c, DEL, 0xff, newline: ten bytes.
record.Put(std::string("a\0b\r\t\\c\x7f\xff\n", 10));
EXPECT_EQ(RingText(record, 10), "a\\x00b\\x0d\\x09\\x5cc\\x7f\\xff\n");
}
} // namespace
} // namespace StarletBootstrapProbe
+41
View File
@@ -0,0 +1,41 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <optional>
#include <string_view>
#include "Common/CommonTypes.h"
// How long StarletBootstrapProbe runs an image: whole slices of SLICE_CYCLES ARM cycles, between
// which it checks for a fault, a panic or a terminal trace line. --cycles <n> sets the budget;
// without it, the probe runs DEFAULT_CYCLES, as it always has. StarOS's EXP-003 images hash MEM2
// and time series of operations, which take more than that (StarOS
// experiments/proposals/mmu-console.md, P-7 and P-8).
namespace StarletBootstrapProbe
{
constexpr u32 SLICE_CYCLES = 4096;
constexpr u64 DEFAULT_CYCLES = 512 * SLICE_CYCLES;
// 2^30 cycles: 262,144 slices.
constexpr u64 MAX_CYCLES = u64{1} << 30;
// The budget --cycles asks for: a decimal number of cycles, a whole number of slices, from one
// slice to MAX_CYCLES. Anything else is refused (nullopt), never rounded.
inline std::optional<u64> ParseCycles(std::string_view text)
{
// Twelve digits cannot overflow a u64; MAX_CYCLES has ten.
if (text.empty() || text.size() > 12)
return std::nullopt;
u64 value = 0;
for (const char digit : text)
{
if (digit < '0' || digit > '9')
return std::nullopt;
value = value * 10 + static_cast<u64>(digit - '0');
}
if (value == 0 || value % SLICE_CYCLES != 0 || value > MAX_CYCLES)
return std::nullopt;
return value;
}
} // namespace StarletBootstrapProbe
+40
View File
@@ -0,0 +1,40 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <algorithm>
#include <functional>
#include <string>
#include <fmt/format.h>
#include "Common/CommonTypes.h"
#include "StarOSPanicCheck.h"
// The text of StarOS's SBR1 ring (StarOS crates/boot-contract, report.rs), which --sbr1-record
// prints after the run, as StarOS's Broadway loader prints it over the Gecko on the console
// (SBL:RING BEGIN/END). Bytes come through a callback, so the formatting is tested without a CPU.
namespace StarletBootstrapProbe
{
constexpr u32 SBR1_RING = 0x40;
constexpr u32 SBR1_RING_BYTES = SBR1_SIZE - SBR1_RING;
// The ring's first `length` bytes (at most SBR1_RING_BYTES), read at their record offsets. A
// byte outside printable ASCII and '\n', and the backslash itself, is written \xHH, so the text
// stays printable and reads back unambiguously.
inline std::string RingText(const std::function<u8(u32)>& read_byte, u32 length)
{
std::string text;
const u32 end = std::min(length, SBR1_RING_BYTES);
for (u32 index = 0; index < end; ++index)
{
const u8 byte = read_byte(SBR1_RING + index);
if (byte == '\n' || (byte >= 0x20 && byte < 0x7f && byte != '\\'))
text += static_cast<char>(byte);
else
text += fmt::format("\\x{:02x}", byte);
}
return text;
}
} // namespace StarletBootstrapProbe
@@ -5,6 +5,7 @@
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
@@ -17,6 +18,8 @@
#include "Core/IOS/Starlet/BootstrapELF.h"
#include "Core/IOS/Starlet/StarletMemory.h"
#include "Core/System.h"
#include "ProbeBudget.h"
#include "ProbeRing.h"
#include "StarOSPanicCheck.h"
namespace
@@ -48,7 +51,7 @@ bool FailOnAlert(const char* caption, const char* text, bool, Common::MsgType)
}
bool Run(const std::string& path, bool jit, bool mmu_faults, SBR1Mode sbr1,
ExpectedOutcome expected)
ExpectedOutcome expected, u64 cycles)
{
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file || file.tellg() <= 0 || file.tellg() > 16 * 1024 * 1024)
@@ -106,11 +109,11 @@ bool Run(const std::string& path, bool jit, bool mmu_faults, SBR1Mode sbr1,
<< memory.Read32(info.panic_code) << '\n';
};
int panic_settled = 0;
for (int slice = 0; slice < 512; ++slice)
for (u64 slice = 0; slice < cycles / SLICE_CYCLES; ++slice)
{
core.SetIRQLine(system.GetWiiIPC().IsStarletIRQAsserted());
core.SetFIQLine(system.GetWiiIPC().IsStarletFIQAsserted());
core.RunCycles(4096);
core.RunCycles(SLICE_CYCLES);
const u32 fault_code = memory.Read32(info.fault_code);
if (fault_code != 0)
{
@@ -193,11 +196,12 @@ bool Run(const std::string& path, bool jit, bool mmu_faults, SBR1Mode sbr1,
{
report_panic();
const std::string unmet = panic_unmet();
std::cerr << "StarOS panic path unfinished after 2097152 ARM cycles: "
<< (unmet.empty() ? "it ended inside the settle slices" : unmet) << '\n';
std::cerr << "StarOS panic path unfinished after " << cycles
<< " ARM cycles: " << (unmet.empty() ? "it ended inside the settle slices" : unmet)
<< '\n';
return false;
}
std::cerr << "No terminal bootstrap marker after 2097152 ARM cycles, PC=0x" << std::hex
std::cerr << "No terminal bootstrap marker after " << cycles << " ARM cycles, PC=0x" << std::hex
<< core.GetRegister(15) << std::dec << '\n';
return false;
}
@@ -206,11 +210,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 > 6)
if (argc < 2 || argc > 8)
{
std::cerr << "Usage: StarletBootstrapProbe <ELF> [--jit] [--mmu-faults] "
"[--sbr1-record|--sbr1-absent] "
"[--expect-panic|--expect-fatal-data|--expect-undefined-fault|--expect-phase2]\n";
"[--expect-panic|--expect-fatal-data|--expect-undefined-fault|--expect-phase2] "
"[--cycles <n>]\n";
return 2;
}
bool jit = false;
@@ -218,10 +223,22 @@ int main(int argc, char** argv)
SBR1Mode sbr1 = SBR1Mode::None;
ExpectedOutcome expected = ExpectedOutcome::Idle;
bool outcome_selected = false;
std::optional<u64> cycles;
for (int index = 2; index < argc; ++index)
{
const std::string_view option = argv[index];
if (option == "--jit")
if (option == "--cycles")
{
// A whole number of slices, at most MAX_CYCLES (ProbeBudget.h); given once.
if (cycles || index + 1 == argc || !(cycles = ParseCycles(argv[index + 1])))
{
std::cerr << "--cycles takes one decimal multiple of " << SLICE_CYCLES << ", at most "
<< MAX_CYCLES << ", once\n";
return 2;
}
++index;
}
else if (option == "--jit")
jit = true;
else if (option == "--mmu-faults")
mmu_faults = true;
@@ -273,7 +290,8 @@ int main(int argc, char** argv)
memory.Write_U32(SBR1_RELAY_JUMPING, SBR1_BASE + SBR1_RELAY_STATUS);
memory.Write_U32(SBR1_MAGIC, SBR1_BASE);
}
const bool passed = Run(argv[1], jit, mmu_faults, sbr1, expected);
const bool passed =
Run(argv[1], jit, mmu_faults, sbr1, expected, cycles.value_or(DEFAULT_CYCLES));
if (sbr1 != SBR1Mode::None)
{
// The record's summary after the run; with --sbr1-absent every word must still be zero (an
@@ -289,6 +307,16 @@ int main(int argc, char** argv)
for (u32 offset = 0; offset < SBR1_SIZE; offset += 4)
written += memory.Read_U32(SBR1_BASE + offset) != 0;
std::cerr << " nonzero_words " << written << '\n';
if (sbr1 == SBR1Mode::Record)
{
// The ring's text, as StarOS's Broadway loader prints it over the Gecko on the console;
// END starts its own line even when the text does not end with one.
const std::string ring =
RingText([&memory](u32 offset) { return memory.Read_U8(SBR1_BASE + offset); },
memory.Read_U32(SBR1_BASE + SBR1_RING_LENGTH));
std::cerr << "SBR1 RING BEGIN\n"
<< ring << (ring.empty() || ring.back() == '\n' ? "" : "\n") << "SBR1 RING END\n";
}
}
system.GetMemory().Shutdown();
return passed ? 0 : 1;