Compare commits

...
2 Commits
Author SHA1 Message Date
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
6 changed files with 150 additions and 0 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
{
+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,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;
@@ -1104,6 +1107,23 @@ 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());
}
u32 StarletMemory::GetTimer() const
{
// Hollywood's timer runs at Starlet / 128 (1.8984375 MHz at 243 MHz).
@@ -4147,6 +4167,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 +4250,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, 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,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,118 @@ 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(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.
@@ -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);