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