From b457eb18c3c7afa77c0c24de6e95e5784b8e745f Mon Sep 17 00:00:00 2001 From: Yaya48 Date: Wed, 14 Oct 2026 17:34:06 +0200 Subject: [PATCH] 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 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 --- Source/UnitTests/Core/CMakeLists.txt | 3 ++ .../Core/IOS/Starlet/ProbeBudgetTest.cpp | 46 +++++++++++++++++++ Tools/StarletBootstrapProbe/ProbeBudget.h | 41 +++++++++++++++++ .../StarletBootstrapProbe.cpp | 37 +++++++++++---- 4 files changed, 117 insertions(+), 10 deletions(-) create mode 100644 Source/UnitTests/Core/IOS/Starlet/ProbeBudgetTest.cpp create mode 100644 Tools/StarletBootstrapProbe/ProbeBudget.h diff --git a/Source/UnitTests/Core/CMakeLists.txt b/Source/UnitTests/Core/CMakeLists.txt index 66351f12b4..6d9eae4b49 100644 --- a/Source/UnitTests/Core/CMakeLists.txt +++ b/Source/UnitTests/Core/CMakeLists.txt @@ -19,6 +19,9 @@ 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) 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/ProbeBudgetTest.cpp b/Source/UnitTests/Core/IOS/Starlet/ProbeBudgetTest.cpp new file mode 100644 index 0000000000..fcb4da533b --- /dev/null +++ b/Source/UnitTests/Core/IOS/Starlet/ProbeBudgetTest.cpp @@ -0,0 +1,46 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include + +#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 diff --git a/Tools/StarletBootstrapProbe/ProbeBudget.h b/Tools/StarletBootstrapProbe/ProbeBudget.h new file mode 100644 index 0000000000..5aec19226a --- /dev/null +++ b/Tools/StarletBootstrapProbe/ProbeBudget.h @@ -0,0 +1,41 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#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 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 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(digit - '0'); + } + if (value == 0 || value % SLICE_CYCLES != 0 || value > MAX_CYCLES) + return std::nullopt; + return value; +} +} // namespace StarletBootstrapProbe diff --git a/Tools/StarletBootstrapProbe/StarletBootstrapProbe.cpp b/Tools/StarletBootstrapProbe/StarletBootstrapProbe.cpp index 8b0a6914ca..aab2c4e06e 100644 --- a/Tools/StarletBootstrapProbe/StarletBootstrapProbe.cpp +++ b/Tools/StarletBootstrapProbe/StarletBootstrapProbe.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -17,6 +18,7 @@ #include "Core/IOS/Starlet/BootstrapELF.h" #include "Core/IOS/Starlet/StarletMemory.h" #include "Core/System.h" +#include "ProbeBudget.h" #include "StarOSPanicCheck.h" namespace @@ -48,7 +50,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 +108,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 +195,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 +209,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 [--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"; return 2; } bool jit = false; @@ -218,10 +222,22 @@ int main(int argc, char** argv) SBR1Mode sbr1 = SBR1Mode::None; ExpectedOutcome expected = ExpectedOutcome::Idle; bool outcome_selected = false; + std::optional 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 +289,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