Compare commits
7
Commits
15da28ad7b
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd2336f278 | ||
|
|
6c44367b5c | ||
|
|
d486779f50 | ||
|
|
bef2c8cf02 | ||
|
|
b457eb18c3 | ||
|
|
6ede30f99e | ||
|
|
a62c65615a |
No files matched your search
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user