Compare commits

..
7 Commits
Author SHA1 Message Date
Yaya48andClaude Opus 5.5 dd2336f278 Starlet: keep a matched alarm's timer cause until HW_ALARM is rewritten
A console run (StarOS M3C gate H1, 2026-10-16) acknowledged the timer
through HW_ARMIRQFLAG before rewriting HW_ALARM, tens of microseconds after
the match, and read the cause still set. MINI's IRQ handler rewrites
HW_ALARM first, then acknowledges. The model fired the cause once on the
crossing, and a write-one-to-clear ended it for good, so the probe hid the
difference.

Once the counter reaches HW_ALARM, the timer's cause now stays asserted:
an acknowledgement sets it again until HW_ALARM is written. Writing the
comparator re-arms it (an immediate match when equal to the counter, as
before); a counter write does not end the match. One run does not tell a
latch from a "counter >= alarm" comparison: the latch is the smaller model
that explains it. The state is saved (STATE_VERSION 197).

Tests: StarletTimer.MatchedAlarmStaysAssertedUntilRewritten (both write
paths, a counter write, MINI's order, the next match) and
MatchedAlarmSurvivesStateRoundTrip.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-16 12:51:16 +02:00
Yaya48andClaude Opus 5.5 6c44367b5c Starlet: re-sample the interrupt lines on HW_ARMFIQMASK writes too
Review of d486779f50 (StarOS decision 44):
- A write to HW_ARMFIQMASK re-samples the attached core's IRQ and FIQ lines,
  as writes to HW_ARMIRQFLAG and HW_ARMIRQMASK do.
- Tests: FIQ masking and unmasking of a latched cause (wide and
  byte-assembled writes, then an acknowledgement); two pending IRQ sources,
  where acknowledging one keeps the line asserted and acknowledging the
  last lowers it.

The attached core cannot be used after it is destroyed: Starlet::Shutdown
resets m_core then m_memory with nothing in between, ~Starlet destroys
m_core (declared after m_memory) first, Init attaches only after the last
path that can fail, StarletMemory::Reset keeps the core, which is still
alive, and the probe's core is a local declared after its memory.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-16 02:49:39 +02:00
Yaya48andClaude Opus 5.5 d486779f50 Starlet: re-sample the ARM IRQ and FIQ lines on interrupt register writes
The core's IRQ and FIQ lines were set from WiiIPC's flags and masks only at
the start of each scheduler slice (Starlet::RunSlice, StarletBootstrapProbe).
Acknowledging an interrupt (HW_ARMIRQFLAG, write one to clear) or masking it
(HW_ARMIRQMASK) left the line asserted until the next slice, so code that
acknowledged an interrupt and unmasked IRQ within the same slice took it
again with no cause pending. Hollywood's interrupt output is level-sensitive.

StarletMemory now re-samples the lines of the core attached with
AttachInterruptLines on every write to those two registers, on both its
wide and byte-assembled write paths; Starlet and StarletBootstrapProbe
attach theirs. Raising a cause still reaches the core at the next slice, as
before. ARMCore exposes IsIRQLineAsserted and IsFIQLineAsserted for the
unit test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-16 02:19:55 +02:00
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
Yaya48andClaude Opus 5.5 6ede30f99e StarletBootstrapProbe: wait for StarOS's whole panic path
--expect-panic passed as soon as __panic_state was nonzero at the end of
a 4096-cycle slice. StarOS's handler stores the guard first, then the
code, then publishes the SBR1 record, so the run could stop with the path
half done. The probe now keeps running until the path is finished and
still is two slices later, and fails if the run budget ends first:
- __panic_state 1 and __panic_code 1;
- with --sbr1-record: STARLET_STATE 5 (Panic), FATAL_REASON 0 and
  EXI_OWNER 1 (handed back to the PPC);
- with --sbr1-absent: every word of the record area still zero.
The failure names the first unmet condition. The check is
StarOSPanicCheck.h, pure, with the SBR1 constants moved there, and
StarletProbePanicCheckTest covers each condition; each of 7 mutants of
the check fails a test. Runs that do not expect a panic are unchanged.

With StarOS's panic test image (eb930ee), interpreter and JIT: the
scenarios pass, two slices later than before. Three images derived from
it pass the previous probe and now fail: EXI kept by the Starlet
(--sbr1-record), __panic_code never stored, the record written although
absent (--sbr1-absent). (StarOS MMU-001 review of F1.)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 00:20:23 +02:00
Yaya48andClaude Opus 5.5 a62c65615a IOS LLE: StarOS's panic words must not alias
LoadBootstrapELF accepted __panic_state and __panic_code at the same
address, or as two words sharing bytes: the code's store would then also
set the guard, and nothing could tell the two apart. They must now be two
non-overlapping words of the data segment, like the other checks, before
any byte is written. BootstrapELFTest.PanicWordsMustNotAlias: the same
word and three partial overlaps, all inside the data segment, are
rejected; adjacent words are accepted. With the rule disabled, the test
fails. (StarOS MMU-001 review of F1.)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 00:20:22 +02:00
16 changed files with 782 additions and 51 deletions

No files matched your search

+2
View File
@@ -159,6 +159,8 @@ public:
}
bool IsWaitingForInterrupt() const { return m_waiting_for_interrupt; }
bool IsWaitingForMemoryPoll() const { return m_waiting_for_memory_poll; }
bool IsIRQLineAsserted() const { return m_irq_line; }
bool IsFIQLineAsserted() const { return m_fiq_line; }
u32 GetMemoryPollAddress() const { return m_memory_poll_address; }
bool IsWaitingForExternalEvent() const
{
@@ -216,9 +216,14 @@ bool LoadBootstrapELF(std::span<const u8> image, ARMBus& bus, BootstrapELFInfo*
{
return Fail(error, "bootstrap ELF is missing the trace or fault symbols");
}
// The panic words are optional, but both or neither, as words of the data segment.
// The panic words are optional, but both or neither, as two distinct words of the data segment:
// aliased, the code's store would also set the guard, and a probe could not tell them apart.
const bool panic_words_overlap =
static_cast<u64>(parsed.panic_state) < static_cast<u64>(parsed.panic_code) + 4 &&
static_cast<u64>(parsed.panic_code) < static_cast<u64>(parsed.panic_state) + 4;
if ((parsed.panic_state != 0) != (parsed.panic_code != 0) ||
(parsed.panic_state != 0 && (!in_data(parsed.panic_state, 4) || !in_data(parsed.panic_code, 4))))
(parsed.panic_state != 0 && (!in_data(parsed.panic_state, 4) ||
!in_data(parsed.panic_code, 4) || panic_words_overlap)))
{
return Fail(error, "bootstrap ELF has an invalid panic symbol");
}
+2
View File
@@ -144,6 +144,8 @@ bool Starlet::Init(const std::string& dump_directory, std::string* error)
// construct it only after the complete physical bus exists.
m_core = std::make_unique<ARMCore>(*m_memory,
Config::Get(Config::MAIN_WII_STARLET_JIT_MEMORY_PROFILING));
// m_core is declared after m_memory, so it is destroyed first.
m_memory->AttachInterruptLines(m_core.get());
#if defined(_M_X86_64)
m_core->SetJitEnabled(Config::Get(Config::MAIN_WII_STARLET_JIT));
#else
+47 -3
View File
@@ -333,6 +333,9 @@ constexpr u32 HW_AHBPROT = HW_BASE + 0x64;
constexpr u32 HW_TIMER = HW_BASE + 0x10;
constexpr u64 TIMER_CLOCK_DIVISOR = 128;
constexpr u32 HW_ALARM = HW_BASE + 0x14;
constexpr u32 HW_ARMIRQFLAG = HW_BASE + 0x38;
constexpr u32 HW_ARMIRQMASK = HW_BASE + 0x3c;
constexpr u32 HW_ARMFIQMASK = HW_BASE + 0x40;
constexpr u32 HW_GPIO_ENABLE = HW_BASE + 0xdc;
constexpr u32 HW_GPIO_OUT = HW_BASE + 0xe0;
constexpr u32 HW_GPIO_DIR = HW_BASE + 0xe4;
@@ -864,6 +867,7 @@ void StarletMemory::Reset()
ResetOHCIController(controller);
m_arm_cycles = 0;
m_timer_offset = 0;
m_alarm_matched = false;
m_boot0_mapped = true;
m_sram_split_mode = false;
// Retail Hollywood production revision. Early firmware branches on this
@@ -1068,6 +1072,7 @@ void StarletMemory::DoState(PointerWrap& p)
p.Do(m_seeprom_write_all);
p.Do(m_arm_cycles);
p.Do(m_timer_offset);
p.Do(m_alarm_matched);
p.Do(m_initialized);
p.Do(m_boot0_mapped);
p.Do(m_sram_split_mode);
@@ -1104,6 +1109,36 @@ void StarletMemory::DoState(PointerWrap& p)
}
}
void StarletMemory::AttachInterruptLines(ARMCore* core)
{
m_interrupt_core = core;
}
void StarletMemory::UpdateInterruptLines(u32 address)
{
if (m_interrupt_core == nullptr ||
(address != HW_ARMIRQFLAG && address != HW_ARMIRQMASK && address != HW_ARMFIQMASK))
{
return;
}
const WiiIPC& ipc = m_system.GetWiiIPC();
m_interrupt_core->SetIRQLine(ipc.IsStarletIRQAsserted());
m_interrupt_core->SetFIQLine(ipc.IsStarletFIQAsserted());
}
void StarletMemory::KeepMatchedAlarmAsserted(u32 address)
{
// Once the counter has reached HW_ALARM, acknowledging the timer through
// HW_ARMIRQFLAG does not clear its cause until HW_ALARM is written again.
// Observed on a console: StarOS's M3C gate H1 (2026-10-16) acknowledged the
// timer before rewriting HW_ALARM, tens of microseconds after the match, and
// read the cause still set. MINI's handler rewrites HW_ALARM first. One run
// does not tell a latch from a "counter >= alarm" comparison; the latch is
// the smaller model that explains it.
if (address == HW_ARMIRQFLAG && m_alarm_matched)
m_system.GetWiiIPC().SetStarletInterrupt(INT_CAUSE_TIMER, true);
}
u32 StarletMemory::GetTimer() const
{
// Hollywood's timer runs at Starlet / 128 (1.8984375 MHz at 243 MHz).
@@ -1134,7 +1169,10 @@ void StarletMemory::AdvanceCycles(u64 cycles)
const u32 alarm = ReadRegister(HW_ALARM);
const u32 ticks_until_alarm = alarm - old_timer;
if (ticks_until_alarm != 0 && ticks_until_alarm <= elapsed_timer_ticks)
{
m_alarm_matched = true;
m_system.GetWiiIPC().SetStarletInterrupt(INT_CAUSE_TIMER, true);
}
if (m_nand_cycles_until_completion != 0)
{
@@ -4099,9 +4137,11 @@ void StarletMemory::Write8(u32 address, u8 value)
{
// HW_ALARM is a comparator, not an interrupt acknowledgement register.
// Original Starlet software acknowledges the timer separately through
// HW_ARMIRQFLAG. Programming the comparator to the current counter value
// creates an immediate match.
if (word == GetTimer())
// HW_ARMIRQFLAG. Programming the comparator re-arms it: a past match no
// longer holds the cause (KeepMatchedAlarmAsserted). Programming it to the
// current counter value creates an immediate match.
m_alarm_matched = word == GetTimer();
if (m_alarm_matched)
m_system.GetWiiIPC().SetStarletInterrupt(INT_CAUSE_TIMER, true);
}
else if (word_address == HW_SPARE0)
@@ -4147,6 +4187,8 @@ void StarletMemory::Write8(u32 address, u8 value)
word_address == HW_AHBPROT || word_address == HW_RESETS)
{
m_system.GetWiiIPC().WriteStarletRegister(word_address - HW_BASE, word);
KeepMatchedAlarmAsserted(word_address);
UpdateInterruptLines(word_address);
if (word_address == HW_BASE + 0x0c && (word & 0x09) != 0)
{
// The physical CPUs are concurrent. Give Broadway a scheduling boundary
@@ -4229,6 +4271,8 @@ void StarletMemory::Write32(u32 address, u32 value)
{
WriteRegister(address, value);
m_system.GetWiiIPC().WriteStarletRegister(address - HW_BASE, value);
KeepMatchedAlarmAsserted(address);
UpdateInterruptLines(address);
// Phase 11 H1: an IPC reply (ARMCTRL Y1) audits the watched ranges.
if (!m_watch_ranges.empty() && address == HW_BASE + 0x0c && (value & 1) != 0)
WatchAudit();
@@ -82,6 +82,13 @@ public:
// that touches one of those physical ranges. The ARM JIT sends the accesses that hit a watched
// range to its bus slow path, with the instruction address. `core` gives that address.
void SetWatchCore(const ARMCore* core);
// The core whose IRQ and FIQ lines follow Hollywood's ARM interrupt registers. A write to
// HW_ARMIRQFLAG, HW_ARMIRQMASK or HW_ARMFIQMASK re-samples both lines at once, as the
// level-sensitive Hollywood output follows them on hardware, instead of at the next scheduler
// slice: an acknowledged or masked interrupt is never taken again in between. Raising a cause
// still reaches the line at the next slice. The owner of both attaches the core once it exists
// and destroys it before the memory.
void AttachInterruptLines(ARMCore* core);
void Reset();
void DoState(PointerWrap& p);
@@ -110,6 +117,10 @@ public:
private:
// Synthetic NAND fixtures exercise startup pairing without any console dumps or keys.
friend class StarletPairingTest;
void UpdateInterruptLines(u32 address);
void KeepMatchedAlarmAsserted(u32 address);
ARMCore* m_interrupt_core = nullptr;
friend class StarletDMATest;
static constexpr u32 NAND_PAGE_DATA_SIZE = 0x800;
@@ -347,6 +358,8 @@ private:
bool m_seeprom_write_all = false;
u64 m_arm_cycles = 0;
u32 m_timer_offset = 0;
// The counter has reached HW_ALARM since it was last written.
bool m_alarm_matched = false;
bool m_initialized = false;
// Phase 11 F1 write watch (REV_WATCH); empty when off.
std::vector<std::pair<u32, u32>> m_watch_ranges;
+1 -1
View File
@@ -97,7 +97,7 @@ struct CompressAndDumpStateArgs
static Common::WorkQueueThreadSP<CompressAndDumpStateArgs> s_compress_and_dump_thread;
// Don't forget to increase this after doing changes on the savestate system
constexpr u32 STATE_VERSION = 196; // Correct Hollywood DI/reset pending interrupt bit mapping.
constexpr u32 STATE_VERSION = 197; // Hollywood's alarm stays matched until HW_ALARM is rewritten.
// Increase this if the StateExtendedHeader definition changes
constexpr u32 EXTENDED_HEADER_VERSION = 1; // Last changed in PR 12217
+9
View File
@@ -16,6 +16,15 @@ add_dolphin_test(ESFormatsTest IOS/ES/FormatsTest.cpp)
add_dolphin_test(StarletARMCoreTest IOS/Starlet/ARMCoreTest.cpp)
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)
@@ -568,6 +568,208 @@ TEST(StarletRegisters, WideTimerAndInterruptAccessesMatchHardwareSemantics)
EXPECT_EQ(memory.Read32(arm_irq_mask), 0x800619efu);
}
TEST(StarletInterrupts, InterruptRegisterWritesResampleTheAttachedCoreLines)
{
constexpr u32 hardware_base = 0x0d800000;
constexpr u32 arm_irq_flag = hardware_base + 0x38;
constexpr u32 arm_irq_mask = hardware_base + 0x3c;
constexpr u32 arm_fiq_mask = hardware_base + 0x40;
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
auto& ipc = system.GetWiiIPC();
ipc.Reset();
StarletMemory memory(system);
memory.Reset();
ARMCore core(memory);
memory.AttachInterruptLines(&core);
// A latched, unmasked timer interrupt, sampled at a scheduler slice's start.
memory.Write32(arm_irq_mask, INT_CAUSE_TIMER);
ipc.SetStarletInterrupt(INT_CAUSE_TIMER, true);
core.SetIRQLine(ipc.IsStarletIRQAsserted());
ASSERT_TRUE(core.IsIRQLineAsserted());
// Acknowledging it lowers the line at once, not at the next slice.
memory.Write32(arm_irq_flag, INT_CAUSE_TIMER);
EXPECT_FALSE(core.IsIRQLineAsserted());
// Raising a cause still waits for the slice; masking and unmasking a latched cause do not.
ipc.SetStarletInterrupt(INT_CAUSE_TIMER, true);
EXPECT_FALSE(core.IsIRQLineAsserted());
memory.Write32(arm_irq_mask, 0);
EXPECT_FALSE(core.IsIRQLineAsserted());
memory.Write32(arm_irq_mask, INT_CAUSE_TIMER);
EXPECT_TRUE(core.IsIRQLineAsserted());
// The byte-assembled write path acknowledges as the wide one does.
for (u32 i = 0; i < 4; ++i)
memory.Write8(arm_irq_flag + i, static_cast<u8>(INT_CAUSE_TIMER >> (24 - 8 * i)));
EXPECT_FALSE(core.IsIRQLineAsserted());
// An acknowledgement re-samples the FIQ line from the same flags.
memory.Write32(arm_fiq_mask, INT_CAUSE_TIMER);
ipc.SetStarletInterrupt(INT_CAUSE_TIMER, true);
core.SetFIQLine(ipc.IsStarletFIQAsserted());
ASSERT_TRUE(core.IsFIQLineAsserted());
memory.Write32(arm_irq_flag, INT_CAUSE_TIMER);
EXPECT_FALSE(core.IsFIQLineAsserted());
}
TEST(StarletInterrupts, FIQMaskWritesResampleTheAttachedCoreLine)
{
constexpr u32 hardware_base = 0x0d800000;
constexpr u32 arm_irq_flag = hardware_base + 0x38;
constexpr u32 arm_fiq_mask = hardware_base + 0x40;
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
auto& ipc = system.GetWiiIPC();
ipc.Reset();
StarletMemory memory(system);
memory.Reset();
ARMCore core(memory);
memory.AttachInterruptLines(&core);
// A latched cause routed to no FIQ, sampled at a scheduler slice's start.
ipc.SetStarletInterrupt(INT_CAUSE_TIMER, true);
core.SetFIQLine(ipc.IsStarletFIQAsserted());
ASSERT_FALSE(core.IsFIQLineAsserted());
// Unmasking it as an FIQ raises the line at once; masking it lowers the line at once.
memory.Write32(arm_fiq_mask, INT_CAUSE_TIMER);
EXPECT_TRUE(core.IsFIQLineAsserted());
EXPECT_FALSE(core.IsIRQLineAsserted());
memory.Write32(arm_fiq_mask, 0);
EXPECT_FALSE(core.IsFIQLineAsserted());
// The byte-assembled write path re-samples the same way.
for (u32 i = 0; i < 4; ++i)
memory.Write8(arm_fiq_mask + i, static_cast<u8>(INT_CAUSE_TIMER >> (24 - 8 * i)));
EXPECT_TRUE(core.IsFIQLineAsserted());
memory.Write32(arm_irq_flag, INT_CAUSE_TIMER);
EXPECT_FALSE(core.IsFIQLineAsserted());
}
TEST(StarletTimer, MatchedAlarmStaysAssertedUntilRewritten)
{
constexpr u32 hardware_base = 0x0d800000;
constexpr u32 timer = hardware_base + 0x10;
constexpr u32 alarm = hardware_base + 0x14;
constexpr u32 arm_irq_flag = hardware_base + 0x38;
constexpr u32 arm_irq_mask = hardware_base + 0x3c;
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
auto& ipc = system.GetWiiIPC();
ipc.Reset();
StarletMemory memory(system);
memory.Reset();
ARMCore core(memory);
memory.AttachInterruptLines(&core);
const auto timer_cause = [&ipc] { return ipc.ReadStarletRegister(0x38) & INT_CAUSE_TIMER; };
memory.Write32(arm_irq_mask, INT_CAUSE_TIMER);
memory.Write32(alarm, 64);
memory.AdvanceCycles(64 * 128);
ASSERT_EQ(memory.Read32(timer), 64u);
ASSERT_EQ(timer_cause(), INT_CAUSE_TIMER);
core.SetIRQLine(ipc.IsStarletIRQAsserted());
ASSERT_TRUE(core.IsIRQLineAsserted());
// Acknowledged before HW_ALARM is rewritten, after the counter moved on: the
// cause and the line stay asserted (the console's StarOS M3C gate H1).
memory.AdvanceCycles(32 * 128);
memory.Write32(arm_irq_flag, INT_CAUSE_TIMER);
EXPECT_EQ(timer_cause(), INT_CAUSE_TIMER);
EXPECT_TRUE(core.IsIRQLineAsserted());
for (u32 i = 0; i < 4; ++i)
memory.Write8(arm_irq_flag + i, static_cast<u8>(INT_CAUSE_TIMER >> (24 - 8 * i)));
EXPECT_EQ(timer_cause(), INT_CAUSE_TIMER);
// A counter write does not end the match either.
memory.Write32(timer, 0);
memory.Write32(arm_irq_flag, INT_CAUSE_TIMER);
EXPECT_EQ(timer_cause(), INT_CAUSE_TIMER);
// MINI's order: rewrite HW_ALARM ahead, then acknowledge. The rewrite alone
// acknowledges nothing; the acknowledgement then clears the cause and the line.
memory.Write32(alarm, memory.Read32(timer) + 64);
EXPECT_EQ(timer_cause(), INT_CAUSE_TIMER);
memory.Write32(arm_irq_flag, INT_CAUSE_TIMER);
EXPECT_EQ(timer_cause(), 0u);
EXPECT_FALSE(core.IsIRQLineAsserted());
// The next match latches again.
memory.AdvanceCycles(64 * 128);
EXPECT_EQ(timer_cause(), INT_CAUSE_TIMER);
memory.Write32(arm_irq_flag, INT_CAUSE_TIMER);
EXPECT_EQ(timer_cause(), INT_CAUSE_TIMER);
}
TEST(StarletTimer, MatchedAlarmSurvivesStateRoundTrip)
{
constexpr u32 alarm = 0x0d800014;
constexpr u32 arm_irq_flag = 0x0d800038;
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
auto& ipc = system.GetWiiIPC();
ipc.Reset();
StarletMemory memory(system);
memory.Reset();
memory.Write32(alarm, 8);
memory.AdvanceCycles(8 * 128);
ASSERT_EQ(ipc.ReadStarletRegister(0x38) & INT_CAUSE_TIMER, INT_CAUSE_TIMER);
std::vector<u8> state_buffer(1024 * 1024);
u8* state_pointer = state_buffer.data();
PointerWrap writer(&state_pointer, state_buffer.size(), PointerWrap::Mode::Write);
memory.DoState(writer);
ASSERT_TRUE(writer.IsWriteMode());
const size_t state_size = state_pointer - state_buffer.data();
// A reset forgets the match: the acknowledgement clears the cause.
memory.Reset();
memory.Write32(arm_irq_flag, INT_CAUSE_TIMER);
ASSERT_EQ(ipc.ReadStarletRegister(0x38) & INT_CAUSE_TIMER, 0u);
// The restored state holds it again.
state_pointer = state_buffer.data();
PointerWrap reader(&state_pointer, state_size, PointerWrap::Mode::Read);
memory.DoState(reader);
ASSERT_TRUE(reader.IsReadMode());
memory.Write32(arm_irq_flag, INT_CAUSE_TIMER);
EXPECT_EQ(ipc.ReadStarletRegister(0x38) & INT_CAUSE_TIMER, INT_CAUSE_TIMER);
}
TEST(StarletInterrupts, AcknowledgingOneOfTwoPendingSourcesKeepsIRQAsserted)
{
constexpr u32 hardware_base = 0x0d800000;
constexpr u32 arm_irq_flag = hardware_base + 0x38;
constexpr u32 arm_irq_mask = hardware_base + 0x3c;
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
auto& ipc = system.GetWiiIPC();
ipc.Reset();
StarletMemory memory(system);
memory.Reset();
ARMCore core(memory);
memory.AttachInterruptLines(&core);
memory.Write32(arm_irq_mask, INT_CAUSE_TIMER | INT_CAUSE_NAND);
ipc.SetStarletInterrupt(INT_CAUSE_TIMER, true);
ipc.SetStarletInterrupt(INT_CAUSE_NAND, true);
core.SetIRQLine(ipc.IsStarletIRQAsserted());
ASSERT_TRUE(core.IsIRQLineAsserted());
// One source acknowledged: the other still holds the line.
memory.Write32(arm_irq_flag, INT_CAUSE_TIMER);
EXPECT_TRUE(core.IsIRQLineAsserted());
EXPECT_EQ(memory.Read32(arm_irq_flag) & (INT_CAUSE_TIMER | INT_CAUSE_NAND), INT_CAUSE_NAND);
// The last one acknowledged: the line drops.
memory.Write32(arm_irq_flag, INT_CAUSE_NAND);
EXPECT_FALSE(core.IsIRQLineAsserted());
EXPECT_EQ(memory.Read32(arm_irq_flag) & (INT_CAUSE_TIMER | INT_CAUSE_NAND), 0u);
}
TEST(StarletInterrupts, DriveAndResetUseDistinctHollywoodLines)
{
// Hollywood IRQ numbers, not the Broadway Processor Interface's DVD line.
@@ -281,5 +281,32 @@ TEST(BootstrapELFTest, PanicWordsAreOptionalButBothInData)
EXPECT_TRUE(rejected.bytes.empty());
}
}
TEST(BootstrapELFTest, PanicWordsMustNotAlias)
{
BootstrapELFInfo info;
std::string error;
// The same word, or two words sharing bytes, in either order; all inside the 0x420-byte data
// segment, so only the aliasing rule rejects them.
for (const auto [state, code] :
{std::pair{DATA_BASE + 4, DATA_BASE + 4}, std::pair{DATA_BASE + 4, DATA_BASE + 6},
std::pair{DATA_BASE + 7, DATA_BASE + 4}, std::pair{DATA_BASE + 0x41c, DATA_BASE + 0x41a}})
{
RecordingBus rejected;
EXPECT_FALSE(LoadBootstrapELF(MakePanicImage(state, code), rejected, &info, &error))
<< std::hex << state << ' ' << code;
EXPECT_EQ(error, "bootstrap ELF has an invalid panic symbol");
EXPECT_TRUE(rejected.bytes.empty());
}
// Adjacent words do not alias.
for (const auto [state, code] :
{std::pair{DATA_BASE + 4, DATA_BASE + 8}, std::pair{DATA_BASE + 8, DATA_BASE + 4}})
{
RecordingBus bus;
EXPECT_TRUE(LoadBootstrapELF(MakePanicImage(state, code), bus, &info, &error)) << error;
EXPECT_EQ(info.panic_state, state);
EXPECT_EQ(info.panic_code, code);
}
}
} // namespace
} // namespace IOS::LLE
@@ -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,99 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <map>
#include <string>
#include <gtest/gtest.h>
#include "StarOSPanicCheck.h"
namespace StarletBootstrapProbe
{
namespace
{
// MEM1 words by address; absent words read as zero.
class FakeMem1
{
public:
std::map<u32, u32> words;
u32 operator()(u32 address) const
{
const auto it = words.find(address);
return it == words.end() ? 0 : it->second;
}
};
// The record as the loader leaves it, before the image runs.
FakeMem1 CreatedRecord()
{
FakeMem1 mem1;
mem1.words[SBR1_BASE] = SBR1_MAGIC;
mem1.words[SBR1_BASE + 0x04] = SBR1_VERSION;
mem1.words[SBR1_BASE + 0x08] = SBR1_SIZE;
mem1.words[SBR1_BASE + SBR1_EXI_OWNER] = SBR1_EXI_STARLET;
mem1.words[SBR1_BASE + SBR1_RELAY_STATUS] = SBR1_RELAY_JUMPING;
return mem1;
}
// The record once StarOS's panic path has published it.
FakeMem1 PanicRecord()
{
FakeMem1 mem1 = CreatedRecord();
mem1.words[SBR1_BASE + SBR1_STARLET_STATE] = SBR1_STATE_PANIC;
mem1.words[SBR1_BASE + SBR1_EXI_OWNER] = SBR1_EXI_PPC;
return mem1;
}
TEST(StarletProbePanicCheck, WaitsForTheCodeAfterTheGuard)
{
const FakeMem1 none;
EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::None, none), "");
// The guard is stored before the code: a slice can end in between.
EXPECT_EQ(UnmetPanicCondition(1, 0, SBR1Mode::None, none), "__panic_code is 0, not 1");
EXPECT_EQ(UnmetPanicCondition(1, 2, SBR1Mode::None, none), "__panic_code is 2, not 1");
EXPECT_EQ(UnmetPanicCondition(0, 1, SBR1Mode::None, none), "__panic_state is 0, not 1");
EXPECT_EQ(UnmetPanicCondition(2, 1, SBR1Mode::None, none), "__panic_state is 2, not 1");
// The words come first, whatever the record says.
EXPECT_EQ(UnmetPanicCondition(1, 0, SBR1Mode::Record, PanicRecord()),
"__panic_code is 0, not 1");
}
TEST(StarletProbePanicCheck, RecordNeedsPanicStateReasonAndEXIBack)
{
EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::Record, PanicRecord()), "");
// Panic words set, record not yet published.
EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::Record, CreatedRecord()),
"SBR1 STARLET_STATE is 0, not 5");
FakeMem1 running = PanicRecord();
running.words[SBR1_BASE + SBR1_STARLET_STATE] = 2;
EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::Record, running), "SBR1 STARLET_STATE is 2, not 5");
FakeMem1 reason = PanicRecord();
reason.words[SBR1_BASE + SBR1_FATAL_REASON] = 3;
EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::Record, reason), "SBR1 FATAL_REASON is 3, not 0");
// State published, EXI not yet handed back to the PPC.
FakeMem1 exi = PanicRecord();
exi.words[SBR1_BASE + SBR1_EXI_OWNER] = SBR1_EXI_STARLET;
EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::Record, exi), "SBR1 EXI_OWNER is 2, not 1");
}
TEST(StarletProbePanicCheck, AbsentRecordAreaStaysZero)
{
EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::Absent, FakeMem1{}), "");
for (const u32 offset : {0x0u, SBR1_STARLET_STATE, SBR1_EXI_OWNER, SBR1_SIZE - 4})
{
FakeMem1 written;
written.words[SBR1_BASE + offset] = 5;
EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::Absent, written),
fmt::format("SBR1 area word +{:#x} is 0x5 without a record", offset));
}
// Words outside the area are not the record's.
FakeMem1 outside;
outside.words[SBR1_BASE - 4] = 5;
outside.words[SBR1_BASE + SBR1_SIZE] = 5;
EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::Absent, outside), "");
// Without an SBR1 option, a written area is not checked.
EXPECT_EQ(UnmetPanicCondition(1, 1, SBR1Mode::None, PanicRecord()), "");
}
} // 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
@@ -0,0 +1,89 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <functional>
#include <string>
#include <fmt/format.h>
#include "Common/CommonTypes.h"
// What StarletBootstrapProbe requires of a finished StarOS panic path (StarOS ADR-0014 R6). The
// path sets __panic_state first, then __panic_code, then publishes the SBR1 boot record when
// there is one, so the first nonzero guard can show it half done: the probe keeps running until
// every condition holds. Words come through a callback, so the check is tested without a CPU.
namespace StarletBootstrapProbe
{
// StarOS's SBR1 boot record in MEM1 (StarOS crates/boot-contract, report.rs).
enum class SBR1Mode
{
None,
// --sbr1-record: the probe creates the record as the loader does.
Record,
// --sbr1-absent: no record; its area must stay zero.
Absent,
};
constexpr u32 SBR1_BASE = 0x00200000;
constexpr u32 SBR1_SIZE = 0x1000;
constexpr u32 SBR1_MAGIC = 0x53425231;
constexpr u32 SBR1_VERSION = 1;
// Word offsets.
constexpr u32 SBR1_EXI_OWNER = 0x0c;
constexpr u32 SBR1_RELAY_STATUS = 0x10;
constexpr u32 SBR1_STARLET_STATE = 0x18;
constexpr u32 SBR1_FATAL_REASON = 0x1c;
constexpr u32 SBR1_FAULT_CODE = 0x20;
constexpr u32 SBR1_RING_LENGTH = 0x2c;
constexpr u32 SBR1_RING_DROPPED = 0x30;
// Values.
constexpr u32 SBR1_EXI_PPC = 1;
constexpr u32 SBR1_EXI_STARLET = 2;
constexpr u32 SBR1_RELAY_JUMPING = 4;
constexpr u32 SBR1_STATE_PANIC = 5;
// __panic_state once the path is entered; __panic_code of a Rust panic.
constexpr u32 PANIC_ENTERED = 1;
constexpr u32 PANIC_CODE_RUST = 1;
// The first condition of a finished panic path that does not hold, or an empty string:
// - __panic_state 1 and __panic_code 1;
// - with --sbr1-record: STARLET_STATE 5 (Panic), FATAL_REASON 0 and EXI_OWNER 1 (PPC);
// - with --sbr1-absent: every word of the record area still zero.
// read_mem1 returns the big-endian MEM1 word at an address.
inline std::string UnmetPanicCondition(u32 panic_state, u32 panic_code, SBR1Mode sbr1,
const std::function<u32(u32)>& read_mem1)
{
if (panic_state != PANIC_ENTERED)
return fmt::format("__panic_state is {}, not {}", panic_state, PANIC_ENTERED);
if (panic_code != PANIC_CODE_RUST)
return fmt::format("__panic_code is {}, not {}", panic_code, PANIC_CODE_RUST);
if (sbr1 == SBR1Mode::Record)
{
const struct
{
const char* name;
u32 offset;
u32 expected;
} fields[] = {{"STARLET_STATE", SBR1_STARLET_STATE, SBR1_STATE_PANIC},
{"FATAL_REASON", SBR1_FATAL_REASON, 0},
{"EXI_OWNER", SBR1_EXI_OWNER, SBR1_EXI_PPC}};
for (const auto& field : fields)
{
const u32 value = read_mem1(SBR1_BASE + field.offset);
if (value != field.expected)
return fmt::format("SBR1 {} is {}, not {}", field.name, value, field.expected);
}
}
if (sbr1 == SBR1Mode::Absent)
{
for (u32 offset = 0; offset < SBR1_SIZE; offset += 4)
{
const u32 value = read_mem1(SBR1_BASE + offset);
if (value != 0)
return fmt::format("SBR1 area word +{:#x} is {:#x} without a record", offset, value);
}
}
return {};
}
} // namespace StarletBootstrapProbe
@@ -5,6 +5,7 @@
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
@@ -17,9 +18,14 @@
#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
{
using namespace StarletBootstrapProbe;
enum class ExpectedOutcome
{
Idle,
@@ -32,21 +38,8 @@ enum class ExpectedOutcome
// Exit status of a run stopped by a Dolphin alert (1: failed expectation, 2: usage).
constexpr int ALERT_EXIT_STATUS = 3;
// StarOS's SBR1 boot record in MEM1 (StarOS crates/boot-contract, report.rs).
enum class SBR1Mode
{
None,
// --sbr1-record: create the record as the loader does, report it after the run.
Record,
// --sbr1-absent: no record; report the area after the run.
Absent,
};
constexpr u32 SBR1_BASE = 0x00200000;
constexpr u32 SBR1_MAGIC = 0x53425231;
constexpr u32 SBR1_VERSION = 1;
constexpr u32 SBR1_SIZE = 0x1000;
constexpr u32 SBR1_EXI_STARLET = 2;
constexpr u32 SBR1_RELAY_JUMPING = 4;
// Slices run once a StarOS panic path looks finished, to see that it stays so.
constexpr int PANIC_SETTLE_SLICES = 2;
// Dolphin's default handler shows an alert in a modal MessageBox on Windows, which blocks the
// probe until someone closes it. Report the alert on stderr and fail the run at once instead.
@@ -57,7 +50,8 @@ bool FailOnAlert(const char* caption, const char* text, bool, Common::MsgType)
std::_Exit(ALERT_EXIT_STATUS);
}
bool Run(const std::string& path, bool jit, bool mmu_faults, ExpectedOutcome expected)
bool Run(const std::string& path, bool jit, bool mmu_faults, SBR1Mode sbr1,
ExpectedOutcome expected, u64 cycles)
{
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file || file.tellg() <= 0 || file.tellg() > 16 * 1024 * 1024)
@@ -88,6 +82,8 @@ bool Run(const std::string& path, bool jit, bool mmu_faults, ExpectedOutcome exp
}
IOS::LLE::ARMCore core(memory);
// Declared after memory, so destroyed first.
memory.AttachInterruptLines(&core);
core.SetJitEnabled(jit);
core.Reset(info.entry);
core.SetBigEndian(true);
@@ -95,11 +91,31 @@ bool Run(const std::string& path, bool jit, bool mmu_faults, ExpectedOutcome exp
core.SetMMUFaultsEnabled(mmu_faults);
constexpr u32 TRACE_MAGIC = 0x52455631;
constexpr u32 TRACE_CAPACITY = 1024;
for (int slice = 0; slice < 512; ++slice)
auto& mem1 = system.GetMemory();
const auto read_mem1 = [&mem1](u32 address) { return mem1.Read_U32(address); };
const auto panic_unmet = [&] {
return UnmetPanicCondition(memory.Read32(info.panic_state), memory.Read32(info.panic_code),
sbr1, read_mem1);
};
// StarOS's panic path writes no trace line. Print the trace published so far, for the
// scenario's ordered checks, and the panic words.
const auto report_panic = [&] {
const u32 length = memory.Read32(info.trace) == TRACE_MAGIC ?
std::min(memory.Read32(info.trace + 4), TRACE_CAPACITY) :
0;
for (u32 index = 0; index < length; ++index)
std::cout << static_cast<char>(memory.Read8(info.trace + 8 + index));
std::cout << "PC=0x" << std::hex << core.GetRegister(15) << std::dec
<< " instructions=" << core.GetExecutedInstructions() << '\n';
std::cerr << "Starlet panic state " << memory.Read32(info.panic_state) << " code "
<< memory.Read32(info.panic_code) << '\n';
};
int panic_settled = 0;
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)
{
@@ -109,21 +125,25 @@ bool Run(const std::string& path, bool jit, bool mmu_faults, ExpectedOutcome exp
return expected == ExpectedOutcome::UndefinedFault && fault_code == 1 &&
memory.Read32(info.fault_lr) != 0 && memory.Read32(info.fault_spsr) != 0;
}
// StarOS's panic path writes no trace line: it sets its guard word, then its code, and halts.
// The trace published so far is printed for the scenario's ordered checks.
// StarOS's panic path sets its guard word first, then its code, then publishes the SBR1
// record when there is one, and halts. An expected panic passes once all of that holds
// (UnmetPanicCondition) and still holds PANIC_SETTLE_SLICES slices later.
if (info.panic_state != 0 && memory.Read32(info.panic_state) != 0)
{
const u32 panic_state = memory.Read32(info.panic_state);
const u32 panic_code = memory.Read32(info.panic_code);
const u32 length = memory.Read32(info.trace) == TRACE_MAGIC ?
std::min(memory.Read32(info.trace + 4), TRACE_CAPACITY) :
0;
for (u32 index = 0; index < length; ++index)
std::cout << static_cast<char>(memory.Read8(info.trace + 8 + index));
std::cout << "PC=0x" << std::hex << core.GetRegister(15) << std::dec
<< " instructions=" << core.GetExecutedInstructions() << '\n';
std::cerr << "Starlet panic state " << panic_state << " code " << panic_code << '\n';
return expected == ExpectedOutcome::Panic && panic_state == 1;
if (expected != ExpectedOutcome::Panic)
{
report_panic();
return false;
}
if (!panic_unmet().empty())
{
panic_settled = 0;
continue;
}
if (++panic_settled <= PANIC_SETTLE_SLICES)
continue;
report_panic();
return true;
}
if (memory.Read32(info.trace) != TRACE_MAGIC)
continue;
@@ -174,7 +194,16 @@ bool Run(const std::string& path, bool jit, bool mmu_faults, ExpectedOutcome exp
expected == ExpectedOutcome::Idle && trace.find("REV:BOOT:IDLE\n") != std::string::npos;
return startup_ok && outcome_ok;
}
std::cerr << "No terminal bootstrap marker after 2097152 ARM cycles, PC=0x" << std::hex
if (info.panic_state != 0 && memory.Read32(info.panic_state) != 0)
{
report_panic();
const std::string unmet = panic_unmet();
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 " << cycles << " ARM cycles, PC=0x" << std::hex
<< core.GetRegister(15) << std::dec << '\n';
return false;
}
@@ -183,11 +212,12 @@ bool Run(const std::string& path, bool jit, bool mmu_faults, ExpectedOutcome exp
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;
@@ -195,10 +225,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;
@@ -246,25 +288,37 @@ int main(int argc, char** argv)
// channel 1 given to the Starlet, the magic written last.
memory.Write_U32(SBR1_VERSION, SBR1_BASE + 0x04);
memory.Write_U32(SBR1_SIZE, SBR1_BASE + 0x08);
memory.Write_U32(SBR1_EXI_STARLET, SBR1_BASE + 0x0c);
memory.Write_U32(SBR1_RELAY_JUMPING, SBR1_BASE + 0x10);
memory.Write_U32(SBR1_EXI_STARLET, SBR1_BASE + SBR1_EXI_OWNER);
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, 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.
// The record's summary after the run; with --sbr1-absent every word must still be zero (an
// expected panic fails otherwise).
std::cerr << std::hex << "SBR1 magic 0x" << memory.Read_U32(SBR1_BASE) << std::dec
<< " state " << memory.Read_U32(SBR1_BASE + 0x18) << " reason "
<< memory.Read_U32(SBR1_BASE + 0x1c) << " owner "
<< memory.Read_U32(SBR1_BASE + 0x0c) << " fault "
<< memory.Read_U32(SBR1_BASE + 0x20) << " ring "
<< memory.Read_U32(SBR1_BASE + 0x2c) << " dropped "
<< memory.Read_U32(SBR1_BASE + 0x30);
<< " state " << memory.Read_U32(SBR1_BASE + SBR1_STARLET_STATE) << " reason "
<< memory.Read_U32(SBR1_BASE + SBR1_FATAL_REASON) << " owner "
<< memory.Read_U32(SBR1_BASE + SBR1_EXI_OWNER) << " fault "
<< memory.Read_U32(SBR1_BASE + SBR1_FAULT_CODE) << " ring "
<< memory.Read_U32(SBR1_BASE + SBR1_RING_LENGTH) << " dropped "
<< memory.Read_U32(SBR1_BASE + SBR1_RING_DROPPED);
u32 written = 0;
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;