diff --git a/Source/UnitTests/Core/CMakeLists.txt b/Source/UnitTests/Core/CMakeLists.txt index 6d9eae4b49..52a00f237c 100644 --- a/Source/UnitTests/Core/CMakeLists.txt +++ b/Source/UnitTests/Core/CMakeLists.txt @@ -22,6 +22,9 @@ target_include_directories(StarletProbePanicCheckTest PRIVATE ${CMAKE_SOURCE_DIR # 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) diff --git a/Source/UnitTests/Core/IOS/Starlet/ProbeRingTest.cpp b/Source/UnitTests/Core/IOS/Starlet/ProbeRingTest.cpp new file mode 100644 index 0000000000..18f178cbd6 --- /dev/null +++ b/Source/UnitTests/Core/IOS/Starlet/ProbeRingTest.cpp @@ -0,0 +1,58 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include + +#include "ProbeRing.h" + +namespace StarletBootstrapProbe +{ +namespace +{ +// Record bytes by offset; absent bytes read as zero. +class FakeRecord +{ +public: + std::map bytes; + void Put(const std::string& text) + { + for (size_t index = 0; index < text.size(); ++index) + bytes[SBR1_RING + static_cast(index)] = static_cast(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 diff --git a/Tools/StarletBootstrapProbe/ProbeRing.h b/Tools/StarletBootstrapProbe/ProbeRing.h new file mode 100644 index 0000000000..dc92dd7ab8 --- /dev/null +++ b/Tools/StarletBootstrapProbe/ProbeRing.h @@ -0,0 +1,40 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include + +#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& 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(byte); + else + text += fmt::format("\\x{:02x}", byte); + } + return text; +} +} // namespace StarletBootstrapProbe diff --git a/Tools/StarletBootstrapProbe/StarletBootstrapProbe.cpp b/Tools/StarletBootstrapProbe/StarletBootstrapProbe.cpp index aab2c4e06e..989572a648 100644 --- a/Tools/StarletBootstrapProbe/StarletBootstrapProbe.cpp +++ b/Tools/StarletBootstrapProbe/StarletBootstrapProbe.cpp @@ -19,6 +19,7 @@ #include "Core/IOS/Starlet/StarletMemory.h" #include "Core/System.h" #include "ProbeBudget.h" +#include "ProbeRing.h" #include "StarOSPanicCheck.h" namespace @@ -306,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;