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]>
This commit is contained in:
Yaya48andClaude Opus 5.5 committed 2026-10-16 02:19:55 +02:00
1 parent bef2c8cf02
commit d486779f50
6 files changed
+81

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
{
+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
@@ -333,6 +333,8 @@ 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_GPIO_ENABLE = HW_BASE + 0xdc;
constexpr u32 HW_GPIO_OUT = HW_BASE + 0xe0;
constexpr u32 HW_GPIO_DIR = HW_BASE + 0xe4;
@@ -1104,6 +1106,20 @@ 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))
return;
const WiiIPC& ipc = m_system.GetWiiIPC();
m_interrupt_core->SetIRQLine(ipc.IsStarletIRQAsserted());
m_interrupt_core->SetFIQLine(ipc.IsStarletFIQAsserted());
}
u32 StarletMemory::GetTimer() const
{
// Hollywood's timer runs at Starlet / 128 (1.8984375 MHz at 243 MHz).
@@ -4147,6 +4163,7 @@ void StarletMemory::Write8(u32 address, u8 value)
word_address == HW_AHBPROT || word_address == HW_RESETS)
{
m_system.GetWiiIPC().WriteStarletRegister(word_address - HW_BASE, word);
UpdateInterruptLines(word_address);
if (word_address == HW_BASE + 0x0c && (word & 0x09) != 0)
{
// The physical CPUs are concurrent. Give Broadway a scheduling boundary
@@ -4229,6 +4246,7 @@ void StarletMemory::Write32(u32 address, u32 value)
{
WriteRegister(address, value);
m_system.GetWiiIPC().WriteStarletRegister(address - HW_BASE, value);
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 or HW_ARMIRQMASK 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,9 @@ public:
private:
// Synthetic NAND fixtures exercise startup pairing without any console dumps or keys.
friend class StarletPairingTest;
void UpdateInterruptLines(u32 address);
ARMCore* m_interrupt_core = nullptr;
friend class StarletDMATest;
static constexpr u32 NAND_PAGE_DATA_SIZE = 0x800;
@@ -568,6 +568,53 @@ 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, DriveAndResetUseDistinctHollywoodLines)
{
// Hollywood IRQ numbers, not the Broadway Processor Interface's DVD line.
@@ -82,6 +82,8 @@ bool Run(const std::string& path, bool jit, bool mmu_faults, SBR1Mode sbr1,
}
IOS::LLE::ARMCore core(memory);
// Declared after memory, so destroyed first.
memory.AttachInterruptLines(&core);
core.SetJitEnabled(jit);
core.Reset(info.entry);
core.SetBigEndian(true);