diff --git a/Source/Core/Core/IOS/Starlet/StarletMemory.cpp b/Source/Core/Core/IOS/Starlet/StarletMemory.cpp index 5ac67fe09c..448342369f 100644 --- a/Source/Core/Core/IOS/Starlet/StarletMemory.cpp +++ b/Source/Core/Core/IOS/Starlet/StarletMemory.cpp @@ -867,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 @@ -1071,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); @@ -1124,6 +1126,19 @@ void StarletMemory::UpdateInterruptLines(u32 address) 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). @@ -1154,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) { @@ -4119,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) @@ -4167,6 +4187,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); + KeepMatchedAlarmAsserted(word_address); UpdateInterruptLines(word_address); if (word_address == HW_BASE + 0x0c && (word & 0x09) != 0) { @@ -4250,6 +4271,7 @@ 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) diff --git a/Source/Core/Core/IOS/Starlet/StarletMemory.h b/Source/Core/Core/IOS/Starlet/StarletMemory.h index 039c194971..85e0ec1a5e 100644 --- a/Source/Core/Core/IOS/Starlet/StarletMemory.h +++ b/Source/Core/Core/IOS/Starlet/StarletMemory.h @@ -119,6 +119,7 @@ private: friend class StarletPairingTest; void UpdateInterruptLines(u32 address); + void KeepMatchedAlarmAsserted(u32 address); ARMCore* m_interrupt_core = nullptr; friend class StarletDMATest; @@ -357,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> m_watch_ranges; diff --git a/Source/Core/Core/State.cpp b/Source/Core/Core/State.cpp index 7ba8a51965..4e410d0efb 100644 --- a/Source/Core/Core/State.cpp +++ b/Source/Core/Core/State.cpp @@ -97,7 +97,7 @@ struct CompressAndDumpStateArgs static Common::WorkQueueThreadSP 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 diff --git a/Source/UnitTests/Core/IOS/Starlet/ARMCoreTest.cpp b/Source/UnitTests/Core/IOS/Starlet/ARMCoreTest.cpp index 32b3127409..365ba1de03 100644 --- a/Source/UnitTests/Core/IOS/Starlet/ARMCoreTest.cpp +++ b/Source/UnitTests/Core/IOS/Starlet/ARMCoreTest.cpp @@ -649,6 +649,96 @@ TEST(StarletInterrupts, FIQMaskWritesResampleTheAttachedCoreLine) 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(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 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;