Compare commits
2
Commits
6ede30f99e
...
bef2c8cf02
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bef2c8cf02 | ||
|
|
b457eb18c3 |
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user