Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd2336f278 | ||
|
|
6c44367b5c | ||
|
|
d486779f50 | ||
|
|
bef2c8cf02 | ||
|
|
b457eb18c3 | ||
|
|
6ede30f99e | ||
|
|
a62c65615a | ||
|
|
15da28ad7b | ||
|
|
2bae49bdad | ||
|
|
612cab7b30 | ||
|
|
cdec826bb8 | ||
|
|
9b493a1e16 |
No files matched your search
@@ -26,8 +26,29 @@ namespace
|
|||||||
{
|
{
|
||||||
constexpr u32 CP15_CONTROL_BIG_ENDIAN = 1U << 7;
|
constexpr u32 CP15_CONTROL_BIG_ENDIAN = 1U << 7;
|
||||||
constexpr u32 CP15_CONTROL_MMU = 1U << 0;
|
constexpr u32 CP15_CONTROL_MMU = 1U << 0;
|
||||||
|
constexpr u32 CP15_CONTROL_ALIGNMENT = 1U << 1;
|
||||||
|
constexpr u32 CP15_CONTROL_SYSTEM_PROTECTION = 1U << 8;
|
||||||
|
constexpr u32 CP15_CONTROL_ROM_PROTECTION = 1U << 9;
|
||||||
constexpr u32 STARLET_EXCEPTION_VECTOR_BASE = 0xffff0000;
|
constexpr u32 STARLET_EXCEPTION_VECTOR_BASE = 0xffff0000;
|
||||||
|
|
||||||
|
// Fault Status Register status values (ARM926EJ-S TRM, c5). The domain goes in bits 7:4 except
|
||||||
|
// for alignment and section translation faults, whose domain is not valid (written as 0 here).
|
||||||
|
constexpr u32 FSR_ALIGNMENT = 0x1;
|
||||||
|
constexpr u32 FSR_TRANSLATION_SECTION = 0x5;
|
||||||
|
constexpr u32 FSR_TRANSLATION_PAGE = 0x7;
|
||||||
|
constexpr u32 FSR_DOMAIN_SECTION = 0x9;
|
||||||
|
constexpr u32 FSR_DOMAIN_PAGE = 0xb;
|
||||||
|
constexpr u32 FSR_PERMISSION_SECTION = 0xd;
|
||||||
|
constexpr u32 FSR_PERMISSION_PAGE = 0xf;
|
||||||
|
|
||||||
|
// TLBEntry::protection, filled with MMU faults enabled.
|
||||||
|
constexpr u32 PROTECTION_PAGE = 1U << 6;
|
||||||
|
|
||||||
|
constexpr u32 MakeProtection(u32 domain, u32 access_permission, bool page, u32 size_log2)
|
||||||
|
{
|
||||||
|
return domain | (access_permission << 4) | (page ? PROTECTION_PAGE : 0) | (size_log2 << 8);
|
||||||
|
}
|
||||||
|
|
||||||
constexpr u32 ModeValue(ARMCore::Mode mode)
|
constexpr u32 ModeValue(ARMCore::Mode mode)
|
||||||
{
|
{
|
||||||
return static_cast<u32>(mode);
|
return static_cast<u32>(mode);
|
||||||
@@ -205,6 +226,16 @@ u32 ARMCore::TranslateVirtualAddress(u32 address) const
|
|||||||
if ((m_cp15.control & CP15_CONTROL_MMU) == 0)
|
if ((m_cp15.control & CP15_CONTROL_MMU) == 0)
|
||||||
return address;
|
return address;
|
||||||
|
|
||||||
|
if (m_mmu_faults)
|
||||||
|
{
|
||||||
|
// Callers that cannot fault here (a fetch after its permission check, the JIT's decoder)
|
||||||
|
// share the checked TLB. A faulting translation is neither cached nor followed.
|
||||||
|
const u32 modified_address = ModifiedVirtualAddress(address);
|
||||||
|
u32 fault_status = 0;
|
||||||
|
const TLBEntry* const entry = LookupTLBChecked(modified_address, &fault_status);
|
||||||
|
return entry ? entry->physical_page | (modified_address & 0x3ff) : modified_address;
|
||||||
|
}
|
||||||
|
|
||||||
// ARM926 FCSE remaps the low 32 MiB through the process identifier before
|
// ARM926 FCSE remaps the low 32 MiB through the process identifier before
|
||||||
// translation.
|
// translation.
|
||||||
const u32 modified_address =
|
const u32 modified_address =
|
||||||
@@ -235,6 +266,11 @@ u32 ARMCore::TranslateVirtualAddressForJit(u32 address, const u8** first_descrip
|
|||||||
*second_descriptor_value = 0;
|
*second_descriptor_value = 0;
|
||||||
if ((m_cp15.control & CP15_CONTROL_MMU) == 0)
|
if ((m_cp15.control & CP15_CONTROL_MMU) == 0)
|
||||||
return address;
|
return address;
|
||||||
|
// With MMU faults enabled the JIT dispatches every block through ARMJitX64::Dispatch, which has
|
||||||
|
// just checked this fetch. Refilling the entry from a fresh walk here would hide a stale TLB
|
||||||
|
// entry that the guest failed to invalidate, so reuse the checked TLB instead.
|
||||||
|
if (m_mmu_faults)
|
||||||
|
return TranslateVirtualAddress(address);
|
||||||
|
|
||||||
const u32 modified_address =
|
const u32 modified_address =
|
||||||
address < 0x02000000 ? address | (m_cp15.process_id & 0xfe000000) : address;
|
address < 0x02000000 ? address | (m_cp15.process_id & 0xfe000000) : address;
|
||||||
@@ -339,6 +375,272 @@ void ARMCore::InvalidateTLB()
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ARMCore::SetMMUFaultsEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
m_mmu_faults = enabled;
|
||||||
|
m_abort_pending = false;
|
||||||
|
m_user_access = false;
|
||||||
|
m_waiting_for_memory_poll = false;
|
||||||
|
// Translations and translated code cached under the other model must not survive the switch.
|
||||||
|
InvalidateTLB();
|
||||||
|
InvalidateInstructionCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 ARMCore::ModifiedVirtualAddress(u32 address) const
|
||||||
|
{
|
||||||
|
return address < 0x02000000 ? address | (m_cp15.process_id & 0xfe000000) : address;
|
||||||
|
}
|
||||||
|
|
||||||
|
ARMCore::CheckedWalk ARMCore::WalkPageTablesChecked(u32 modified_address) const
|
||||||
|
{
|
||||||
|
// ARM926EJ-S first- and second-level descriptors. Domain and AP come from the descriptor that
|
||||||
|
// maps the address: the first level's domain field, then the AP subpage of a large (16 KiB)
|
||||||
|
// or small (1 KiB) page, or the single AP of a section or tiny page.
|
||||||
|
CheckedWalk walk;
|
||||||
|
const u32 first_level = ReadPhysical32((m_cp15.translation_table_base & 0xffffc000) |
|
||||||
|
((modified_address >> 18) & 0x3ffc));
|
||||||
|
const u32 domain = (first_level >> 5) & 0xf;
|
||||||
|
const auto map_page = [&](u32 second_level_address, bool fine) {
|
||||||
|
const u32 second_level = ReadPhysical32(second_level_address);
|
||||||
|
switch (second_level & 3)
|
||||||
|
{
|
||||||
|
case 1: // 64 KiB large page.
|
||||||
|
walk.physical_address = (second_level & 0xffff0000) | (modified_address & 0xffff);
|
||||||
|
walk.protection = MakeProtection(
|
||||||
|
domain, (second_level >> (4 + 2 * ((modified_address >> 14) & 3))) & 3, true, 16);
|
||||||
|
return;
|
||||||
|
case 2: // 4 KiB small page.
|
||||||
|
walk.physical_address = (second_level & 0xfffff000) | (modified_address & 0xfff);
|
||||||
|
walk.protection = MakeProtection(
|
||||||
|
domain, (second_level >> (4 + 2 * ((modified_address >> 10) & 3))) & 3, true, 12);
|
||||||
|
return;
|
||||||
|
case 3:
|
||||||
|
if (fine) // 1 KiB tiny page.
|
||||||
|
{
|
||||||
|
walk.physical_address = (second_level & 0xfffffc00) | (modified_address & 0x3ff);
|
||||||
|
walk.protection = MakeProtection(domain, (second_level >> 4) & 3, true, 10);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// A coarse table has no tiny pages: fail closed.
|
||||||
|
[[fallthrough]];
|
||||||
|
default:
|
||||||
|
walk.fault_status = FSR_TRANSLATION_PAGE | (domain << 4);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
switch (first_level & 3)
|
||||||
|
{
|
||||||
|
case 1: // Coarse second-level table.
|
||||||
|
map_page((first_level & 0xfffffc00) | ((modified_address >> 10) & 0x3fc), false);
|
||||||
|
break;
|
||||||
|
case 2: // 1 MiB section.
|
||||||
|
walk.physical_address = (first_level & 0xfff00000) | (modified_address & 0x000fffff);
|
||||||
|
walk.protection = MakeProtection(domain, (first_level >> 10) & 3, false, 20);
|
||||||
|
break;
|
||||||
|
case 3: // Fine second-level table.
|
||||||
|
map_page((first_level & 0xfffff000) | ((modified_address >> 8) & 0xffc), true);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
walk.fault_status = FSR_TRANSLATION_SECTION;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return walk;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ARMCore::TLBEntry* ARMCore::LookupTLBChecked(u32 modified_address, u32* fault_status) const
|
||||||
|
{
|
||||||
|
const u32 virtual_page = modified_address >> 10;
|
||||||
|
TLBEntry& entry = m_tlb[GetTLBIndex(virtual_page)];
|
||||||
|
if (entry.generation == m_tlb_generation && entry.virtual_page == virtual_page)
|
||||||
|
return &entry;
|
||||||
|
|
||||||
|
const CheckedWalk walk = WalkPageTablesChecked(modified_address);
|
||||||
|
if (walk.fault_status != 0)
|
||||||
|
{
|
||||||
|
// The TLB never holds a translation that faulted: the next access walks again.
|
||||||
|
*fault_status = walk.fault_status;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
entry.virtual_page = virtual_page;
|
||||||
|
entry.physical_page = walk.physical_address & ~0x3ffU;
|
||||||
|
entry.generation = m_tlb_generation;
|
||||||
|
entry.protection = walk.protection;
|
||||||
|
return &entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 ARMCore::PermissionFault(const TLBEntry& entry, AccessKind kind, bool user) const
|
||||||
|
{
|
||||||
|
// The DACR is read at every access, as on ARM926; AP comes from the TLB entry, so changing a
|
||||||
|
// descriptor's AP takes effect only after the guest invalidates that entry.
|
||||||
|
const u32 domain = entry.protection & 0xf;
|
||||||
|
const bool page = (entry.protection & PROTECTION_PAGE) != 0;
|
||||||
|
switch ((m_cp15.domain_access_control >> (2 * domain)) & 3)
|
||||||
|
{
|
||||||
|
case 3: // Manager: accesses are not checked against AP.
|
||||||
|
return 0;
|
||||||
|
case 1: // Client: AP below.
|
||||||
|
break;
|
||||||
|
default: // No access, and the reserved value 0b10 (fail closed).
|
||||||
|
return (page ? FSR_DOMAIN_PAGE : FSR_DOMAIN_SECTION) | (domain << 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ARMv5 has no execute permission: a fetch needs read permission (no XN, no W^X).
|
||||||
|
const bool write = kind == AccessKind::Write;
|
||||||
|
bool allowed;
|
||||||
|
switch ((entry.protection >> 4) & 3)
|
||||||
|
{
|
||||||
|
case 3: // Read/write for both.
|
||||||
|
allowed = true;
|
||||||
|
break;
|
||||||
|
case 2: // Privileged read/write, User read-only.
|
||||||
|
allowed = !user || !write;
|
||||||
|
break;
|
||||||
|
case 1: // Privileged read/write, User none.
|
||||||
|
allowed = !user;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
// AP 00 depends on the control register: S alone grants privileged reads, R alone grants
|
||||||
|
// reads to both. Neither grants nothing; both together are reserved (fail closed).
|
||||||
|
const bool system = (m_cp15.control & CP15_CONTROL_SYSTEM_PROTECTION) != 0;
|
||||||
|
const bool rom = (m_cp15.control & CP15_CONTROL_ROM_PROTECTION) != 0;
|
||||||
|
allowed = !write && ((system && !rom && !user) || (!system && rom));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allowed ? 0 : (page ? FSR_PERMISSION_PAGE : FSR_PERMISSION_SECTION) | (domain << 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ARMCore::IsUserAccess() const
|
||||||
|
{
|
||||||
|
return GetMode() == Mode::User || m_user_access;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 ARMCore::DataAddress(u32 address, bool write) const
|
||||||
|
{
|
||||||
|
if (!m_mmu_faults)
|
||||||
|
return TranslateVirtualAddress(address);
|
||||||
|
if (m_abort_pending || (m_cp15.control & CP15_CONTROL_MMU) == 0)
|
||||||
|
return address;
|
||||||
|
|
||||||
|
const u32 modified_address = ModifiedVirtualAddress(address);
|
||||||
|
u32 fault_status = 0;
|
||||||
|
const TLBEntry* const entry = LookupTLBChecked(modified_address, &fault_status);
|
||||||
|
if (entry != nullptr)
|
||||||
|
{
|
||||||
|
fault_status =
|
||||||
|
PermissionFault(*entry, write ? AccessKind::Write : AccessKind::Read, IsUserAccess());
|
||||||
|
}
|
||||||
|
if (fault_status != 0)
|
||||||
|
{
|
||||||
|
RaiseDataAbort(fault_status, modified_address);
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
return entry->physical_page | (modified_address & 0x3ff);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ARMCore::RaiseDataAbort(u32 fault_status, u32 modified_address) const
|
||||||
|
{
|
||||||
|
// Only the first fault of an instruction is reported.
|
||||||
|
if (m_abort_pending)
|
||||||
|
return;
|
||||||
|
m_abort_pending = true;
|
||||||
|
m_abort_status = fault_status;
|
||||||
|
m_abort_address = modified_address;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ARMCore::CheckAlignment(u32 address, u32 mask) const
|
||||||
|
{
|
||||||
|
// ARM926 checks alignment before translation, when control bit A is set.
|
||||||
|
if (!m_mmu_faults || (m_cp15.control & CP15_CONTROL_ALIGNMENT) == 0 || (address & mask) == 0)
|
||||||
|
return;
|
||||||
|
const bool mmu = (m_cp15.control & CP15_CONTROL_MMU) != 0;
|
||||||
|
RaiseDataAbort(FSR_ALIGNMENT, mmu ? ModifiedVirtualAddress(address) : address);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ARMCore::TryTranslateFetch(u32 address, u32* physical_address, u32* fault_status) const
|
||||||
|
{
|
||||||
|
*fault_status = 0;
|
||||||
|
if ((m_cp15.control & CP15_CONTROL_MMU) == 0)
|
||||||
|
{
|
||||||
|
*physical_address = address;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const u32 modified_address = ModifiedVirtualAddress(address);
|
||||||
|
const TLBEntry* const entry = LookupTLBChecked(modified_address, fault_status);
|
||||||
|
if (entry != nullptr)
|
||||||
|
*fault_status = PermissionFault(*entry, AccessKind::Fetch, GetMode() == Mode::User);
|
||||||
|
if (*fault_status != 0)
|
||||||
|
return false;
|
||||||
|
*physical_address = entry->physical_page | (modified_address & 0x3ff);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ARMCore::EnterPrefetchAbortIfFaulted(u32 address)
|
||||||
|
{
|
||||||
|
u32 physical_address = 0;
|
||||||
|
u32 fault_status = 0;
|
||||||
|
if (TryTranslateFetch(address, &physical_address, &fault_status))
|
||||||
|
return false;
|
||||||
|
// A prefetch abort sets the IFSR only. The FAR keeps the address of the last data abort; the
|
||||||
|
// handler finds the faulting instruction from LR, which is its address + 4 in both states.
|
||||||
|
m_instruction_fault_status = fault_status;
|
||||||
|
EnterException(Mode::Abort, STARLET_EXCEPTION_VECTOR_BASE + 0x0c, address + 4);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ARMCore::ExecuteARMWithAborts(u32 instruction)
|
||||||
|
{
|
||||||
|
const std::array<u32, 16> registers = m_registers;
|
||||||
|
const std::array<u32, 7> user_registers = m_user_r8_r14;
|
||||||
|
const u32 cpsr = m_cpsr;
|
||||||
|
ExecuteARM(instruction);
|
||||||
|
if (!m_abort_pending)
|
||||||
|
return;
|
||||||
|
// Undo the instruction's register results: an LDM^ with PC may also have restored the CPSR
|
||||||
|
// after its loads. Memory it wrote before the fault stays written, as on ARM926.
|
||||||
|
m_abort_pending = false;
|
||||||
|
if (m_cpsr != cpsr)
|
||||||
|
SetCPSR(cpsr);
|
||||||
|
m_registers = registers;
|
||||||
|
m_user_r8_r14 = user_registers;
|
||||||
|
m_cp15.fault_status = m_abort_status;
|
||||||
|
m_cp15.fault_address = m_abort_address;
|
||||||
|
// A data abort returns to the aborted instruction + 8 in both states.
|
||||||
|
EnterException(Mode::Abort, STARLET_EXCEPTION_VECTOR_BASE + 0x10, m_instruction_address + 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ARMCore::ExecuteThumbWithAborts(u16 instruction)
|
||||||
|
{
|
||||||
|
const std::array<u32, 16> registers = m_registers;
|
||||||
|
const u32 cpsr = m_cpsr;
|
||||||
|
ExecuteThumb(instruction);
|
||||||
|
if (!m_abort_pending)
|
||||||
|
return;
|
||||||
|
m_abort_pending = false;
|
||||||
|
if (m_cpsr != cpsr)
|
||||||
|
SetCPSR(cpsr);
|
||||||
|
m_registers = registers;
|
||||||
|
m_cp15.fault_status = m_abort_status;
|
||||||
|
m_cp15.fault_address = m_abort_address;
|
||||||
|
EnterException(Mode::Abort, STARLET_EXCEPTION_VECTOR_BASE + 0x10, m_instruction_address + 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ARMCore::InvalidateTLBEntry(u32 modified_address)
|
||||||
|
{
|
||||||
|
// ARM926 drops the one entry that maps this MVA, whatever the size of its page. The software TLB
|
||||||
|
// holds that page as 1 KiB granules: drop each of them, and nothing else.
|
||||||
|
for (TLBEntry& entry : m_tlb)
|
||||||
|
{
|
||||||
|
if (entry.generation != m_tlb_generation)
|
||||||
|
continue;
|
||||||
|
const u32 size_log2 = (entry.protection >> 8) & 0xff;
|
||||||
|
const u32 mask = ~((1U << size_log2) - 1);
|
||||||
|
if (((entry.virtual_page << 10) & mask) == (modified_address & mask))
|
||||||
|
entry.generation = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
u32 ARMCore::FetchARMInstruction(u32 address)
|
u32 ARMCore::FetchARMInstruction(u32 address)
|
||||||
{
|
{
|
||||||
const u32 physical_address = TranslateVirtualAddress(address);
|
const u32 physical_address = TranslateVirtualAddress(address);
|
||||||
@@ -391,35 +693,61 @@ void ARMCore::InvalidateInstructionCache()
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The data accessors translate through DataAddress. Without MMU faults that is the plain
|
||||||
|
// translation and m_abort_pending is never set. With them, a fault records the instruction's
|
||||||
|
// data abort and the bus is not touched by this or any later access of that instruction.
|
||||||
u8 ARMCore::ReadByte(u32 address) const
|
u8 ARMCore::ReadByte(u32 address) const
|
||||||
{
|
{
|
||||||
return m_bus.Read8(TranslateVirtualAddress(address));
|
const u32 physical_address = DataAddress(address, false);
|
||||||
|
return m_abort_pending ? 0 : m_bus.Read8(physical_address);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ARMCore::WriteByte(u32 address, u8 value)
|
void ARMCore::WriteByte(u32 address, u8 value)
|
||||||
{
|
{
|
||||||
m_bus.Write8(TranslateVirtualAddress(address), value);
|
const u32 physical_address = DataAddress(address, true);
|
||||||
|
if (!m_abort_pending)
|
||||||
|
m_bus.Write8(physical_address, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
u16 ARMCore::Read16(u32 address) const
|
u16 ARMCore::Read16(u32 address) const
|
||||||
{
|
{
|
||||||
const u16 value =
|
const auto read8 = [this](u32 byte_address) -> u16 {
|
||||||
(address & 1) == 0 ?
|
const u32 physical_address = DataAddress(byte_address, false);
|
||||||
m_bus.Read16(TranslateVirtualAddress(address)) :
|
return m_abort_pending ? 0 : m_bus.Read8(physical_address);
|
||||||
static_cast<u16>((static_cast<u16>(m_bus.Read8(TranslateVirtualAddress(address))) << 8) |
|
};
|
||||||
m_bus.Read8(TranslateVirtualAddress(address + 1)));
|
u16 value;
|
||||||
|
if ((address & 1) == 0)
|
||||||
|
{
|
||||||
|
const u32 physical_address = DataAddress(address, false);
|
||||||
|
value = m_abort_pending ? 0 : m_bus.Read16(physical_address);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const u16 high = read8(address);
|
||||||
|
value = static_cast<u16>((high << 8) | read8(address + 1));
|
||||||
|
}
|
||||||
return m_big_endian ? value : static_cast<u16>((value >> 8) | (value << 8));
|
return m_big_endian ? value : static_cast<u16>((value >> 8) | (value << 8));
|
||||||
}
|
}
|
||||||
|
|
||||||
u32 ARMCore::Read32(u32 address) const
|
u32 ARMCore::Read32(u32 address) const
|
||||||
{
|
{
|
||||||
const u32 value =
|
const auto read8 = [this](u32 byte_address) -> u32 {
|
||||||
(address & 3) == 0 ?
|
const u32 physical_address = DataAddress(byte_address, false);
|
||||||
m_bus.Read32(TranslateVirtualAddress(address)) :
|
return m_abort_pending ? 0 : m_bus.Read8(physical_address);
|
||||||
(static_cast<u32>(m_bus.Read8(TranslateVirtualAddress(address))) << 24) |
|
};
|
||||||
(static_cast<u32>(m_bus.Read8(TranslateVirtualAddress(address + 1))) << 16) |
|
u32 value;
|
||||||
(static_cast<u32>(m_bus.Read8(TranslateVirtualAddress(address + 2))) << 8) |
|
if ((address & 3) == 0)
|
||||||
m_bus.Read8(TranslateVirtualAddress(address + 3));
|
{
|
||||||
|
const u32 physical_address = DataAddress(address, false);
|
||||||
|
value = m_abort_pending ? 0 : m_bus.Read32(physical_address);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const u32 byte0 = read8(address);
|
||||||
|
const u32 byte1 = read8(address + 1);
|
||||||
|
const u32 byte2 = read8(address + 2);
|
||||||
|
value = (byte0 << 24) | (byte1 << 16) | (byte2 << 8) | read8(address + 3);
|
||||||
|
}
|
||||||
if (m_big_endian)
|
if (m_big_endian)
|
||||||
return value;
|
return value;
|
||||||
return (value >> 24) | ((value >> 8) & 0x0000ff00) | ((value << 8) & 0x00ff0000) | (value << 24);
|
return (value >> 24) | ((value >> 8) & 0x0000ff00) | ((value << 8) & 0x00ff0000) | (value << 24);
|
||||||
@@ -431,11 +759,13 @@ void ARMCore::Write16(u32 address, u16 value)
|
|||||||
value = static_cast<u16>((value >> 8) | (value << 8));
|
value = static_cast<u16>((value >> 8) | (value << 8));
|
||||||
if ((address & 1) == 0)
|
if ((address & 1) == 0)
|
||||||
{
|
{
|
||||||
m_bus.Write16(TranslateVirtualAddress(address), value);
|
const u32 physical_address = DataAddress(address, true);
|
||||||
|
if (!m_abort_pending)
|
||||||
|
m_bus.Write16(physical_address, value);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
m_bus.Write8(TranslateVirtualAddress(address), static_cast<u8>(value >> 8));
|
WriteByte(address, static_cast<u8>(value >> 8));
|
||||||
m_bus.Write8(TranslateVirtualAddress(address + 1), static_cast<u8>(value));
|
WriteByte(address + 1, static_cast<u8>(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ARMCore::Write32(u32 address, u32 value)
|
void ARMCore::Write32(u32 address, u32 value)
|
||||||
@@ -447,13 +777,15 @@ void ARMCore::Write32(u32 address, u32 value)
|
|||||||
}
|
}
|
||||||
if ((address & 3) == 0)
|
if ((address & 3) == 0)
|
||||||
{
|
{
|
||||||
m_bus.Write32(TranslateVirtualAddress(address), value);
|
const u32 physical_address = DataAddress(address, true);
|
||||||
|
if (!m_abort_pending)
|
||||||
|
m_bus.Write32(physical_address, value);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
m_bus.Write8(TranslateVirtualAddress(address), static_cast<u8>(value >> 24));
|
WriteByte(address, static_cast<u8>(value >> 24));
|
||||||
m_bus.Write8(TranslateVirtualAddress(address + 1), static_cast<u8>(value >> 16));
|
WriteByte(address + 1, static_cast<u8>(value >> 16));
|
||||||
m_bus.Write8(TranslateVirtualAddress(address + 2), static_cast<u8>(value >> 8));
|
WriteByte(address + 2, static_cast<u8>(value >> 8));
|
||||||
m_bus.Write8(TranslateVirtualAddress(address + 3), static_cast<u8>(value));
|
WriteByte(address + 3, static_cast<u8>(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
u32 ARMCore::ReadRegisterOperand(u32 index) const
|
u32 ARMCore::ReadRegisterOperand(u32 index) const
|
||||||
@@ -769,11 +1101,23 @@ int ARMCore::StepInternal(bool advance_bus)
|
|||||||
|
|
||||||
m_instruction_address = m_registers[15];
|
m_instruction_address = m_registers[15];
|
||||||
m_pc_written = false;
|
m_pc_written = false;
|
||||||
if (m_cpsr & CPSR_T)
|
if (m_mmu_faults && EnterPrefetchAbortIfFaulted(m_instruction_address))
|
||||||
|
{
|
||||||
|
// The instruction is not executed. Idle-loop fast-forwarding is off with MMU faults: its
|
||||||
|
// polls translate without permission checks.
|
||||||
|
}
|
||||||
|
else if (m_cpsr & CPSR_T)
|
||||||
{
|
{
|
||||||
const u16 instruction = FetchThumbInstruction(m_instruction_address);
|
const u16 instruction = FetchThumbInstruction(m_instruction_address);
|
||||||
ExecuteThumb(instruction);
|
if (m_mmu_faults)
|
||||||
TryEnterThumbMemoryPoll(instruction);
|
{
|
||||||
|
ExecuteThumbWithAborts(instruction);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ExecuteThumb(instruction);
|
||||||
|
TryEnterThumbMemoryPoll(instruction);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -790,9 +1134,13 @@ int ARMCore::StepInternal(bool advance_bus)
|
|||||||
}
|
}
|
||||||
else if (ConditionPassed(instruction >> 28))
|
else if (ConditionPassed(instruction >> 28))
|
||||||
{
|
{
|
||||||
ExecuteARM(instruction);
|
if (m_mmu_faults)
|
||||||
|
ExecuteARMWithAborts(instruction);
|
||||||
|
else
|
||||||
|
ExecuteARM(instruction);
|
||||||
}
|
}
|
||||||
TryEnterARMSliceStablePoll(instruction);
|
if (!m_mmu_faults)
|
||||||
|
TryEnterARMSliceStablePoll(instruction);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!m_pc_written)
|
if (!m_pc_written)
|
||||||
@@ -1460,6 +1808,7 @@ void ARMCore::ExecuteARM(u32 instruction)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
CheckAlignment(address, 3);
|
||||||
const u32 old = Read32(address & ~3U);
|
const u32 old = Read32(address & ~3U);
|
||||||
Write32(address & ~3U, ReadRegisterOperand(rm));
|
Write32(address & ~3U, ReadRegisterOperand(rm));
|
||||||
m_registers[rd] = old;
|
m_registers[rd] = old;
|
||||||
@@ -1734,6 +2083,10 @@ void ARMCore::ExecuteThumb(u16 instruction)
|
|||||||
const u32 rb = (instruction >> 3) & 7;
|
const u32 rb = (instruction >> 3) & 7;
|
||||||
const u32 rd = instruction & 7;
|
const u32 rd = instruction & 7;
|
||||||
const u32 address = m_registers[rb] + m_registers[ro];
|
const u32 address = m_registers[rb] + m_registers[ro];
|
||||||
|
if (operation == 0 || operation == 4)
|
||||||
|
CheckAlignment(address, 3);
|
||||||
|
else if (operation == 1 || operation == 5 || operation == 7)
|
||||||
|
CheckAlignment(address, 1);
|
||||||
switch (operation)
|
switch (operation)
|
||||||
{
|
{
|
||||||
case 0: // STR
|
case 0: // STR
|
||||||
@@ -1773,6 +2126,8 @@ void ARMCore::ExecuteThumb(u16 instruction)
|
|||||||
const u32 rb = (instruction >> 3) & 7;
|
const u32 rb = (instruction >> 3) & 7;
|
||||||
const u32 rd = instruction & 7;
|
const u32 rd = instruction & 7;
|
||||||
const u32 address = m_registers[rb] + offset;
|
const u32 address = m_registers[rb] + offset;
|
||||||
|
if (!byte)
|
||||||
|
CheckAlignment(address, 3);
|
||||||
if (load)
|
if (load)
|
||||||
{
|
{
|
||||||
m_registers[rd] = byte ? ReadByte(address) : Read32(address);
|
m_registers[rd] = byte ? ReadByte(address) : Read32(address);
|
||||||
@@ -1794,6 +2149,7 @@ void ARMCore::ExecuteThumb(u16 instruction)
|
|||||||
const bool load = (instruction & 0x0800) != 0;
|
const bool load = (instruction & 0x0800) != 0;
|
||||||
const u32 address = m_registers[(instruction >> 3) & 7] + (((instruction >> 6) & 0x1f) << 1);
|
const u32 address = m_registers[(instruction >> 3) & 7] + (((instruction >> 6) & 0x1f) << 1);
|
||||||
const u32 rd = instruction & 7;
|
const u32 rd = instruction & 7;
|
||||||
|
CheckAlignment(address, 1);
|
||||||
if (load)
|
if (load)
|
||||||
m_registers[rd] = Read16(address);
|
m_registers[rd] = Read16(address);
|
||||||
else
|
else
|
||||||
@@ -1807,6 +2163,7 @@ void ARMCore::ExecuteThumb(u16 instruction)
|
|||||||
const bool load = (instruction & 0x0800) != 0;
|
const bool load = (instruction & 0x0800) != 0;
|
||||||
const u32 rd = (instruction >> 8) & 7;
|
const u32 rd = (instruction >> 8) & 7;
|
||||||
const u32 address = m_registers[13] + ((instruction & 0xff) << 2);
|
const u32 address = m_registers[13] + ((instruction & 0xff) << 2);
|
||||||
|
CheckAlignment(address, 3);
|
||||||
if (load)
|
if (load)
|
||||||
m_registers[rd] = Read32(address);
|
m_registers[rd] = Read32(address);
|
||||||
else
|
else
|
||||||
@@ -1851,6 +2208,7 @@ void ARMCore::ExecuteThumb(u16 instruction)
|
|||||||
if (!pop)
|
if (!pop)
|
||||||
{
|
{
|
||||||
u32 address = m_registers[13] - count * 4;
|
u32 address = m_registers[13] - count * 4;
|
||||||
|
CheckAlignment(address, 3);
|
||||||
m_registers[13] = address;
|
m_registers[13] = address;
|
||||||
for (u32 reg = 0; reg < 8; ++reg)
|
for (u32 reg = 0; reg < 8; ++reg)
|
||||||
{
|
{
|
||||||
@@ -1866,6 +2224,7 @@ void ARMCore::ExecuteThumb(u16 instruction)
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
u32 address = m_registers[13];
|
u32 address = m_registers[13];
|
||||||
|
CheckAlignment(address, 3);
|
||||||
for (u32 reg = 0; reg < 8; ++reg)
|
for (u32 reg = 0; reg < 8; ++reg)
|
||||||
{
|
{
|
||||||
if (register_list & (1U << reg))
|
if (register_list & (1U << reg))
|
||||||
@@ -1903,6 +2262,7 @@ void ARMCore::ExecuteThumb(u16 instruction)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
u32 address = m_registers[rb];
|
u32 address = m_registers[rb];
|
||||||
|
CheckAlignment(address, 3);
|
||||||
for (u32 reg = 0; reg < 8; ++reg)
|
for (u32 reg = 0; reg < 8; ++reg)
|
||||||
{
|
{
|
||||||
if (!(register_list & (1U << reg)))
|
if (!(register_list & (1U << reg)))
|
||||||
@@ -2121,6 +2481,10 @@ void ARMCore::ExecuteSingleDataTransfer(u32 instruction)
|
|||||||
const u32 base = ReadRegisterOperand(rn);
|
const u32 base = ReadRegisterOperand(rn);
|
||||||
const u32 indexed = add_offset ? base + offset : base - offset;
|
const u32 indexed = add_offset ? base + offset : base - offset;
|
||||||
const u32 address = preindex ? indexed : base;
|
const u32 address = preindex ? indexed : base;
|
||||||
|
if (!byte)
|
||||||
|
CheckAlignment(address, 3);
|
||||||
|
// LDRT/STRT/LDRBT/STRBT (post-indexed with W set) check User permissions in any mode.
|
||||||
|
m_user_access = !preindex && writeback;
|
||||||
if (load)
|
if (load)
|
||||||
{
|
{
|
||||||
u32 value;
|
u32 value;
|
||||||
@@ -2147,6 +2511,7 @@ void ARMCore::ExecuteSingleDataTransfer(u32 instruction)
|
|||||||
else
|
else
|
||||||
Write32(address & ~3U, value);
|
Write32(address & ~3U, value);
|
||||||
}
|
}
|
||||||
|
m_user_access = false;
|
||||||
|
|
||||||
if ((!preindex || writeback) && !(load && rn == rd))
|
if ((!preindex || writeback) && !(load && rn == rd))
|
||||||
m_registers[rn] = indexed;
|
m_registers[rn] = indexed;
|
||||||
@@ -2203,6 +2568,8 @@ void ARMCore::ExecuteHalfwordDataTransfer(u32 instruction)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type == 1 || type == 3)
|
||||||
|
CheckAlignment(address, 1);
|
||||||
if (load)
|
if (load)
|
||||||
{
|
{
|
||||||
u32 value;
|
u32 value;
|
||||||
@@ -2280,6 +2647,7 @@ void ARMCore::ExecuteBlockDataTransfer(u32 instruction)
|
|||||||
address = preindex ? base - count * 4 : base - (count - 1) * 4;
|
address = preindex ? base - count * 4 : base - (count - 1) * 4;
|
||||||
if (!m_watch_ranges.empty())
|
if (!m_watch_ranges.empty())
|
||||||
TraceWatchedBlockTransfer(address, count * 4, load);
|
TraceWatchedBlockTransfer(address, count * 4, load);
|
||||||
|
CheckAlignment(address, 3);
|
||||||
|
|
||||||
u32 loaded_pc = 0;
|
u32 loaded_pc = 0;
|
||||||
bool pc_loaded = false;
|
bool pc_loaded = false;
|
||||||
@@ -2475,6 +2843,12 @@ void ARMCore::ExecuteCoprocessor(u32 instruction)
|
|||||||
UndefinedInstruction(instruction);
|
UndefinedInstruction(instruction);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// CP15 is privileged: from User mode an MRC or MCR is an undefined instruction.
|
||||||
|
if (m_mmu_faults && GetMode() == Mode::User)
|
||||||
|
{
|
||||||
|
UndefinedInstruction(instruction);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (load)
|
if (load)
|
||||||
{
|
{
|
||||||
@@ -2699,7 +3073,8 @@ u32 ARMCore::ReadCP15(u32 opcode1, u32 crn, u32 crm, u32 opcode2) const
|
|||||||
case 3:
|
case 3:
|
||||||
return m_cp15.domain_access_control;
|
return m_cp15.domain_access_control;
|
||||||
case 5:
|
case 5:
|
||||||
return m_cp15.fault_status;
|
// Opcode_2 1 is the IFSR, modelled with MMU faults only.
|
||||||
|
return m_mmu_faults && opcode2 == 1 ? m_instruction_fault_status : m_cp15.fault_status;
|
||||||
case 6:
|
case 6:
|
||||||
return m_cp15.fault_address;
|
return m_cp15.fault_address;
|
||||||
case 13:
|
case 13:
|
||||||
@@ -2738,7 +3113,10 @@ void ARMCore::WriteCP15(u32 opcode1, u32 crn, u32 crm, u32 opcode2, u32 value)
|
|||||||
m_cp15.domain_access_control = value;
|
m_cp15.domain_access_control = value;
|
||||||
break;
|
break;
|
||||||
case 5:
|
case 5:
|
||||||
m_cp15.fault_status = value;
|
if (m_mmu_faults && opcode2 == 1)
|
||||||
|
m_instruction_fault_status = value;
|
||||||
|
else
|
||||||
|
m_cp15.fault_status = value;
|
||||||
break;
|
break;
|
||||||
case 6:
|
case 6:
|
||||||
m_cp15.fault_address = value;
|
m_cp15.fault_address = value;
|
||||||
@@ -2752,7 +3130,12 @@ void ARMCore::WriteCP15(u32 opcode1, u32 crn, u32 crm, u32 opcode2, u32 value)
|
|||||||
InvalidateInstructionCache();
|
InvalidateInstructionCache();
|
||||||
break;
|
break;
|
||||||
case 8: // TLB maintenance.
|
case 8: // TLB maintenance.
|
||||||
InvalidateTLB();
|
// With MMU faults, c8 c5/c6/c7 opcode2 1 invalidate the single entry that maps the MVA in Rd.
|
||||||
|
// The ARM926EJ-S TLB is unified, so the I, D and unified forms act on the same entries.
|
||||||
|
if (m_mmu_faults && opcode2 == 1 && (crm == 5 || crm == 6 || crm == 7))
|
||||||
|
InvalidateTLBEntry(value);
|
||||||
|
else
|
||||||
|
InvalidateTLB();
|
||||||
break;
|
break;
|
||||||
case 13:
|
case 13:
|
||||||
// ARM926 FCSE is specifically designed to switch low-address process spaces without flushing
|
// ARM926 FCSE is specifically designed to switch low-address process spaces without flushing
|
||||||
|
|||||||
@@ -159,6 +159,8 @@ public:
|
|||||||
}
|
}
|
||||||
bool IsWaitingForInterrupt() const { return m_waiting_for_interrupt; }
|
bool IsWaitingForInterrupt() const { return m_waiting_for_interrupt; }
|
||||||
bool IsWaitingForMemoryPoll() const { return m_waiting_for_memory_poll; }
|
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; }
|
u32 GetMemoryPollAddress() const { return m_memory_poll_address; }
|
||||||
bool IsWaitingForExternalEvent() const
|
bool IsWaitingForExternalEvent() const
|
||||||
{
|
{
|
||||||
@@ -172,6 +174,15 @@ public:
|
|||||||
u64 GetExecutedInstructions() const { return m_executed_instructions; }
|
u64 GetExecutedInstructions() const { return m_executed_instructions; }
|
||||||
void SetJitEnabled(bool enabled);
|
void SetJitEnabled(bool enabled);
|
||||||
bool IsJitEnabled() const { return m_jit_enabled; }
|
bool IsJitEnabled() const { return m_jit_enabled; }
|
||||||
|
// StarOS MMU-001 (F1): the ARM926 MMU's aborts and the CP15 privilege rule. When enabled, a
|
||||||
|
// translation, domain or permission fault (and, with control bit A, an alignment fault) raises
|
||||||
|
// a data abort (FSR c5 opcode2 0, FAR c6) or a prefetch abort (IFSR c5 opcode2 1; FAR left
|
||||||
|
// unchanged), access permissions follow AP, the S and R control bits and the DACR, TLB entries
|
||||||
|
// can be invalidated one MVA at a time, and CP15 from User mode is undefined. Off by default:
|
||||||
|
// the IOS LLE path keeps its identity fallback for fault descriptors and its savestate format.
|
||||||
|
void SetMMUFaultsEnabled(bool enabled);
|
||||||
|
bool AreMMUFaultsEnabled() const { return m_mmu_faults; }
|
||||||
|
u32 GetInstructionFaultStatus() const { return m_instruction_fault_status; }
|
||||||
bool IsJitMemoryProfilingEnabled() const { return m_profile_jit_memory; }
|
bool IsJitMemoryProfilingEnabled() const { return m_profile_jit_memory; }
|
||||||
u64 GetTraceGeneration() const { return m_trace_generation; }
|
u64 GetTraceGeneration() const { return m_trace_generation; }
|
||||||
u64 GetJitExecutedInstructions() const;
|
u64 GetJitExecutedInstructions() const;
|
||||||
@@ -246,6 +257,26 @@ private:
|
|||||||
u32 virtual_page = 0;
|
u32 virtual_page = 0;
|
||||||
u32 physical_page = 0;
|
u32 physical_page = 0;
|
||||||
u32 generation = 0;
|
u32 generation = 0;
|
||||||
|
// Filled only with MMU faults enabled: the descriptor's domain (bits 3:0), the AP of this
|
||||||
|
// 1 KiB granule (5:4), whether it came from a second-level page (bit 6), and log2 of the
|
||||||
|
// size the descriptor maps (15:8), so that one MVA invalidation drops its whole page.
|
||||||
|
u32 protection = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class AccessKind
|
||||||
|
{
|
||||||
|
Read,
|
||||||
|
Write,
|
||||||
|
Fetch,
|
||||||
|
};
|
||||||
|
|
||||||
|
// The result of a page-table walk with MMU faults enabled.
|
||||||
|
struct CheckedWalk
|
||||||
|
{
|
||||||
|
u32 physical_address = 0;
|
||||||
|
// 0, or the FSR value of a translation fault: status in bits 3:0, domain in bits 7:4.
|
||||||
|
u32 fault_status = 0;
|
||||||
|
u32 protection = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct InstructionCacheEntry
|
struct InstructionCacheEntry
|
||||||
@@ -291,6 +322,20 @@ private:
|
|||||||
u32* second_descriptor_value) const;
|
u32* second_descriptor_value) const;
|
||||||
u32 ReadPhysical32(u32 address) const;
|
u32 ReadPhysical32(u32 address) const;
|
||||||
void InvalidateTLB();
|
void InvalidateTLB();
|
||||||
|
// MMU faults enabled only.
|
||||||
|
u32 ModifiedVirtualAddress(u32 address) const;
|
||||||
|
CheckedWalk WalkPageTablesChecked(u32 modified_address) const;
|
||||||
|
const TLBEntry* LookupTLBChecked(u32 modified_address, u32* fault_status) const;
|
||||||
|
u32 PermissionFault(const TLBEntry& entry, AccessKind kind, bool user) const;
|
||||||
|
bool IsUserAccess() const;
|
||||||
|
u32 DataAddress(u32 address, bool write) const;
|
||||||
|
void RaiseDataAbort(u32 fault_status, u32 modified_address) const;
|
||||||
|
void CheckAlignment(u32 address, u32 mask) const;
|
||||||
|
bool TryTranslateFetch(u32 address, u32* physical_address, u32* fault_status) const;
|
||||||
|
bool EnterPrefetchAbortIfFaulted(u32 address);
|
||||||
|
void ExecuteARMWithAborts(u32 instruction);
|
||||||
|
void ExecuteThumbWithAborts(u16 instruction);
|
||||||
|
void InvalidateTLBEntry(u32 modified_address);
|
||||||
u16 FetchThumbInstruction(u32 address);
|
u16 FetchThumbInstruction(u32 address);
|
||||||
u32 FetchARMInstruction(u32 address);
|
u32 FetchARMInstruction(u32 address);
|
||||||
void InvalidateInstructionCache();
|
void InvalidateInstructionCache();
|
||||||
@@ -356,9 +401,13 @@ private:
|
|||||||
u32 m_spsr_und = 0;
|
u32 m_spsr_und = 0;
|
||||||
|
|
||||||
CP15State m_cp15{};
|
CP15State m_cp15{};
|
||||||
// ARM926 has separate instruction/data TLBs. A unified 1 KiB-granularity
|
// The ARM926EJ-S has one TLB for instructions and data: set-associative main entries plus
|
||||||
// software cache keeps the observable translations while avoiding a
|
// lockdown entries (c10) that whole-TLB invalidation preserves (ARM926EJ-S TRM). This software
|
||||||
// page-table walk on every interpreted access.
|
// TLB is unified too, but it is a 4096-entry direct-mapped cache of 1 KiB granules, so its
|
||||||
|
// capacity and replacement differ: an entry can stay here after the hardware would have
|
||||||
|
// replaced it, or be replaced here while the hardware keeps it. Lockdown is not modelled (c10
|
||||||
|
// writes are ignored). It keeps the observable translations while avoiding a page-table walk on
|
||||||
|
// every interpreted access.
|
||||||
mutable std::array<TLBEntry, TLB_ENTRY_COUNT> m_tlb{};
|
mutable std::array<TLBEntry, TLB_ENTRY_COUNT> m_tlb{};
|
||||||
std::array<InstructionCacheEntry, INSTRUCTION_CACHE_ENTRY_COUNT> m_instruction_cache{};
|
std::array<InstructionCacheEntry, INSTRUCTION_CACHE_ENTRY_COUNT> m_instruction_cache{};
|
||||||
mutable u32 m_tlb_generation = 1;
|
mutable u32 m_tlb_generation = 1;
|
||||||
@@ -386,6 +435,18 @@ private:
|
|||||||
u64 m_next_profile_instruction = PROFILE_SAMPLE_INTERVAL;
|
u64 m_next_profile_instruction = PROFILE_SAMPLE_INTERVAL;
|
||||||
std::unordered_map<u32, u64> m_pc_samples;
|
std::unordered_map<u32, u64> m_pc_samples;
|
||||||
bool m_jit_enabled = false;
|
bool m_jit_enabled = false;
|
||||||
|
// StarOS MMU-001 (F1). Not serialized: the option is a host choice made before the run, and
|
||||||
|
// the IOS LLE savestate format stays unchanged.
|
||||||
|
bool m_mmu_faults = false;
|
||||||
|
u32 m_instruction_fault_status = 0;
|
||||||
|
// Set by LDRT/STRT/LDRBT/STRBT around their access: User permissions in a privileged mode.
|
||||||
|
bool m_user_access = false;
|
||||||
|
// The first faulting access of an instruction records its abort here. Later accesses of the
|
||||||
|
// same instruction neither read nor write the bus, and the instruction's register results are
|
||||||
|
// undone before the data abort is taken (ARM926 base-restored abort model).
|
||||||
|
mutable bool m_abort_pending = false;
|
||||||
|
mutable u32 m_abort_status = 0;
|
||||||
|
mutable u32 m_abort_address = 0;
|
||||||
#if defined(_M_X86_64)
|
#if defined(_M_X86_64)
|
||||||
std::unique_ptr<ARMJitX64> m_jit;
|
std::unique_ptr<ARMJitX64> m_jit;
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -194,6 +194,11 @@ void ARMJitX64::GenerateDispatcher()
|
|||||||
MOV(64, R(RAX), ImmPtr(&m_clear_pending));
|
MOV(64, R(RAX), ImmPtr(&m_clear_pending));
|
||||||
CMP(8, MatR(RAX), Imm8(0));
|
CMP(8, MatR(RAX), Imm8(0));
|
||||||
FixupBranch invalidated = J_CC(CC_NE, Jump::Near);
|
FixupBranch invalidated = J_CC(CC_NE, Jump::Near);
|
||||||
|
// With MMU faults enabled, every block entry goes through C++, which checks the fetch under the
|
||||||
|
// current mode and DACR: the native caches below validate descriptors, not permissions.
|
||||||
|
MOV(64, R(RAX), ImmPtr(&m_core.m_mmu_faults));
|
||||||
|
CMP(8, MatR(RAX), Imm8(0));
|
||||||
|
FixupBranch checked_dispatch = J_CC(CC_NE, Jump::Near);
|
||||||
|
|
||||||
// Four-way native block cache. The key includes CPSR.T in bit zero and uses the ARM926 modified
|
// Four-way native block cache. The key includes CPSR.T in bit zero and uses the ARM926 modified
|
||||||
// virtual address (MVA) for low FCSE addresses. Different IOS process identifiers can therefore
|
// virtual address (MVA) for low FCSE addresses. Different IOS process identifiers can therefore
|
||||||
@@ -379,6 +384,11 @@ void ARMJitX64::GenerateDispatcher()
|
|||||||
MOV(32, R(ABI_PARAM3), R(EAX));
|
MOV(32, R(ABI_PARAM3), R(EAX));
|
||||||
MOV(32, R(ABI_PARAM2), Imm32(static_cast<u32>(DispatchReason::PageMiss)));
|
MOV(32, R(ABI_PARAM2), Imm32(static_cast<u32>(DispatchReason::PageMiss)));
|
||||||
FixupBranch dispatch_reason_page = J(Jump::Near);
|
FixupBranch dispatch_reason_page = J(Jump::Near);
|
||||||
|
SetJumpTarget(checked_dispatch);
|
||||||
|
XOR(32, R(ABI_PARAM4), R(ABI_PARAM4));
|
||||||
|
XOR(32, R(ABI_PARAM3), R(ABI_PARAM3));
|
||||||
|
MOV(32, R(ABI_PARAM2), Imm32(static_cast<u32>(DispatchReason::Checked)));
|
||||||
|
FixupBranch dispatch_reason_checked = J(Jump::Near);
|
||||||
SetJumpTarget(empty_entry);
|
SetJumpTarget(empty_entry);
|
||||||
SetJumpTarget(empty_entry_no_mmu);
|
SetJumpTarget(empty_entry_no_mmu);
|
||||||
MOV(32, R(ABI_PARAM4), R(EDX));
|
MOV(32, R(ABI_PARAM4), R(EDX));
|
||||||
@@ -387,6 +397,7 @@ void ARMJitX64::GenerateDispatcher()
|
|||||||
SetJumpTarget(dispatch_reason_translation);
|
SetJumpTarget(dispatch_reason_translation);
|
||||||
SetJumpTarget(dispatch_reason_key);
|
SetJumpTarget(dispatch_reason_key);
|
||||||
SetJumpTarget(dispatch_reason_page);
|
SetJumpTarget(dispatch_reason_page);
|
||||||
|
SetJumpTarget(dispatch_reason_checked);
|
||||||
MOV(64, R(ABI_PARAM1), ImmPtr(this));
|
MOV(64, R(ABI_PARAM1), ImmPtr(this));
|
||||||
ABI_CallFunction(Dispatch);
|
ABI_CallFunction(Dispatch);
|
||||||
TEST(64, R(RAX), R(RAX));
|
TEST(64, R(RAX), R(RAX));
|
||||||
@@ -427,9 +438,62 @@ size_t ARMJitX64::GetFastEntrySetIndex(u32 key)
|
|||||||
return static_cast<u32>(key * 0x9e3779b1U) >> 16;
|
return static_cast<u32>(key * 0x9e3779b1U) >> 16;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const u8* ARMJitX64::DispatchChecked(ARMJitX64* jit)
|
||||||
|
{
|
||||||
|
ARMCore& core = jit->m_core;
|
||||||
|
++jit->m_dispatch_slow_count;
|
||||||
|
u32 physical_address = 0;
|
||||||
|
u32 fault_status = 0;
|
||||||
|
// A fetch that faults is left to the interpreter, whose next step takes the prefetch abort.
|
||||||
|
if (!core.TryTranslateFetch(core.m_registers[15], &physical_address, &fault_status))
|
||||||
|
return nullptr;
|
||||||
|
const bool thumb = (core.m_cpsr & ARMCore::CPSR_T) != 0;
|
||||||
|
const u8* first_descriptor = nullptr;
|
||||||
|
const u8* second_descriptor = nullptr;
|
||||||
|
u32 first_descriptor_value = 0;
|
||||||
|
u32 second_descriptor_value = 0;
|
||||||
|
// No fast entry is recorded: the generated dispatcher sends the next entry here again.
|
||||||
|
Block* const block = jit->GetOrCompileBlock(
|
||||||
|
core.m_registers[15] | (thumb ? 1U : 0U), &physical_address, &first_descriptor,
|
||||||
|
&first_descriptor_value, &second_descriptor, &second_descriptor_value);
|
||||||
|
return block && block->runnable ? block->entry : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ARMJitX64::IsCheckedNativeARM(u32 instruction)
|
||||||
|
{
|
||||||
|
if ((instruction >> 28) == 0xf)
|
||||||
|
return false;
|
||||||
|
// B and BL.
|
||||||
|
if ((instruction & 0x0e000000) == 0x0a000000)
|
||||||
|
return true;
|
||||||
|
// Data processing only: not the multiply and extra load/store space, not the miscellaneous
|
||||||
|
// space (MRS, MSR, BX, BLX, CLZ, DSP: opcodes 8 to 11 with S clear), and not a write of PC,
|
||||||
|
// which may be an exception return.
|
||||||
|
if ((instruction & 0x0c000000) != 0 || (instruction & 0x02000090) == 0x00000090 ||
|
||||||
|
(instruction & 0x01900000) == 0x01000000)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return ((instruction >> 12) & 0xf) != 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ARMJitX64::IsCheckedNativeThumb(u16 instruction)
|
||||||
|
{
|
||||||
|
// Shifts, add/subtract, move/compare/add/subtract immediate, ALU operations, conditional
|
||||||
|
// branches (not SWI or the undefined condition) and unconditional branches.
|
||||||
|
if ((instruction & 0xc000) == 0x0000 || (instruction & 0xfc00) == 0x4000 ||
|
||||||
|
(instruction & 0xf800) == 0xe000)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return (instruction & 0xf000) == 0xd000 && ((instruction >> 8) & 0xf) < 0xe;
|
||||||
|
}
|
||||||
|
|
||||||
const u8* ARMJitX64::Dispatch(ARMJitX64* jit, DispatchReason reason, u32 generated_key,
|
const u8* ARMJitX64::Dispatch(ARMJitX64* jit, DispatchReason reason, u32 generated_key,
|
||||||
u32 generated_set_offset)
|
u32 generated_set_offset)
|
||||||
{
|
{
|
||||||
|
if (reason == DispatchReason::Checked)
|
||||||
|
return DispatchChecked(jit);
|
||||||
const bool thumb = (jit->m_core.m_cpsr & ARMCore::CPSR_T) != 0;
|
const bool thumb = (jit->m_core.m_cpsr & ARMCore::CPSR_T) != 0;
|
||||||
const u32 key = jit->m_core.m_registers[15] | (thumb ? 1U : 0U);
|
const u32 key = jit->m_core.m_registers[15] | (thumb ? 1U : 0U);
|
||||||
const u32 fast_key =
|
const u32 fast_key =
|
||||||
@@ -767,6 +831,10 @@ ARMJitX64::Block ARMJitX64::CompileBlock(u32 address, bool thumb)
|
|||||||
|
|
||||||
bool ARMJitX64::EmitDirectARM(u32 instruction, u32 address, bool* terminal, bool* dispatcher_exit)
|
bool ARMJitX64::EmitDirectARM(u32 instruction, u32 address, bool* terminal, bool* dispatcher_exit)
|
||||||
{
|
{
|
||||||
|
// With MMU faults, the interpreter (through FallbackARM, which ends the block) runs every
|
||||||
|
// instruction that could fault or change the next fetch's permission.
|
||||||
|
if (m_core.m_mmu_faults && !IsCheckedNativeARM(instruction))
|
||||||
|
return false;
|
||||||
// The IOS scheduler executes a small set of ARM926 system-control writes in virtually every
|
// The IOS scheduler executes a small set of ARM926 system-control writes in virtually every
|
||||||
// syscall and interrupt path. Decode those architectural operations once while compiling the
|
// syscall and interrupt path. Decode those architectural operations once while compiling the
|
||||||
// block instead of entering the complete interpreter decoder every time the block runs.
|
// block instead of entering the complete interpreter decoder every time the block runs.
|
||||||
@@ -1862,6 +1930,8 @@ void ARMJitX64::EmitExchangeBranch(OpArg target, bool link, u32 return_address)
|
|||||||
|
|
||||||
bool ARMJitX64::EmitDirectThumb(u16 instruction, u32 address, bool* terminal)
|
bool ARMJitX64::EmitDirectThumb(u16 instruction, u32 address, bool* terminal)
|
||||||
{
|
{
|
||||||
|
if (m_core.m_mmu_faults && !IsCheckedNativeThumb(instruction))
|
||||||
|
return false;
|
||||||
if ((instruction & 0xff00) == 0xdf00)
|
if ((instruction & 0xff00) == 0xdf00)
|
||||||
{
|
{
|
||||||
FlushRegisterCache();
|
FlushRegisterCache();
|
||||||
@@ -2911,8 +2981,15 @@ void ARMJitX64::FallbackThumb(ARMJitX64* jit, u16 instruction, u32 address)
|
|||||||
core->RecordJitFallback(address, true);
|
core->RecordJitFallback(address, true);
|
||||||
core->m_instruction_address = address;
|
core->m_instruction_address = address;
|
||||||
core->m_pc_written = false;
|
core->m_pc_written = false;
|
||||||
core->ExecuteThumb(instruction);
|
if (core->m_mmu_faults)
|
||||||
core->TryEnterThumbMemoryPoll(instruction);
|
{
|
||||||
|
core->ExecuteThumbWithAborts(instruction);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
core->ExecuteThumb(instruction);
|
||||||
|
core->TryEnterThumbMemoryPoll(instruction);
|
||||||
|
}
|
||||||
if (!core->m_pc_written)
|
if (!core->m_pc_written)
|
||||||
core->m_registers[15] = address + ((core->m_cpsr & ARMCore::CPSR_T) ? 2 : 4);
|
core->m_registers[15] = address + ((core->m_cpsr & ARMCore::CPSR_T) ? 2 : 4);
|
||||||
}
|
}
|
||||||
@@ -2934,9 +3011,13 @@ void ARMJitX64::FallbackARM(ARMJitX64* jit, u32 instruction, u32 address)
|
|||||||
}
|
}
|
||||||
else if (core->ConditionPassed(instruction >> 28))
|
else if (core->ConditionPassed(instruction >> 28))
|
||||||
{
|
{
|
||||||
core->ExecuteARM(instruction);
|
if (core->m_mmu_faults)
|
||||||
|
core->ExecuteARMWithAborts(instruction);
|
||||||
|
else
|
||||||
|
core->ExecuteARM(instruction);
|
||||||
}
|
}
|
||||||
core->TryEnterARMSliceStablePoll(instruction);
|
if (!core->m_mmu_faults)
|
||||||
|
core->TryEnterARMSliceStablePoll(instruction);
|
||||||
if (!core->m_pc_written)
|
if (!core->m_pc_written)
|
||||||
core->m_registers[15] = address + ((core->m_cpsr & ARMCore::CPSR_T) ? 2 : 4);
|
core->m_registers[15] = address + ((core->m_cpsr & ARMCore::CPSR_T) ? 2 : 4);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,6 +157,8 @@ private:
|
|||||||
KeyMiss,
|
KeyMiss,
|
||||||
PageMiss,
|
PageMiss,
|
||||||
EmptyEntry,
|
EmptyEntry,
|
||||||
|
// MMU faults enabled: every block entry checks its fetch (ARMCore::SetMMUFaultsEnabled).
|
||||||
|
Checked,
|
||||||
};
|
};
|
||||||
|
|
||||||
void PoisonMemory() override;
|
void PoisonMemory() override;
|
||||||
@@ -172,6 +174,11 @@ private:
|
|||||||
u32* first_descriptor_value_out, const u8** second_descriptor_out,
|
u32* first_descriptor_value_out, const u8** second_descriptor_out,
|
||||||
u32* second_descriptor_value_out);
|
u32* second_descriptor_value_out);
|
||||||
Block CompileBlock(u32 address, bool thumb);
|
Block CompileBlock(u32 address, bool thumb);
|
||||||
|
static const u8* DispatchChecked(ARMJitX64* jit);
|
||||||
|
// With MMU faults enabled, the instructions that run natively: they touch neither memory, CP15,
|
||||||
|
// the PSRs nor the exception model, so they can neither fault nor change a fetch's permission.
|
||||||
|
static bool IsCheckedNativeARM(u32 instruction);
|
||||||
|
static bool IsCheckedNativeThumb(u16 instruction);
|
||||||
bool EmitDirectARM(u32 instruction, u32 address, bool* terminal, bool* dispatcher_exit);
|
bool EmitDirectARM(u32 instruction, u32 address, bool* terminal, bool* dispatcher_exit);
|
||||||
bool EmitARMMultiplyLong(u32 instruction);
|
bool EmitARMMultiplyLong(u32 instruction);
|
||||||
bool CanEmitARMDataProcessing(u32 instruction) const;
|
bool CanEmitARMDataProcessing(u32 instruction) const;
|
||||||
|
|||||||
@@ -192,6 +192,14 @@ bool LoadBootstrapELF(std::span<const u8> image, ARMBus& bus, BootstrapELFInfo*
|
|||||||
parsed.fault_spsr = address;
|
parsed.fault_spsr = address;
|
||||||
found_fault_spsr = true;
|
found_fault_spsr = true;
|
||||||
}
|
}
|
||||||
|
else if (*name == "__panic_state")
|
||||||
|
{
|
||||||
|
parsed.panic_state = address;
|
||||||
|
}
|
||||||
|
else if (*name == "__panic_code")
|
||||||
|
{
|
||||||
|
parsed.panic_code = address;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const auto data_segment =
|
const auto data_segment =
|
||||||
@@ -208,6 +216,17 @@ bool LoadBootstrapELF(std::span<const u8> image, ARMBus& bus, BootstrapELFInfo*
|
|||||||
{
|
{
|
||||||
return Fail(error, "bootstrap ELF is missing the trace or fault symbols");
|
return Fail(error, "bootstrap ELF is missing the trace or fault symbols");
|
||||||
}
|
}
|
||||||
|
// 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) || panic_words_overlap)))
|
||||||
|
{
|
||||||
|
return Fail(error, "bootstrap ELF has an invalid panic symbol");
|
||||||
|
}
|
||||||
|
|
||||||
for (const Segment& segment : segments)
|
for (const Segment& segment : segments)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ struct BootstrapELFInfo
|
|||||||
u32 fault_code = 0;
|
u32 fault_code = 0;
|
||||||
u32 fault_lr = 0;
|
u32 fault_lr = 0;
|
||||||
u32 fault_spsr = 0;
|
u32 fault_spsr = 0;
|
||||||
|
// Optional (StarOS ADR-0014 R6): the panic path's guard and code words, 0 when the image has
|
||||||
|
// none. A nonzero guard means the panic handler ran.
|
||||||
|
u32 panic_state = 0;
|
||||||
|
u32 panic_code = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load the Phase 01 ELF into a bus already configured with the high SRAM mapping.
|
// Load the Phase 01 ELF into a bus already configured with the high SRAM mapping.
|
||||||
|
|||||||
@@ -144,6 +144,8 @@ bool Starlet::Init(const std::string& dump_directory, std::string* error)
|
|||||||
// construct it only after the complete physical bus exists.
|
// construct it only after the complete physical bus exists.
|
||||||
m_core = std::make_unique<ARMCore>(*m_memory,
|
m_core = std::make_unique<ARMCore>(*m_memory,
|
||||||
Config::Get(Config::MAIN_WII_STARLET_JIT_MEMORY_PROFILING));
|
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)
|
#if defined(_M_X86_64)
|
||||||
m_core->SetJitEnabled(Config::Get(Config::MAIN_WII_STARLET_JIT));
|
m_core->SetJitEnabled(Config::Get(Config::MAIN_WII_STARLET_JIT));
|
||||||
#else
|
#else
|
||||||
|
|||||||
@@ -333,6 +333,9 @@ constexpr u32 HW_AHBPROT = HW_BASE + 0x64;
|
|||||||
constexpr u32 HW_TIMER = HW_BASE + 0x10;
|
constexpr u32 HW_TIMER = HW_BASE + 0x10;
|
||||||
constexpr u64 TIMER_CLOCK_DIVISOR = 128;
|
constexpr u64 TIMER_CLOCK_DIVISOR = 128;
|
||||||
constexpr u32 HW_ALARM = HW_BASE + 0x14;
|
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_ENABLE = HW_BASE + 0xdc;
|
||||||
constexpr u32 HW_GPIO_OUT = HW_BASE + 0xe0;
|
constexpr u32 HW_GPIO_OUT = HW_BASE + 0xe0;
|
||||||
constexpr u32 HW_GPIO_DIR = HW_BASE + 0xe4;
|
constexpr u32 HW_GPIO_DIR = HW_BASE + 0xe4;
|
||||||
@@ -864,6 +867,7 @@ void StarletMemory::Reset()
|
|||||||
ResetOHCIController(controller);
|
ResetOHCIController(controller);
|
||||||
m_arm_cycles = 0;
|
m_arm_cycles = 0;
|
||||||
m_timer_offset = 0;
|
m_timer_offset = 0;
|
||||||
|
m_alarm_matched = false;
|
||||||
m_boot0_mapped = true;
|
m_boot0_mapped = true;
|
||||||
m_sram_split_mode = false;
|
m_sram_split_mode = false;
|
||||||
// Retail Hollywood production revision. Early firmware branches on this
|
// 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_seeprom_write_all);
|
||||||
p.Do(m_arm_cycles);
|
p.Do(m_arm_cycles);
|
||||||
p.Do(m_timer_offset);
|
p.Do(m_timer_offset);
|
||||||
|
p.Do(m_alarm_matched);
|
||||||
p.Do(m_initialized);
|
p.Do(m_initialized);
|
||||||
p.Do(m_boot0_mapped);
|
p.Do(m_boot0_mapped);
|
||||||
p.Do(m_sram_split_mode);
|
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
|
u32 StarletMemory::GetTimer() const
|
||||||
{
|
{
|
||||||
// Hollywood's timer runs at Starlet / 128 (1.8984375 MHz at 243 MHz).
|
// 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 alarm = ReadRegister(HW_ALARM);
|
||||||
const u32 ticks_until_alarm = alarm - old_timer;
|
const u32 ticks_until_alarm = alarm - old_timer;
|
||||||
if (ticks_until_alarm != 0 && ticks_until_alarm <= elapsed_timer_ticks)
|
if (ticks_until_alarm != 0 && ticks_until_alarm <= elapsed_timer_ticks)
|
||||||
|
{
|
||||||
|
m_alarm_matched = true;
|
||||||
m_system.GetWiiIPC().SetStarletInterrupt(INT_CAUSE_TIMER, true);
|
m_system.GetWiiIPC().SetStarletInterrupt(INT_CAUSE_TIMER, true);
|
||||||
|
}
|
||||||
|
|
||||||
if (m_nand_cycles_until_completion != 0)
|
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.
|
// HW_ALARM is a comparator, not an interrupt acknowledgement register.
|
||||||
// Original Starlet software acknowledges the timer separately through
|
// Original Starlet software acknowledges the timer separately through
|
||||||
// HW_ARMIRQFLAG. Programming the comparator to the current counter value
|
// HW_ARMIRQFLAG. Programming the comparator re-arms it: a past match no
|
||||||
// creates an immediate match.
|
// longer holds the cause (KeepMatchedAlarmAsserted). Programming it to the
|
||||||
if (word == GetTimer())
|
// current counter value creates an immediate match.
|
||||||
|
m_alarm_matched = word == GetTimer();
|
||||||
|
if (m_alarm_matched)
|
||||||
m_system.GetWiiIPC().SetStarletInterrupt(INT_CAUSE_TIMER, true);
|
m_system.GetWiiIPC().SetStarletInterrupt(INT_CAUSE_TIMER, true);
|
||||||
}
|
}
|
||||||
else if (word_address == HW_SPARE0)
|
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)
|
word_address == HW_AHBPROT || word_address == HW_RESETS)
|
||||||
{
|
{
|
||||||
m_system.GetWiiIPC().WriteStarletRegister(word_address - HW_BASE, word);
|
m_system.GetWiiIPC().WriteStarletRegister(word_address - HW_BASE, word);
|
||||||
|
KeepMatchedAlarmAsserted(word_address);
|
||||||
|
UpdateInterruptLines(word_address);
|
||||||
if (word_address == HW_BASE + 0x0c && (word & 0x09) != 0)
|
if (word_address == HW_BASE + 0x0c && (word & 0x09) != 0)
|
||||||
{
|
{
|
||||||
// The physical CPUs are concurrent. Give Broadway a scheduling boundary
|
// The physical CPUs are concurrent. Give Broadway a scheduling boundary
|
||||||
@@ -4229,6 +4271,8 @@ void StarletMemory::Write32(u32 address, u32 value)
|
|||||||
{
|
{
|
||||||
WriteRegister(address, value);
|
WriteRegister(address, value);
|
||||||
m_system.GetWiiIPC().WriteStarletRegister(address - HW_BASE, 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.
|
// Phase 11 H1: an IPC reply (ARMCTRL Y1) audits the watched ranges.
|
||||||
if (!m_watch_ranges.empty() && address == HW_BASE + 0x0c && (value & 1) != 0)
|
if (!m_watch_ranges.empty() && address == HW_BASE + 0x0c && (value & 1) != 0)
|
||||||
WatchAudit();
|
WatchAudit();
|
||||||
|
|||||||
@@ -82,6 +82,13 @@ public:
|
|||||||
// that touches one of those physical ranges. The ARM JIT sends the accesses that hit a watched
|
// 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.
|
// range to its bus slow path, with the instruction address. `core` gives that address.
|
||||||
void SetWatchCore(const ARMCore* core);
|
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 Reset();
|
||||||
void DoState(PointerWrap& p);
|
void DoState(PointerWrap& p);
|
||||||
|
|
||||||
@@ -110,6 +117,10 @@ public:
|
|||||||
private:
|
private:
|
||||||
// Synthetic NAND fixtures exercise startup pairing without any console dumps or keys.
|
// Synthetic NAND fixtures exercise startup pairing without any console dumps or keys.
|
||||||
friend class StarletPairingTest;
|
friend class StarletPairingTest;
|
||||||
|
|
||||||
|
void UpdateInterruptLines(u32 address);
|
||||||
|
void KeepMatchedAlarmAsserted(u32 address);
|
||||||
|
ARMCore* m_interrupt_core = nullptr;
|
||||||
friend class StarletDMATest;
|
friend class StarletDMATest;
|
||||||
|
|
||||||
static constexpr u32 NAND_PAGE_DATA_SIZE = 0x800;
|
static constexpr u32 NAND_PAGE_DATA_SIZE = 0x800;
|
||||||
@@ -347,6 +358,8 @@ private:
|
|||||||
bool m_seeprom_write_all = false;
|
bool m_seeprom_write_all = false;
|
||||||
u64 m_arm_cycles = 0;
|
u64 m_arm_cycles = 0;
|
||||||
u32 m_timer_offset = 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;
|
bool m_initialized = false;
|
||||||
// Phase 11 F1 write watch (REV_WATCH); empty when off.
|
// Phase 11 F1 write watch (REV_WATCH); empty when off.
|
||||||
std::vector<std::pair<u32, u32>> m_watch_ranges;
|
std::vector<std::pair<u32, u32>> m_watch_ranges;
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ struct CompressAndDumpStateArgs
|
|||||||
static Common::WorkQueueThreadSP<CompressAndDumpStateArgs> s_compress_and_dump_thread;
|
static Common::WorkQueueThreadSP<CompressAndDumpStateArgs> s_compress_and_dump_thread;
|
||||||
|
|
||||||
// Don't forget to increase this after doing changes on the savestate system
|
// 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
|
// Increase this if the StateExtendedHeader definition changes
|
||||||
constexpr u32 EXTENDED_HEADER_VERSION = 1; // Last changed in PR 12217
|
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(StarletARMCoreTest IOS/Starlet/ARMCoreTest.cpp)
|
||||||
add_dolphin_test(StarletBootstrapELFTest IOS/Starlet/BootstrapELFTest.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(StarletDMATest IOS/Starlet/DMATest.cpp)
|
||||||
add_dolphin_test(StarletSRAMReadTest IOS/Starlet/SRAMReadTest.cpp)
|
add_dolphin_test(StarletSRAMReadTest IOS/Starlet/SRAMReadTest.cpp)
|
||||||
add_dolphin_test(StarletSRAMWriteTest IOS/Starlet/SRAMWriteTest.cpp)
|
add_dolphin_test(StarletSRAMWriteTest IOS/Starlet/SRAMWriteTest.cpp)
|
||||||
|
|||||||
@@ -6,7 +6,9 @@
|
|||||||
#include <array>
|
#include <array>
|
||||||
#include <bit>
|
#include <bit>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <initializer_list>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
@@ -566,6 +568,208 @@ TEST(StarletRegisters, WideTimerAndInterruptAccessesMatchHardwareSemantics)
|
|||||||
EXPECT_EQ(memory.Read32(arm_irq_mask), 0x800619efu);
|
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)
|
TEST(StarletInterrupts, DriveAndResetUseDistinctHollywoodLines)
|
||||||
{
|
{
|
||||||
// Hollywood IRQ numbers, not the Broadway Processor Interface's DVD line.
|
// Hollywood IRQ numbers, not the Broadway Processor Interface's DVD line.
|
||||||
@@ -3536,5 +3740,495 @@ TEST(StarletARMCore, JitCachesProvisionalIdentityTranslationForFaultPages)
|
|||||||
EXPECT_GT(bus.GetDirectPointerCount(), 0u); // Native descriptor validation is still present.
|
EXPECT_GT(bus.GetDirectPointerCount(), 0u); // Native descriptor validation is still present.
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StarOS MMU-001 (F1): ARM926 MMU aborts with ARMCore::SetMMUFaultsEnabled. Every case runs in the
|
||||||
|
// interpreter and, on x86-64, in the JIT; both must reach the same state.
|
||||||
|
class MMUFaultMachine
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static constexpr u32 TABLE = 0x4000; // first-level table, 16 KiB aligned
|
||||||
|
static constexpr u32 COARSE = 0x8000; // one coarse second-level table
|
||||||
|
// Section 0x800 maps physical 0 privileged-only (AP 01), section 0x880 the same bytes with full
|
||||||
|
// access (AP 11), both in domain 0. Code is written once and run from either view.
|
||||||
|
static constexpr u32 KERNEL = 0x80000000;
|
||||||
|
static constexpr u32 CODE = KERNEL + 0x1000;
|
||||||
|
static constexpr u32 USER_CODE = 0x88001000;
|
||||||
|
static constexpr u32 FAR_SENTINEL = 0xdeadbeef;
|
||||||
|
static constexpr u32 SVC_CPSR = 0xd3;
|
||||||
|
static constexpr u32 USER_CPSR = 0xd0;
|
||||||
|
static constexpr u32 ABORT_MODE = 0x17;
|
||||||
|
static constexpr u32 UNDEFINED_MODE = 0x1b;
|
||||||
|
|
||||||
|
explicit MMUFaultMachine(bool jit) : m_bus(0x100000), m_core(m_bus)
|
||||||
|
{
|
||||||
|
// High vectors: undefined and SWI stop on themselves; both aborts branch to a handler that
|
||||||
|
// records SPSR (r8), DFSR (r9), FAR (r10) and IFSR (r11) through MRS/MRC, then marks its kind
|
||||||
|
// in r12 (0xd data, 0xc prefetch) and stops.
|
||||||
|
for (u32 vector = 0; vector < 0x20; vector += 4)
|
||||||
|
m_bus.WriteARM(0xffff0000 + vector, 0xeafffffe); // b .
|
||||||
|
m_bus.WriteARM(0xffff000c, 0xea00004b); // b 0xffff0140
|
||||||
|
m_bus.WriteARM(0xffff0010, 0xea00003a); // b 0xffff0100
|
||||||
|
for (const auto [handler, kind] : {std::pair{0xffff0100u, 0xe3a0c00du},
|
||||||
|
std::pair{0xffff0140u, 0xe3a0c00cu}})
|
||||||
|
{
|
||||||
|
m_bus.WriteARM(handler, 0xe14f8000); // mrs r8, spsr
|
||||||
|
m_bus.WriteARM(handler + 4, 0xee159f10); // mrc p15, 0, r9, c5, c0, 0 (DFSR)
|
||||||
|
m_bus.WriteARM(handler + 8, 0xee16af10); // mrc p15, 0, r10, c6, c0, 0 (FAR)
|
||||||
|
m_bus.WriteARM(handler + 12, 0xee15bf30); // mrc p15, 0, r11, c5, c0, 1 (IFSR)
|
||||||
|
m_bus.WriteARM(handler + 16, kind); // mov r12, #kind
|
||||||
|
m_bus.WriteARM(handler + 20, 0xeafffffe); // b .
|
||||||
|
}
|
||||||
|
MapSection(0x800, 0, 1, 0);
|
||||||
|
MapSection(0x880, 0, 3, 0);
|
||||||
|
MapSection(0xfff, 0xfff00000, 1, 0);
|
||||||
|
auto& cp15 = m_core.GetCP15State();
|
||||||
|
cp15.translation_table_base = TABLE;
|
||||||
|
cp15.domain_access_control = 1; // domain 0 client, every other domain no access
|
||||||
|
cp15.control |= 1;
|
||||||
|
cp15.fault_address = FAR_SENTINEL;
|
||||||
|
m_core.SetMMUFaultsEnabled(true);
|
||||||
|
m_core.SetJitEnabled(jit);
|
||||||
|
m_core.SetRegister(15, CODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MapSection(u32 index, u32 physical, u32 ap, u32 domain)
|
||||||
|
{
|
||||||
|
m_bus.WriteARM(TABLE + index * 4,
|
||||||
|
(physical & 0xfff00000) | (ap << 10) | (domain << 5) | 0x12);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The section of `virtual_address` points at the coarse table, whose entry for it maps a small
|
||||||
|
// page of `physical` with the same AP in its four subpages.
|
||||||
|
void MapSmallPage(u32 virtual_address, u32 physical, u32 ap, u32 domain)
|
||||||
|
{
|
||||||
|
m_bus.WriteARM(TABLE + (virtual_address >> 20) * 4, COARSE | (domain << 5) | 0x11);
|
||||||
|
m_bus.WriteARM(COARSE + ((virtual_address >> 12) & 0xff) * 4,
|
||||||
|
(physical & 0xfffff000) | ((ap * 0x55) << 4) | 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Code at a physical address (its view in section 0x800 or 0x880 is `physical` + base).
|
||||||
|
void Code(u32 physical, std::initializer_list<u32> instructions)
|
||||||
|
{
|
||||||
|
for (const u32 instruction : instructions)
|
||||||
|
{
|
||||||
|
m_bus.WriteARM(physical, instruction);
|
||||||
|
physical += 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Start(u32 cpsr, u32 pc)
|
||||||
|
{
|
||||||
|
m_core.SetCPSR(cpsr);
|
||||||
|
m_core.SetRegister(15, pc);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Run(u64 cycles = 48) { m_core.RunCycles(cycles); }
|
||||||
|
|
||||||
|
void ExpectDataAbort(u32 instruction_address, u32 dfsr, u32 far, u32 spsr)
|
||||||
|
{
|
||||||
|
EXPECT_EQ(m_core.GetCPSR() & 0x1f, ABORT_MODE);
|
||||||
|
EXPECT_EQ(m_core.GetRegister(12), 0xdu) << "not the data abort vector";
|
||||||
|
EXPECT_EQ(m_core.GetRegister(14), instruction_address + 8);
|
||||||
|
EXPECT_EQ(m_core.GetRegister(8), spsr);
|
||||||
|
EXPECT_EQ(m_core.GetRegister(9), dfsr);
|
||||||
|
EXPECT_EQ(m_core.GetRegister(10), far);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExpectPrefetchAbort(u32 instruction_address, u32 ifsr, u32 spsr)
|
||||||
|
{
|
||||||
|
EXPECT_EQ(m_core.GetCPSR() & 0x1f, ABORT_MODE);
|
||||||
|
EXPECT_EQ(m_core.GetRegister(12), 0xcu) << "not the prefetch abort vector";
|
||||||
|
EXPECT_EQ(m_core.GetRegister(14), instruction_address + 4);
|
||||||
|
EXPECT_EQ(m_core.GetRegister(8), spsr);
|
||||||
|
EXPECT_EQ(m_core.GetRegister(11), ifsr);
|
||||||
|
// A prefetch abort leaves the FAR as it was.
|
||||||
|
EXPECT_EQ(m_core.GetRegister(10), FAR_SENTINEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBus& Bus() { return m_bus; }
|
||||||
|
ARMCore& Core() { return m_core; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
TestBus m_bus;
|
||||||
|
ARMCore m_core;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename Case>
|
||||||
|
void ForEachEngine(Case run_case)
|
||||||
|
{
|
||||||
|
for (const bool jit : {false, true})
|
||||||
|
{
|
||||||
|
#if !defined(_M_X86_64)
|
||||||
|
if (jit)
|
||||||
|
continue;
|
||||||
|
#endif
|
||||||
|
SCOPED_TRACE(jit ? "JIT" : "interpreter");
|
||||||
|
MMUFaultMachine machine(jit);
|
||||||
|
run_case(machine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using M = MMUFaultMachine;
|
||||||
|
|
||||||
|
TEST(StarletARMCoreMMUFaults, SectionTranslationFaultIsADataAbort)
|
||||||
|
{
|
||||||
|
ForEachEngine([](M& m) {
|
||||||
|
m.Code(0x1000, {0xe5921000}); // ldr r1, [r2]
|
||||||
|
m.Core().SetRegister(1, 0x11111111);
|
||||||
|
m.Core().SetRegister(2, 0x90000004); // section 0x900 has no descriptor
|
||||||
|
m.Run();
|
||||||
|
m.ExpectDataAbort(M::CODE, 0x05, 0x90000004, M::SVC_CPSR);
|
||||||
|
EXPECT_EQ(m.Core().GetRegister(1), 0x11111111u);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StarletARMCoreMMUFaults, PageTranslationFaultReportsItsDomain)
|
||||||
|
{
|
||||||
|
ForEachEngine([](M& m) {
|
||||||
|
m.MapSmallPage(0x90000000, 0x2000, 3, 2);
|
||||||
|
m.Code(0x1000, {0xe5921000}); // ldr r1, [r2]
|
||||||
|
m.Core().SetRegister(2, 0x90001000); // the next small page has no descriptor
|
||||||
|
m.Run();
|
||||||
|
m.ExpectDataAbort(M::CODE, 0x27, 0x90001000, M::SVC_CPSR);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StarletARMCoreMMUFaults, DomainWithoutAccessFaultsForSectionsAndPages)
|
||||||
|
{
|
||||||
|
ForEachEngine([](M& m) {
|
||||||
|
m.MapSection(0x900, 0, 3, 2);
|
||||||
|
m.Code(0x1000, {0xe5921000}); // ldr r1, [r2]
|
||||||
|
m.Core().SetRegister(2, 0x90002000);
|
||||||
|
m.Run();
|
||||||
|
m.ExpectDataAbort(M::CODE, 0x29, 0x90002000, M::SVC_CPSR);
|
||||||
|
});
|
||||||
|
ForEachEngine([](M& m) {
|
||||||
|
m.MapSmallPage(0x91000000, 0x2000, 3, 4);
|
||||||
|
m.Code(0x1000, {0xe5921000}); // ldr r1, [r2]
|
||||||
|
m.Core().SetRegister(2, 0x91000010);
|
||||||
|
m.Run();
|
||||||
|
m.ExpectDataAbort(M::CODE, 0x4b, 0x91000010, M::SVC_CPSR);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StarletARMCoreMMUFaults, UserAccessFollowsAP)
|
||||||
|
{
|
||||||
|
// AP 01: privileged only. AP 10: User read-only. AP 11: full access.
|
||||||
|
ForEachEngine([](M& m) {
|
||||||
|
m.Code(0x1000, {0xe5921000}); // ldr r1, [r2]
|
||||||
|
m.Core().SetRegister(2, M::KERNEL + 0x2000);
|
||||||
|
m.Start(M::USER_CPSR, M::USER_CODE);
|
||||||
|
m.Run();
|
||||||
|
m.ExpectDataAbort(M::USER_CODE, 0x0d, M::KERNEL + 0x2000, M::USER_CPSR);
|
||||||
|
});
|
||||||
|
ForEachEngine([](M& m) {
|
||||||
|
m.MapSection(0x920, 0, 2, 0);
|
||||||
|
m.Bus().WriteARM(0x2000, 0x12345678);
|
||||||
|
m.Code(0x1000, {0xe5921000, 0xe5823000}); // ldr r1, [r2]; str r3, [r2]
|
||||||
|
m.Core().SetRegister(2, 0x92002000);
|
||||||
|
m.Core().SetRegister(3, 0xabcdef01);
|
||||||
|
m.Start(M::USER_CPSR, M::USER_CODE);
|
||||||
|
m.Run();
|
||||||
|
EXPECT_EQ(m.Core().GetRegister(1), 0x12345678u);
|
||||||
|
m.ExpectDataAbort(M::USER_CODE + 4, 0x0d, 0x92002000, M::USER_CPSR);
|
||||||
|
EXPECT_EQ(m.Bus()[0x2000], 0x12u) << "the faulting store wrote memory";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StarletARMCoreMMUFaults, APZeroDependsOnTheSAndRBits)
|
||||||
|
{
|
||||||
|
struct Case
|
||||||
|
{
|
||||||
|
u32 control_bits;
|
||||||
|
bool user;
|
||||||
|
bool write;
|
||||||
|
bool allowed;
|
||||||
|
};
|
||||||
|
constexpr u32 S = 1U << 8;
|
||||||
|
constexpr u32 R = 1U << 9;
|
||||||
|
for (const Case c : {Case{0, false, false, false}, Case{S, false, false, true},
|
||||||
|
Case{S, false, true, false}, Case{S, true, false, false},
|
||||||
|
Case{R, true, false, true}, Case{R, false, true, false},
|
||||||
|
Case{S | R, false, false, false}})
|
||||||
|
{
|
||||||
|
SCOPED_TRACE(testing::Message() << "control " << std::hex << c.control_bits << " user "
|
||||||
|
<< c.user << " write " << c.write);
|
||||||
|
ForEachEngine([&](M& m) {
|
||||||
|
m.MapSection(0x900, 0, 0, 0);
|
||||||
|
m.Bus().WriteARM(0x2000, 0x0badf00d);
|
||||||
|
m.Core().GetCP15State().control |= c.control_bits;
|
||||||
|
m.Code(0x1000, {c.write ? 0xe5821000u : 0xe5921000u}); // str/ldr r1, [r2]
|
||||||
|
m.Core().SetRegister(1, 0x11111111);
|
||||||
|
m.Core().SetRegister(2, 0x90002000);
|
||||||
|
const u32 code = c.user ? M::USER_CODE : M::CODE;
|
||||||
|
m.Start(c.user ? M::USER_CPSR : M::SVC_CPSR, code);
|
||||||
|
m.Run();
|
||||||
|
if (c.allowed)
|
||||||
|
{
|
||||||
|
EXPECT_NE(m.Core().GetCPSR() & 0x1f, M::ABORT_MODE);
|
||||||
|
EXPECT_EQ(m.Core().GetRegister(1), c.write ? 0x11111111u : 0x0badf00du);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m.ExpectDataAbort(code, 0x0d, 0x90002000, c.user ? M::USER_CPSR : M::SVC_CPSR);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StarletARMCoreMMUFaults, ManagerDomainSkipsAP)
|
||||||
|
{
|
||||||
|
ForEachEngine([](M& m) {
|
||||||
|
m.MapSection(0x900, 0, 0, 5);
|
||||||
|
m.Core().GetCP15State().domain_access_control = 1 | (3 << 10);
|
||||||
|
m.Bus().WriteARM(0x2000, 0x600dcafe);
|
||||||
|
m.Code(0x1000, {0xe5921000, 0xe5823004}); // ldr r1, [r2]; str r3, [r2, #4]
|
||||||
|
m.Core().SetRegister(2, 0x90002000);
|
||||||
|
m.Core().SetRegister(3, 0x01020304);
|
||||||
|
m.Start(M::USER_CPSR, M::USER_CODE);
|
||||||
|
m.Run(8);
|
||||||
|
EXPECT_EQ(m.Core().GetCPSR() & 0x1f, 0x10u);
|
||||||
|
EXPECT_EQ(m.Core().GetRegister(1), 0x600dcafeu);
|
||||||
|
EXPECT_EQ(m.Bus()[0x2004], 0x01u);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StarletARMCoreMMUFaults, FetchFromAnUnmappedSectionIsAPrefetchAbort)
|
||||||
|
{
|
||||||
|
ForEachEngine([](M& m) {
|
||||||
|
m.Code(0x1000, {0xe12fff13}); // bx r3
|
||||||
|
m.Core().SetRegister(3, 0x90000000);
|
||||||
|
m.Run();
|
||||||
|
m.ExpectPrefetchAbort(0x90000000, 0x05, M::SVC_CPSR);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StarletARMCoreMMUFaults, UserFetchFromPrivilegedCodeIsAPrefetchAbort)
|
||||||
|
{
|
||||||
|
// ARMv5 has no execute permission: a fetch needs read permission, so AP 01 stops User code.
|
||||||
|
ForEachEngine([](M& m) {
|
||||||
|
m.Code(0x1000, {0xe12fff13}); // bx r3
|
||||||
|
m.Core().SetRegister(3, M::CODE + 0x40);
|
||||||
|
m.Start(M::USER_CPSR, M::USER_CODE);
|
||||||
|
m.Run();
|
||||||
|
m.ExpectPrefetchAbort(M::CODE + 0x40, 0x0d, M::USER_CPSR);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StarletARMCoreMMUFaults, CachedCodeIsCheckedAgainAtEveryEntry)
|
||||||
|
{
|
||||||
|
// The JIT compiles the block in Supervisor mode; entering it again in User mode must fault.
|
||||||
|
ForEachEngine([](M& m) {
|
||||||
|
m.Code(0x1040, {0xe3a07001, 0xeafffffe}); // mov r7, #1; b .
|
||||||
|
m.Start(M::SVC_CPSR, M::CODE + 0x40);
|
||||||
|
m.Run(8);
|
||||||
|
EXPECT_EQ(m.Core().GetRegister(7), 1u);
|
||||||
|
m.Core().SetRegister(7, 0);
|
||||||
|
m.Start(M::USER_CPSR, M::CODE + 0x40);
|
||||||
|
m.Run();
|
||||||
|
m.ExpectPrefetchAbort(M::CODE + 0x40, 0x0d, M::USER_CPSR);
|
||||||
|
EXPECT_EQ(m.Core().GetRegister(7), 0u);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StarletARMCoreMMUFaults, RevokingTheCodeDomainStopsTheNextFetch)
|
||||||
|
{
|
||||||
|
ForEachEngine([](M& m) {
|
||||||
|
m.MapSection(0x930, 0, 1, 3);
|
||||||
|
m.Core().GetCP15State().domain_access_control = 1 | (1 << 6);
|
||||||
|
m.Code(0x1000, {0xee034f10, 0xe3a07001}); // mcr p15, 0, r4, c3, c0, 0; mov r7, #1
|
||||||
|
m.Core().SetRegister(4, 1); // domain 3: no access
|
||||||
|
m.Start(M::SVC_CPSR, 0x93001000);
|
||||||
|
m.Run();
|
||||||
|
m.ExpectPrefetchAbort(0x93001004, 0x39, M::SVC_CPSR);
|
||||||
|
EXPECT_EQ(m.Core().GetRegister(7), 0u);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StarletARMCoreMMUFaults, BlockTransferAbortRestoresTheBase)
|
||||||
|
{
|
||||||
|
ForEachEngine([](M& m) {
|
||||||
|
m.Code(0x1000, {0xe8b0000e}); // ldmia r0!, {r1, r2, r3}
|
||||||
|
for (u32 reg = 1; reg <= 3; ++reg)
|
||||||
|
m.Core().SetRegister(reg, 0x11111111 * reg);
|
||||||
|
m.Core().SetRegister(0, 0x800ffffc); // the second word is in unmapped section 0x801
|
||||||
|
m.Run();
|
||||||
|
m.ExpectDataAbort(M::CODE, 0x05, 0x80100000, M::SVC_CPSR);
|
||||||
|
EXPECT_EQ(m.Core().GetRegister(0), 0x800ffffcu);
|
||||||
|
for (u32 reg = 1; reg <= 3; ++reg)
|
||||||
|
EXPECT_EQ(m.Core().GetRegister(reg), 0x11111111 * reg);
|
||||||
|
});
|
||||||
|
ForEachEngine([](M& m) {
|
||||||
|
m.Code(0x1000, {0xe8a00006}); // stmia r0!, {r1, r2}
|
||||||
|
m.Core().SetRegister(0, 0x800ffffc);
|
||||||
|
m.Core().SetRegister(1, 0xa1a2a3a4);
|
||||||
|
m.Core().SetRegister(2, 0xb1b2b3b4);
|
||||||
|
m.Run();
|
||||||
|
m.ExpectDataAbort(M::CODE, 0x05, 0x80100000, M::SVC_CPSR);
|
||||||
|
EXPECT_EQ(m.Core().GetRegister(0), 0x800ffffcu);
|
||||||
|
// The word stored before the fault stays written, as on ARM926.
|
||||||
|
EXPECT_EQ(m.Bus()[0xffffc], 0xa1u);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StarletARMCoreMMUFaults, LDRTChecksUserPermissionsInAPrivilegedMode)
|
||||||
|
{
|
||||||
|
ForEachEngine([](M& m) {
|
||||||
|
m.Bus().WriteARM(0x2000, 0x13572468);
|
||||||
|
m.Code(0x1000, {0xe5921000, 0xe4b23000}); // ldr r1, [r2]; ldrt r3, [r2], #0
|
||||||
|
m.Core().SetRegister(2, M::KERNEL + 0x2000);
|
||||||
|
m.Run();
|
||||||
|
EXPECT_EQ(m.Core().GetRegister(1), 0x13572468u);
|
||||||
|
m.ExpectDataAbort(M::CODE + 4, 0x0d, M::KERNEL + 0x2000, M::SVC_CPSR);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StarletARMCoreMMUFaults, AlignmentFaultsNeedTheABit)
|
||||||
|
{
|
||||||
|
ForEachEngine([](M& m) {
|
||||||
|
m.Core().GetCP15State().control |= 2;
|
||||||
|
m.Code(0x1000, {0xe5921000}); // ldr r1, [r2]
|
||||||
|
m.Core().SetRegister(2, M::KERNEL + 0x2002);
|
||||||
|
m.Run();
|
||||||
|
m.ExpectDataAbort(M::CODE, 0x01, M::KERNEL + 0x2002, M::SVC_CPSR);
|
||||||
|
});
|
||||||
|
ForEachEngine([](M& m) {
|
||||||
|
m.Bus().WriteARM(0x2000, 0x11223344);
|
||||||
|
m.Code(0x1000, {0xe5921000}); // ldr r1, [r2]: rotated, no fault
|
||||||
|
m.Core().SetRegister(2, M::KERNEL + 0x2002);
|
||||||
|
m.Run(4);
|
||||||
|
EXPECT_EQ(m.Core().GetRegister(1), 0x33441122u);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StarletARMCoreMMUFaults, CP15FromUserModeIsUndefined)
|
||||||
|
{
|
||||||
|
ForEachEngine([](M& m) {
|
||||||
|
m.Code(0x1000, {0xee034f10}); // mcr p15, 0, r4, c3, c0, 0
|
||||||
|
m.Core().SetRegister(4, 0xffffffff);
|
||||||
|
m.Start(M::USER_CPSR, M::USER_CODE);
|
||||||
|
m.Run(8);
|
||||||
|
EXPECT_EQ(m.Core().GetCPSR() & 0x1f, M::UNDEFINED_MODE);
|
||||||
|
EXPECT_EQ(m.Core().GetRegister(15), 0xffff0004u);
|
||||||
|
EXPECT_EQ(m.Core().GetCP15State().domain_access_control, 1u);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StarletARMCoreMMUFaults, TLBEntriesStaleUntilTheirOwnInvalidation)
|
||||||
|
{
|
||||||
|
ForEachEngine([](M& m) {
|
||||||
|
m.MapSmallPage(0x94000000, 0x3000, 3, 0);
|
||||||
|
m.Bus().WriteARM(0x3000, 0xcafef00d);
|
||||||
|
m.Code(0x1000, {
|
||||||
|
0xe5921000, // ldr r1, [r2]: fills the TLB
|
||||||
|
0xe5854000, // str r4, [r5]: clears the page's descriptor
|
||||||
|
0xe5923000, // ldr r3, [r2]: the stale entry still translates
|
||||||
|
0xee086f37, // mcr p15, 0, r6, c8, c7, 1: another page's MVA
|
||||||
|
0xe5927000, // ldr r7, [r2]: still stale
|
||||||
|
0xee082f37, // mcr p15, 0, r2, c8, c7, 1: this page's MVA
|
||||||
|
0xe5920000, // ldr r0, [r2]: translation fault
|
||||||
|
});
|
||||||
|
m.Core().SetRegister(0, 0x5a5a5a5a);
|
||||||
|
m.Core().SetRegister(2, 0x94000000);
|
||||||
|
m.Core().SetRegister(4, 0);
|
||||||
|
m.Core().SetRegister(5, M::KERNEL + M::COARSE);
|
||||||
|
m.Core().SetRegister(6, 0x95000000);
|
||||||
|
m.Run(64);
|
||||||
|
EXPECT_EQ(m.Core().GetRegister(1), 0xcafef00du);
|
||||||
|
EXPECT_EQ(m.Core().GetRegister(3), 0xcafef00du);
|
||||||
|
EXPECT_EQ(m.Core().GetRegister(7), 0xcafef00du);
|
||||||
|
m.ExpectDataAbort(M::CODE + 24, 0x07, 0x94000000, M::SVC_CPSR);
|
||||||
|
EXPECT_EQ(m.Core().GetRegister(0), 0x5a5a5a5au);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StarletARMCoreMMUFaults, DACRChangesNeedNoTLBMaintenance)
|
||||||
|
{
|
||||||
|
ForEachEngine([](M& m) {
|
||||||
|
m.MapSection(0x960, 0, 3, 6);
|
||||||
|
m.Core().GetCP15State().domain_access_control = 1 | (1 << 12);
|
||||||
|
m.Bus().WriteARM(0x2000, 0x76543210);
|
||||||
|
m.Code(0x1000, {0xe5921000, 0xee034f10, 0xe5923000}); // ldr; mcr c3 (r4); ldr r3
|
||||||
|
m.Core().SetRegister(2, 0x96002000);
|
||||||
|
m.Core().SetRegister(3, 0x33333333);
|
||||||
|
m.Core().SetRegister(4, 1);
|
||||||
|
m.Run();
|
||||||
|
EXPECT_EQ(m.Core().GetRegister(1), 0x76543210u);
|
||||||
|
m.ExpectDataAbort(M::CODE + 8, 0x69, 0x96002000, M::SVC_CPSR);
|
||||||
|
EXPECT_EQ(m.Core().GetRegister(3), 0x33333333u);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StarletARMCoreMMUFaults, FCSEFaultsReportTheModifiedAddress)
|
||||||
|
{
|
||||||
|
ForEachEngine([](M& m) {
|
||||||
|
m.Core().GetCP15State().process_id = 0x02000000;
|
||||||
|
m.Code(0x1000, {0xe5921000}); // ldr r1, [r2]
|
||||||
|
m.Core().SetRegister(2, 0x00001000);
|
||||||
|
m.Run();
|
||||||
|
m.ExpectDataAbort(M::CODE, 0x05, 0x02001000, M::SVC_CPSR);
|
||||||
|
});
|
||||||
|
ForEachEngine([](M& m) {
|
||||||
|
m.Core().GetCP15State().process_id = 0x02000000;
|
||||||
|
m.MapSection(0x020, 0, 3, 0);
|
||||||
|
m.Code(0x1000, {0xe5921000}); // ldr r1, [r2]: MVA 0x02001000, this instruction itself
|
||||||
|
m.Core().SetRegister(2, 0x00001000);
|
||||||
|
m.Run(4);
|
||||||
|
EXPECT_EQ(m.Core().GetRegister(1), 0xe5921000u);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StarletARMCoreMMUFaults, ThumbDataAbortReturnsToTheInstructionPlusEight)
|
||||||
|
{
|
||||||
|
ForEachEngine([](M& m) {
|
||||||
|
m.Bus().WriteThumb(0x1080, 0x6811); // ldr r1, [r2, #0]
|
||||||
|
m.Core().SetRegister(2, 0x90000000);
|
||||||
|
m.Start(M::SVC_CPSR | 0x20, M::CODE + 0x80);
|
||||||
|
m.Run();
|
||||||
|
m.ExpectDataAbort(M::CODE + 0x80, 0x05, 0x90000000, M::SVC_CPSR | 0x20);
|
||||||
|
EXPECT_EQ(m.Core().GetCPSR() & 0x20, 0u);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StarletARMCoreMMUFaults, TheJITStillRunsNativeCode)
|
||||||
|
{
|
||||||
|
#if defined(_M_X86_64)
|
||||||
|
MMUFaultMachine m(true);
|
||||||
|
m.Code(0x1000, {
|
||||||
|
0xe3a00000, // mov r0, #0
|
||||||
|
0xe2800001, // add r0, r0, #1
|
||||||
|
0xe3500010, // cmp r0, #16
|
||||||
|
0x1afffffc, // bne 0x1004
|
||||||
|
0xeafffffe, // b .
|
||||||
|
});
|
||||||
|
m.Run(96);
|
||||||
|
EXPECT_EQ(m.Core().GetRegister(0), 16u);
|
||||||
|
EXPECT_GT(m.Core().GetJitNativeExecutedInstructions(), 0u);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StarletARMCoreMMUFaults, DisabledFaultsKeepTheIdentityFallbackAndUserCP15)
|
||||||
|
{
|
||||||
|
// The IOS LLE model is unchanged unless SetMMUFaultsEnabled(true) is called.
|
||||||
|
for (const bool jit : {false, true})
|
||||||
|
{
|
||||||
|
#if !defined(_M_X86_64)
|
||||||
|
if (jit)
|
||||||
|
continue;
|
||||||
|
#endif
|
||||||
|
SCOPED_TRACE(jit ? "JIT" : "interpreter");
|
||||||
|
MMUFaultMachine m(jit);
|
||||||
|
m.Core().SetMMUFaultsEnabled(false);
|
||||||
|
m.Bus().WriteARM(0x2000, 0x44556677);
|
||||||
|
m.Code(0x1000, {0xe5921000, 0xee034f10, 0xeafffffe}); // ldr; mcr c3 (r4); b .
|
||||||
|
m.Core().SetRegister(2, 0x00002000); // section 0x000: no descriptor
|
||||||
|
m.Core().SetRegister(4, 0x55555555);
|
||||||
|
m.Start(M::USER_CPSR, M::USER_CODE);
|
||||||
|
m.Run(8);
|
||||||
|
EXPECT_EQ(m.Core().GetRegister(1), 0x44556677u);
|
||||||
|
EXPECT_EQ(m.Core().GetCP15State().domain_access_control, 0x55555555u);
|
||||||
|
EXPECT_EQ(m.Core().GetCPSR() & 0x1f, 0x10u);
|
||||||
|
}
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
} // namespace IOS::LLE
|
} // namespace IOS::LLE
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <array>
|
#include <array>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
@@ -232,5 +233,80 @@ TEST(ARMCoreTest, Armv5DoublewordTransfersAndAlignmentAbort)
|
|||||||
EXPECT_EQ(core.GetRegister(14), TEXT_BASE + 8);
|
EXPECT_EQ(core.GetRegister(14), TEXT_BASE + 8);
|
||||||
EXPECT_EQ(core.GetCP15State().fault_address, DATA_BASE + 4);
|
EXPECT_EQ(core.GetCP15State().fault_address, DATA_BASE + 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StarOS ADR-0014 R6: the optional panic words. The string table moves past the section headers
|
||||||
|
// to make room for two more symbols.
|
||||||
|
std::array<u8, 0x300> MakePanicImage(u32 state, u32 code)
|
||||||
|
{
|
||||||
|
auto image = MakeImage();
|
||||||
|
constexpr char names[] =
|
||||||
|
"\0REV_TRACE\0__fault_code\0__fault_lr\0__fault_spsr\0__panic_state\0__panic_code";
|
||||||
|
for (size_t index = 0; index < sizeof(names); ++index)
|
||||||
|
image[0x278 + index] = static_cast<u8>(names[index]);
|
||||||
|
Put32(image, 0x228 + 16, 0x278);
|
||||||
|
Put32(image, 0x228 + 20, sizeof(names));
|
||||||
|
Put32(image, 0x250 + 20, 7 * 16);
|
||||||
|
const auto symbol = [&image](size_t index, u32 name, u32 address) {
|
||||||
|
const size_t offset = 0x120 + index * 16;
|
||||||
|
Put32(image, offset, name);
|
||||||
|
Put32(image, offset + 4, address);
|
||||||
|
Put32(image, offset + 8, 4);
|
||||||
|
};
|
||||||
|
// An omitted word keeps its slot, with the empty name.
|
||||||
|
symbol(5, state != 0 ? 48 : 0, state);
|
||||||
|
symbol(6, code != 0 ? 62 : 0, code);
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(BootstrapELFTest, PanicWordsAreOptionalButBothInData)
|
||||||
|
{
|
||||||
|
RecordingBus bus;
|
||||||
|
BootstrapELFInfo info;
|
||||||
|
std::string error;
|
||||||
|
ASSERT_TRUE(LoadBootstrapELF(MakeImage(), bus, &info, &error)) << error;
|
||||||
|
EXPECT_EQ(info.panic_state, 0u);
|
||||||
|
EXPECT_EQ(info.panic_code, 0u);
|
||||||
|
|
||||||
|
ASSERT_TRUE(LoadBootstrapELF(MakePanicImage(DATA_BASE + 0x41c, DATA_BASE + 4), bus, &info, &error))
|
||||||
|
<< error;
|
||||||
|
EXPECT_EQ(info.panic_state, DATA_BASE + 0x41c);
|
||||||
|
EXPECT_EQ(info.panic_code, DATA_BASE + 4);
|
||||||
|
|
||||||
|
for (const auto [state, code] : {std::pair{DATA_BASE + 0x41c, 0u}, std::pair{0u, DATA_BASE + 4},
|
||||||
|
std::pair{DATA_BASE + 0x41c, DATA_BASE + 0x420}})
|
||||||
|
{
|
||||||
|
RecordingBus rejected;
|
||||||
|
EXPECT_FALSE(LoadBootstrapELF(MakePanicImage(state, code), rejected, &info, &error));
|
||||||
|
EXPECT_EQ(error, "bootstrap ELF has an invalid panic symbol");
|
||||||
|
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
|
||||||
} // namespace IOS::LLE
|
} // 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
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
// Copyright 2026 Dolphin Emulator Project
|
// Copyright 2026 Dolphin Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -16,9 +18,14 @@
|
|||||||
#include "Core/IOS/Starlet/BootstrapELF.h"
|
#include "Core/IOS/Starlet/BootstrapELF.h"
|
||||||
#include "Core/IOS/Starlet/StarletMemory.h"
|
#include "Core/IOS/Starlet/StarletMemory.h"
|
||||||
#include "Core/System.h"
|
#include "Core/System.h"
|
||||||
|
#include "ProbeBudget.h"
|
||||||
|
#include "ProbeRing.h"
|
||||||
|
#include "StarOSPanicCheck.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
using namespace StarletBootstrapProbe;
|
||||||
|
|
||||||
enum class ExpectedOutcome
|
enum class ExpectedOutcome
|
||||||
{
|
{
|
||||||
Idle,
|
Idle,
|
||||||
@@ -31,6 +38,9 @@ enum class ExpectedOutcome
|
|||||||
// Exit status of a run stopped by a Dolphin alert (1: failed expectation, 2: usage).
|
// Exit status of a run stopped by a Dolphin alert (1: failed expectation, 2: usage).
|
||||||
constexpr int ALERT_EXIT_STATUS = 3;
|
constexpr int ALERT_EXIT_STATUS = 3;
|
||||||
|
|
||||||
|
// 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
|
// 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.
|
// probe until someone closes it. Report the alert on stderr and fail the run at once instead.
|
||||||
bool FailOnAlert(const char* caption, const char* text, bool, Common::MsgType)
|
bool FailOnAlert(const char* caption, const char* text, bool, Common::MsgType)
|
||||||
@@ -40,7 +50,8 @@ bool FailOnAlert(const char* caption, const char* text, bool, Common::MsgType)
|
|||||||
std::_Exit(ALERT_EXIT_STATUS);
|
std::_Exit(ALERT_EXIT_STATUS);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Run(const std::string& path, bool jit, 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);
|
std::ifstream file(path, std::ios::binary | std::ios::ate);
|
||||||
if (!file || file.tellg() <= 0 || file.tellg() > 16 * 1024 * 1024)
|
if (!file || file.tellg() <= 0 || file.tellg() > 16 * 1024 * 1024)
|
||||||
@@ -71,16 +82,40 @@ bool Run(const std::string& path, bool jit, ExpectedOutcome expected)
|
|||||||
}
|
}
|
||||||
|
|
||||||
IOS::LLE::ARMCore core(memory);
|
IOS::LLE::ARMCore core(memory);
|
||||||
|
// Declared after memory, so destroyed first.
|
||||||
|
memory.AttachInterruptLines(&core);
|
||||||
core.SetJitEnabled(jit);
|
core.SetJitEnabled(jit);
|
||||||
core.Reset(info.entry);
|
core.Reset(info.entry);
|
||||||
core.SetBigEndian(true);
|
core.SetBigEndian(true);
|
||||||
|
// StarOS MMU-001: translation, domain, permission and alignment aborts (off by default).
|
||||||
|
core.SetMMUFaultsEnabled(mmu_faults);
|
||||||
constexpr u32 TRACE_MAGIC = 0x52455631;
|
constexpr u32 TRACE_MAGIC = 0x52455631;
|
||||||
constexpr u32 TRACE_CAPACITY = 1024;
|
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.SetIRQLine(system.GetWiiIPC().IsStarletIRQAsserted());
|
||||||
core.SetFIQLine(system.GetWiiIPC().IsStarletFIQAsserted());
|
core.SetFIQLine(system.GetWiiIPC().IsStarletFIQAsserted());
|
||||||
core.RunCycles(4096);
|
core.RunCycles(SLICE_CYCLES);
|
||||||
const u32 fault_code = memory.Read32(info.fault_code);
|
const u32 fault_code = memory.Read32(info.fault_code);
|
||||||
if (fault_code != 0)
|
if (fault_code != 0)
|
||||||
{
|
{
|
||||||
@@ -90,6 +125,26 @@ bool Run(const std::string& path, bool jit, ExpectedOutcome expected)
|
|||||||
return expected == ExpectedOutcome::UndefinedFault && fault_code == 1 &&
|
return expected == ExpectedOutcome::UndefinedFault && fault_code == 1 &&
|
||||||
memory.Read32(info.fault_lr) != 0 && memory.Read32(info.fault_spsr) != 0;
|
memory.Read32(info.fault_lr) != 0 && memory.Read32(info.fault_spsr) != 0;
|
||||||
}
|
}
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
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)
|
if (memory.Read32(info.trace) != TRACE_MAGIC)
|
||||||
continue;
|
continue;
|
||||||
const u32 length = memory.Read32(info.trace + 4);
|
const u32 length = memory.Read32(info.trace + 4);
|
||||||
@@ -139,7 +194,16 @@ bool Run(const std::string& path, bool jit, ExpectedOutcome expected)
|
|||||||
expected == ExpectedOutcome::Idle && trace.find("REV:BOOT:IDLE\n") != std::string::npos;
|
expected == ExpectedOutcome::Idle && trace.find("REV:BOOT:IDLE\n") != std::string::npos;
|
||||||
return startup_ok && outcome_ok;
|
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';
|
<< core.GetRegister(15) << std::dec << '\n';
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -148,20 +212,47 @@ bool Run(const std::string& path, bool jit, ExpectedOutcome expected)
|
|||||||
int main(int argc, char** argv)
|
int main(int argc, char** argv)
|
||||||
{
|
{
|
||||||
Common::RegisterMsgAlertHandler(FailOnAlert);
|
Common::RegisterMsgAlertHandler(FailOnAlert);
|
||||||
if (argc < 2 || argc > 4)
|
if (argc < 2 || argc > 8)
|
||||||
{
|
{
|
||||||
std::cerr << "Usage: StarletBootstrapProbe <ELF> [--jit] "
|
std::cerr << "Usage: StarletBootstrapProbe <ELF> [--jit] [--mmu-faults] "
|
||||||
"[--expect-panic|--expect-fatal-data|--expect-undefined-fault|--expect-phase2]\n";
|
"[--sbr1-record|--sbr1-absent] "
|
||||||
|
"[--expect-panic|--expect-fatal-data|--expect-undefined-fault|--expect-phase2] "
|
||||||
|
"[--cycles <n>]\n";
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
bool jit = false;
|
bool jit = false;
|
||||||
|
bool mmu_faults = false;
|
||||||
|
SBR1Mode sbr1 = SBR1Mode::None;
|
||||||
ExpectedOutcome expected = ExpectedOutcome::Idle;
|
ExpectedOutcome expected = ExpectedOutcome::Idle;
|
||||||
bool outcome_selected = false;
|
bool outcome_selected = false;
|
||||||
|
std::optional<u64> cycles;
|
||||||
for (int index = 2; index < argc; ++index)
|
for (int index = 2; index < argc; ++index)
|
||||||
{
|
{
|
||||||
const std::string_view option = argv[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;
|
jit = true;
|
||||||
|
else if (option == "--mmu-faults")
|
||||||
|
mmu_faults = true;
|
||||||
|
else if (option == "--sbr1-record" || option == "--sbr1-absent")
|
||||||
|
{
|
||||||
|
if (sbr1 != SBR1Mode::None)
|
||||||
|
{
|
||||||
|
std::cerr << "Select only one SBR1 mode\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
sbr1 = option == "--sbr1-record" ? SBR1Mode::Record : SBR1Mode::Absent;
|
||||||
|
}
|
||||||
else if (option == "--expect-panic" || option == "--expect-fatal-data" ||
|
else if (option == "--expect-panic" || option == "--expect-fatal-data" ||
|
||||||
option == "--expect-undefined-fault" || option == "--expect-phase2")
|
option == "--expect-undefined-fault" || option == "--expect-phase2")
|
||||||
{
|
{
|
||||||
@@ -189,7 +280,46 @@ int main(int argc, char** argv)
|
|||||||
auto& system = Core::System::GetInstance();
|
auto& system = Core::System::GetInstance();
|
||||||
system.SetIsWii(true);
|
system.SetIsWii(true);
|
||||||
system.GetMemory().Init();
|
system.GetMemory().Init();
|
||||||
const bool passed = Run(argv[1], jit, expected);
|
auto& memory = system.GetMemory();
|
||||||
|
if (sbr1 == SBR1Mode::Record)
|
||||||
|
{
|
||||||
|
// As the Broadway loader and the ARM relay leave StarOS's SBR1 boot record before the image
|
||||||
|
// runs (StarOS docs/contracts/wii-dev-boot.md): header, the relay at its last step, EXI
|
||||||
|
// 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 + 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, 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 (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 + 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();
|
system.GetMemory().Shutdown();
|
||||||
return passed ? 0 : 1;
|
return passed ? 0 : 1;
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user