From d486779f50ec8bea72bd20e4bb8769db82f7fb2c Mon Sep 17 00:00:00 2001 From: Yaya48 Date: Fri, 16 Oct 2026 02:19:55 +0200 Subject: [PATCH] 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 --- Source/Core/Core/IOS/Starlet/ARMCore.h | 2 + Source/Core/Core/IOS/Starlet/Starlet.cpp | 2 + .../Core/Core/IOS/Starlet/StarletMemory.cpp | 18 +++++++ Source/Core/Core/IOS/Starlet/StarletMemory.h | 10 ++++ .../Core/IOS/Starlet/ARMCoreTest.cpp | 47 +++++++++++++++++++ .../StarletBootstrapProbe.cpp | 2 + 6 files changed, 81 insertions(+) diff --git a/Source/Core/Core/IOS/Starlet/ARMCore.h b/Source/Core/Core/IOS/Starlet/ARMCore.h index b4cdc1ca13..cd4b55f52f 100644 --- a/Source/Core/Core/IOS/Starlet/ARMCore.h +++ b/Source/Core/Core/IOS/Starlet/ARMCore.h @@ -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 { diff --git a/Source/Core/Core/IOS/Starlet/Starlet.cpp b/Source/Core/Core/IOS/Starlet/Starlet.cpp index ad29974df0..b70dd27f80 100644 --- a/Source/Core/Core/IOS/Starlet/Starlet.cpp +++ b/Source/Core/Core/IOS/Starlet/Starlet.cpp @@ -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(*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 diff --git a/Source/Core/Core/IOS/Starlet/StarletMemory.cpp b/Source/Core/Core/IOS/Starlet/StarletMemory.cpp index a0843771ec..bc18a84f0f 100644 --- a/Source/Core/Core/IOS/Starlet/StarletMemory.cpp +++ b/Source/Core/Core/IOS/Starlet/StarletMemory.cpp @@ -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(); diff --git a/Source/Core/Core/IOS/Starlet/StarletMemory.h b/Source/Core/Core/IOS/Starlet/StarletMemory.h index fc814effa2..bba6369e1a 100644 --- a/Source/Core/Core/IOS/Starlet/StarletMemory.h +++ b/Source/Core/Core/IOS/Starlet/StarletMemory.h @@ -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; diff --git a/Source/UnitTests/Core/IOS/Starlet/ARMCoreTest.cpp b/Source/UnitTests/Core/IOS/Starlet/ARMCoreTest.cpp index 903f3da7eb..d83adf071f 100644 --- a/Source/UnitTests/Core/IOS/Starlet/ARMCoreTest.cpp +++ b/Source/UnitTests/Core/IOS/Starlet/ARMCoreTest.cpp @@ -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(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. diff --git a/Tools/StarletBootstrapProbe/StarletBootstrapProbe.cpp b/Tools/StarletBootstrapProbe/StarletBootstrapProbe.cpp index 989572a648..bf228fb6d4 100644 --- a/Tools/StarletBootstrapProbe/StarletBootstrapProbe.cpp +++ b/Tools/StarletBootstrapProbe/StarletBootstrapProbe.cpp @@ -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);