Compare commits

..
Author SHA1 Message Date
Yaya48andClaude Opus 5.5 dd2336f278 Starlet: keep a matched alarm's timer cause until HW_ALARM is rewritten
A console run (StarOS M3C gate H1, 2026-10-16) acknowledged the timer
through HW_ARMIRQFLAG before rewriting HW_ALARM, tens of microseconds after
the match, and read the cause still set. MINI's IRQ handler rewrites
HW_ALARM first, then acknowledges. The model fired the cause once on the
crossing, and a write-one-to-clear ended it for good, so the probe hid the
difference.

Once the counter reaches HW_ALARM, the timer's cause now stays asserted:
an acknowledgement sets it again until HW_ALARM is written. Writing the
comparator re-arms it (an immediate match when equal to the counter, as
before); a counter write does not end the match. One run does not tell a
latch from a "counter >= alarm" comparison: the latch is the smaller model
that explains it. The state is saved (STATE_VERSION 197).

Tests: StarletTimer.MatchedAlarmStaysAssertedUntilRewritten (both write
paths, a counter write, MINI's order, the next match) and
MatchedAlarmSurvivesStateRoundTrip.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-16 12:51:16 +02:00
Yaya48andClaude Opus 5.5 6c44367b5c Starlet: re-sample the interrupt lines on HW_ARMFIQMASK writes too
Review of d486779f50 (StarOS decision 44):
- A write to HW_ARMFIQMASK re-samples the attached core's IRQ and FIQ lines,
  as writes to HW_ARMIRQFLAG and HW_ARMIRQMASK do.
- Tests: FIQ masking and unmasking of a latched cause (wide and
  byte-assembled writes, then an acknowledgement); two pending IRQ sources,
  where acknowledging one keeps the line asserted and acknowledging the
  last lowers it.

The attached core cannot be used after it is destroyed: Starlet::Shutdown
resets m_core then m_memory with nothing in between, ~Starlet destroys
m_core (declared after m_memory) first, Init attaches only after the last
path that can fail, StarletMemory::Reset keeps the core, which is still
alive, and the probe's core is a local declared after its memory.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-16 02:49:39 +02:00
Yaya48andClaude Opus 5.5 d486779f50 Starlet: re-sample the ARM IRQ and FIQ lines on interrupt register writes
The core's IRQ and FIQ lines were set from WiiIPC's flags and masks only at
the start of each scheduler slice (Starlet::RunSlice, StarletBootstrapProbe).
Acknowledging an interrupt (HW_ARMIRQFLAG, write one to clear) or masking it
(HW_ARMIRQMASK) left the line asserted until the next slice, so code that
acknowledged an interrupt and unmasked IRQ within the same slice took it
again with no cause pending. Hollywood's interrupt output is level-sensitive.

StarletMemory now re-samples the lines of the core attached with
AttachInterruptLines on every write to those two registers, on both its
wide and byte-assembled write paths; Starlet and StarletBootstrapProbe
attach theirs. Raising a cause still reaches the core at the next slice, as
before. ARMCore exposes IsIRQLineAsserted and IsFIQLineAsserted for the
unit test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-16 02:19:55 +02:00
Yaya48andClaude Opus 5.5 bef2c8cf02 StarletBootstrapProbe: print the SBR1 ring with --sbr1-record
The probe printed the ring's length only. StarOS's EXP-003 images put
lines in the ring that REV_TRACE has no room for (the MEM2 monitor, the
measurements), so under Dolphin nothing showed them. After the record's
summary, --sbr1-record now prints the ring's text between "SBR1 RING
BEGIN" and "SBR1 RING END" on stderr, as StarOS's Broadway loader prints
it over the Gecko on the console: at most the ring's 4032 bytes, a byte
outside printable ASCII and newline, and the backslash, written \xHH.

The formatting is in ProbeRing.h, tested by StarletProbeRingTest.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 17:50:01 +02:00
Yaya48andClaude Opus 5.5 b457eb18c3 StarletBootstrapProbe: --cycles
The probe ran every image for a fixed 512 slices of 4096 ARM cycles
(2097152). StarOS's EXP-003 images hash MEM2 and time series of
operations, which need more (StarOS experiments/proposals/mmu-console.md,
P-7 and P-8). --cycles <n> sets the budget: a decimal number of cycles,
a whole number of slices, from one slice to 2^30, given once; anything
else is a usage error (exit 2), never rounded. Without it the probe runs
2097152 cycles, as before, and the timeout messages name the budget.

The parser is in ProbeBudget.h, tested by StarletProbeBudgetTest.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 17:34:06 +02:00
Yaya48andClaude Opus 5.5 6ede30f99e StarletBootstrapProbe: wait for StarOS's whole panic path
--expect-panic passed as soon as __panic_state was nonzero at the end of
a 4096-cycle slice. StarOS's handler stores the guard first, then the
code, then publishes the SBR1 record, so the run could stop with the path
half done. The probe now keeps running until the path is finished and
still is two slices later, and fails if the run budget ends first:
- __panic_state 1 and __panic_code 1;
- with --sbr1-record: STARLET_STATE 5 (Panic), FATAL_REASON 0 and
  EXI_OWNER 1 (handed back to the PPC);
- with --sbr1-absent: every word of the record area still zero.
The failure names the first unmet condition. The check is
StarOSPanicCheck.h, pure, with the SBR1 constants moved there, and
StarletProbePanicCheckTest covers each condition; each of 7 mutants of
the check fails a test. Runs that do not expect a panic are unchanged.

With StarOS's panic test image (eb930ee), interpreter and JIT: the
scenarios pass, two slices later than before. Three images derived from
it pass the previous probe and now fail: EXI kept by the Starlet
(--sbr1-record), __panic_code never stored, the record written although
absent (--sbr1-absent). (StarOS MMU-001 review of F1.)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 00:20:23 +02:00
Yaya48andClaude Opus 5.5 a62c65615a IOS LLE: StarOS's panic words must not alias
LoadBootstrapELF accepted __panic_state and __panic_code at the same
address, or as two words sharing bytes: the code's store would then also
set the guard, and nothing could tell the two apart. They must now be two
non-overlapping words of the data segment, like the other checks, before
any byte is written. BootstrapELFTest.PanicWordsMustNotAlias: the same
word and three partial overlaps, all inside the data segment, are
rejected; adjacent words are accepted. With the rule disabled, the test
fails. (StarOS MMU-001 review of F1.)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 00:20:22 +02:00
Yaya48andClaude Opus 5.5 15da28ad7b StarletBootstrapProbe: --sbr1-record and --sbr1-absent
StarOS's SBR1 boot record (MEM1 0x00200000) is created by its Broadway
loader before the image runs; the probe never had one, so the paths
that publish into it ran only on the console. --sbr1-record writes the
record as the loader and the ARM relay leave it (header, relay at its
last step, EXI channel 1 given to the Starlet, magic last); after the run
both options print the record's magic, state, reason, EXI owner, fault
code, ring length and dropped count, and how many of its 1024 words are
nonzero (0 is expected with --sbr1-absent). Without either option the
probe behaves as before.

With the StarOS M1 images (interpreter and JIT): idle publishes state 3
and hands EXI back (owner 1, ring 258 bytes, no Gecko emulated);
fatal-data state 4 reason 3; undefined-entry state 6, fault 1, ring
empty, as on the console (gates HW2-HW4).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 22:40:28 +02:00
Yaya48andClaude Opus 5.5 2bae49bdad IOS LLE: the ARM926 TLB is unified (StarOS MMU-001 F1 review)
The ARM926EJ-S has one TLB for instructions and data, with lockdown
entries that whole-TLB invalidation preserves. Two comments said the
I and D TLBs were separate; they now say what the software TLB really
differs in: capacity and replacement (a direct-mapped cache of 4096
1 KiB granules), and lockdown, which is not modelled (c10 writes are
ignored). No code change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 22:03:22 +02:00
Yaya48andClaude Opus 5.5 612cab7b30 StarletBootstrapProbe: report StarOS's panic words
StarOS's panic path (ADR-0014 R6) writes no trace line: it sets the guard
word __panic_state, stores __panic_code and halts. LoadBootstrapELF now
returns both symbols when an image has them (optional, but both or
neither, as words of the data segment), and the probe stops a run whose
guard is set: it prints the trace published so far and
"Starlet panic state <n> code <n>", and passes --expect-panic when the
state is 1. Images without these symbols (StarletOS) load and run as
before; a REV:PANIC trace line still satisfies --expect-panic.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 21:21:58 +02:00
Yaya48andClaude Opus 5.5 cdec826bb8 StarletBootstrapProbe: --mmu-faults
Enables the ARM926 MMU aborts of ARMCore (SetMMUFaultsEnabled) for the
run. Without the option the probe behaves as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 21:09:18 +02:00
Yaya48andClaude Opus 5.5 9b493a1e16 IOS LLE: optional ARM926 MMU aborts and CP15 privilege (StarOS MMU-001 F1)
ARMCore::SetMMUFaultsEnabled(true) makes the Starlet core deliver the
faults of the ARM926EJ-S MMU instead of the identity fallback:
- translation (section 0x5, page 0x7), domain (0x9, 0xb) and permission
  (0xd, 0xf) faults from the descriptors, the DACR (no access and the
  reserved value fault; manager skips AP) and AP with the S and R bits;
  ARMv5 has no execute permission, so a fetch needs read permission;
- data aborts (vector 0x10, LR = instruction + 8 in both states) set the
  FSR and the FAR (the MVA); prefetch aborts (vector 0x0c, LR + 4) set the
  IFSR (c5 opcode2 1) and leave the FAR unchanged;
- alignment faults (0x1) when control bit A is set;
- base-restored data aborts: the first faulting access ends the
  instruction's bus traffic and its register results are undone; words
  stored before the fault stay written;
- LDRT/STRT/LDRBT/STRBT check User permissions in any mode;
- the TLB keeps each granule's domain and AP, never caches a faulting
  walk, and c8 single-entry operations drop only the page that maps the
  MVA, so an entry stays stale until the guest invalidates it;
- MRC/MCR to CP15 from User mode is an undefined instruction.

The JIT stays usable: with the option on, every block entry goes through
C++, which checks the fetch under the current mode and DACR, and only
instructions that touch neither memory, CP15, the PSRs nor the exception
model run natively; the rest use the interpreter.

Off by default. The IOS LLE path keeps its identity fallback, its
savestate format (the option and the IFSR are not serialized) and its
idle-loop fast-forwarding; the only change there is that the data
accessors read one abort flag that stays clear.

20 StarletARMCoreMMUFaults tests, each run in the interpreter and the
JIT; the existing Starlet tests pass unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 21:09:18 +02:00
20 changed files with 1946 additions and 50 deletions

No files matched your search

+413 -30
View File
@@ -26,8 +26,29 @@ namespace
{
constexpr u32 CP15_CONTROL_BIG_ENDIAN = 1U << 7;
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;
// 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)
{
return static_cast<u32>(mode);
@@ -205,6 +226,16 @@ u32 ARMCore::TranslateVirtualAddress(u32 address) const
if ((m_cp15.control & CP15_CONTROL_MMU) == 0)
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
// translation.
const u32 modified_address =
@@ -235,6 +266,11 @@ u32 ARMCore::TranslateVirtualAddressForJit(u32 address, const u8** first_descrip
*second_descriptor_value = 0;
if ((m_cp15.control & CP15_CONTROL_MMU) == 0)
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 =
address < 0x02000000 ? address | (m_cp15.process_id & 0xfe000000) : address;
@@ -339,6 +375,272 @@ void ARMCore::InvalidateTLB()
#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)
{
const u32 physical_address = TranslateVirtualAddress(address);
@@ -391,35 +693,61 @@ void ARMCore::InvalidateInstructionCache()
#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
{
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)
{
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
{
const u16 value =
(address & 1) == 0 ?
m_bus.Read16(TranslateVirtualAddress(address)) :
static_cast<u16>((static_cast<u16>(m_bus.Read8(TranslateVirtualAddress(address))) << 8) |
m_bus.Read8(TranslateVirtualAddress(address + 1)));
const auto read8 = [this](u32 byte_address) -> u16 {
const u32 physical_address = DataAddress(byte_address, false);
return m_abort_pending ? 0 : m_bus.Read8(physical_address);
};
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));
}
u32 ARMCore::Read32(u32 address) const
{
const u32 value =
(address & 3) == 0 ?
m_bus.Read32(TranslateVirtualAddress(address)) :
(static_cast<u32>(m_bus.Read8(TranslateVirtualAddress(address))) << 24) |
(static_cast<u32>(m_bus.Read8(TranslateVirtualAddress(address + 1))) << 16) |
(static_cast<u32>(m_bus.Read8(TranslateVirtualAddress(address + 2))) << 8) |
m_bus.Read8(TranslateVirtualAddress(address + 3));
const auto read8 = [this](u32 byte_address) -> u32 {
const u32 physical_address = DataAddress(byte_address, false);
return m_abort_pending ? 0 : m_bus.Read8(physical_address);
};
u32 value;
if ((address & 3) == 0)
{
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)
return value;
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));
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;
}
m_bus.Write8(TranslateVirtualAddress(address), static_cast<u8>(value >> 8));
m_bus.Write8(TranslateVirtualAddress(address + 1), static_cast<u8>(value));
WriteByte(address, static_cast<u8>(value >> 8));
WriteByte(address + 1, static_cast<u8>(value));
}
void ARMCore::Write32(u32 address, u32 value)
@@ -447,13 +777,15 @@ void ARMCore::Write32(u32 address, u32 value)
}
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;
}
m_bus.Write8(TranslateVirtualAddress(address), static_cast<u8>(value >> 24));
m_bus.Write8(TranslateVirtualAddress(address + 1), static_cast<u8>(value >> 16));
m_bus.Write8(TranslateVirtualAddress(address + 2), static_cast<u8>(value >> 8));
m_bus.Write8(TranslateVirtualAddress(address + 3), static_cast<u8>(value));
WriteByte(address, static_cast<u8>(value >> 24));
WriteByte(address + 1, static_cast<u8>(value >> 16));
WriteByte(address + 2, static_cast<u8>(value >> 8));
WriteByte(address + 3, static_cast<u8>(value));
}
u32 ARMCore::ReadRegisterOperand(u32 index) const
@@ -769,11 +1101,23 @@ int ARMCore::StepInternal(bool advance_bus)
m_instruction_address = m_registers[15];
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);
ExecuteThumb(instruction);
TryEnterThumbMemoryPoll(instruction);
if (m_mmu_faults)
{
ExecuteThumbWithAborts(instruction);
}
else
{
ExecuteThumb(instruction);
TryEnterThumbMemoryPoll(instruction);
}
}
else
{
@@ -790,9 +1134,13 @@ int ARMCore::StepInternal(bool advance_bus)
}
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)
@@ -1460,6 +1808,7 @@ void ARMCore::ExecuteARM(u32 instruction)
}
else
{
CheckAlignment(address, 3);
const u32 old = Read32(address & ~3U);
Write32(address & ~3U, ReadRegisterOperand(rm));
m_registers[rd] = old;
@@ -1734,6 +2083,10 @@ void ARMCore::ExecuteThumb(u16 instruction)
const u32 rb = (instruction >> 3) & 7;
const u32 rd = instruction & 7;
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)
{
case 0: // STR
@@ -1773,6 +2126,8 @@ void ARMCore::ExecuteThumb(u16 instruction)
const u32 rb = (instruction >> 3) & 7;
const u32 rd = instruction & 7;
const u32 address = m_registers[rb] + offset;
if (!byte)
CheckAlignment(address, 3);
if (load)
{
m_registers[rd] = byte ? ReadByte(address) : Read32(address);
@@ -1794,6 +2149,7 @@ void ARMCore::ExecuteThumb(u16 instruction)
const bool load = (instruction & 0x0800) != 0;
const u32 address = m_registers[(instruction >> 3) & 7] + (((instruction >> 6) & 0x1f) << 1);
const u32 rd = instruction & 7;
CheckAlignment(address, 1);
if (load)
m_registers[rd] = Read16(address);
else
@@ -1807,6 +2163,7 @@ void ARMCore::ExecuteThumb(u16 instruction)
const bool load = (instruction & 0x0800) != 0;
const u32 rd = (instruction >> 8) & 7;
const u32 address = m_registers[13] + ((instruction & 0xff) << 2);
CheckAlignment(address, 3);
if (load)
m_registers[rd] = Read32(address);
else
@@ -1851,6 +2208,7 @@ void ARMCore::ExecuteThumb(u16 instruction)
if (!pop)
{
u32 address = m_registers[13] - count * 4;
CheckAlignment(address, 3);
m_registers[13] = address;
for (u32 reg = 0; reg < 8; ++reg)
{
@@ -1866,6 +2224,7 @@ void ARMCore::ExecuteThumb(u16 instruction)
else
{
u32 address = m_registers[13];
CheckAlignment(address, 3);
for (u32 reg = 0; reg < 8; ++reg)
{
if (register_list & (1U << reg))
@@ -1903,6 +2262,7 @@ void ARMCore::ExecuteThumb(u16 instruction)
return;
}
u32 address = m_registers[rb];
CheckAlignment(address, 3);
for (u32 reg = 0; reg < 8; ++reg)
{
if (!(register_list & (1U << reg)))
@@ -2121,6 +2481,10 @@ void ARMCore::ExecuteSingleDataTransfer(u32 instruction)
const u32 base = ReadRegisterOperand(rn);
const u32 indexed = add_offset ? base + offset : base - offset;
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)
{
u32 value;
@@ -2147,6 +2511,7 @@ void ARMCore::ExecuteSingleDataTransfer(u32 instruction)
else
Write32(address & ~3U, value);
}
m_user_access = false;
if ((!preindex || writeback) && !(load && rn == rd))
m_registers[rn] = indexed;
@@ -2203,6 +2568,8 @@ void ARMCore::ExecuteHalfwordDataTransfer(u32 instruction)
return;
}
if (type == 1 || type == 3)
CheckAlignment(address, 1);
if (load)
{
u32 value;
@@ -2280,6 +2647,7 @@ void ARMCore::ExecuteBlockDataTransfer(u32 instruction)
address = preindex ? base - count * 4 : base - (count - 1) * 4;
if (!m_watch_ranges.empty())
TraceWatchedBlockTransfer(address, count * 4, load);
CheckAlignment(address, 3);
u32 loaded_pc = 0;
bool pc_loaded = false;
@@ -2475,6 +2843,12 @@ void ARMCore::ExecuteCoprocessor(u32 instruction)
UndefinedInstruction(instruction);
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)
{
@@ -2699,7 +3073,8 @@ u32 ARMCore::ReadCP15(u32 opcode1, u32 crn, u32 crm, u32 opcode2) const
case 3:
return m_cp15.domain_access_control;
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:
return m_cp15.fault_address;
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;
break;
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;
case 6:
m_cp15.fault_address = value;
@@ -2752,7 +3130,12 @@ void ARMCore::WriteCP15(u32 opcode1, u32 crn, u32 crm, u32 opcode2, u32 value)
InvalidateInstructionCache();
break;
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;
case 13:
// ARM926 FCSE is specifically designed to switch low-address process spaces without flushing
+64 -3
View File
@@ -159,6 +159,8 @@ public:
}
bool IsWaitingForInterrupt() const { return m_waiting_for_interrupt; }
bool IsWaitingForMemoryPoll() const { return m_waiting_for_memory_poll; }
bool IsIRQLineAsserted() const { return m_irq_line; }
bool IsFIQLineAsserted() const { return m_fiq_line; }
u32 GetMemoryPollAddress() const { return m_memory_poll_address; }
bool IsWaitingForExternalEvent() const
{
@@ -172,6 +174,15 @@ public:
u64 GetExecutedInstructions() const { return m_executed_instructions; }
void SetJitEnabled(bool 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; }
u64 GetTraceGeneration() const { return m_trace_generation; }
u64 GetJitExecutedInstructions() const;
@@ -246,6 +257,26 @@ private:
u32 virtual_page = 0;
u32 physical_page = 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
@@ -291,6 +322,20 @@ private:
u32* second_descriptor_value) const;
u32 ReadPhysical32(u32 address) const;
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);
u32 FetchARMInstruction(u32 address);
void InvalidateInstructionCache();
@@ -356,9 +401,13 @@ private:
u32 m_spsr_und = 0;
CP15State m_cp15{};
// ARM926 has separate instruction/data TLBs. A unified 1 KiB-granularity
// software cache keeps the observable translations while avoiding a
// page-table walk on every interpreted access.
// The ARM926EJ-S has one TLB for instructions and data: set-associative main entries plus
// lockdown entries (c10) that whole-TLB invalidation preserves (ARM926EJ-S TRM). This software
// 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{};
std::array<InstructionCacheEntry, INSTRUCTION_CACHE_ENTRY_COUNT> m_instruction_cache{};
mutable u32 m_tlb_generation = 1;
@@ -386,6 +435,18 @@ private:
u64 m_next_profile_instruction = PROFILE_SAMPLE_INTERVAL;
std::unordered_map<u32, u64> m_pc_samples;
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)
std::unique_ptr<ARMJitX64> m_jit;
#endif
+85 -4
View File
@@ -194,6 +194,11 @@ void ARMJitX64::GenerateDispatcher()
MOV(64, R(RAX), ImmPtr(&m_clear_pending));
CMP(8, MatR(RAX), Imm8(0));
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
// 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_PARAM2), Imm32(static_cast<u32>(DispatchReason::PageMiss)));
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_no_mmu);
MOV(32, R(ABI_PARAM4), R(EDX));
@@ -387,6 +397,7 @@ void ARMJitX64::GenerateDispatcher()
SetJumpTarget(dispatch_reason_translation);
SetJumpTarget(dispatch_reason_key);
SetJumpTarget(dispatch_reason_page);
SetJumpTarget(dispatch_reason_checked);
MOV(64, R(ABI_PARAM1), ImmPtr(this));
ABI_CallFunction(Dispatch);
TEST(64, R(RAX), R(RAX));
@@ -427,9 +438,62 @@ size_t ARMJitX64::GetFastEntrySetIndex(u32 key)
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,
u32 generated_set_offset)
{
if (reason == DispatchReason::Checked)
return DispatchChecked(jit);
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 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)
{
// 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
// 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.
@@ -1862,6 +1930,8 @@ void ARMJitX64::EmitExchangeBranch(OpArg target, bool link, u32 return_address)
bool ARMJitX64::EmitDirectThumb(u16 instruction, u32 address, bool* terminal)
{
if (m_core.m_mmu_faults && !IsCheckedNativeThumb(instruction))
return false;
if ((instruction & 0xff00) == 0xdf00)
{
FlushRegisterCache();
@@ -2911,8 +2981,15 @@ void ARMJitX64::FallbackThumb(ARMJitX64* jit, u16 instruction, u32 address)
core->RecordJitFallback(address, true);
core->m_instruction_address = address;
core->m_pc_written = false;
core->ExecuteThumb(instruction);
core->TryEnterThumbMemoryPoll(instruction);
if (core->m_mmu_faults)
{
core->ExecuteThumbWithAborts(instruction);
}
else
{
core->ExecuteThumb(instruction);
core->TryEnterThumbMemoryPoll(instruction);
}
if (!core->m_pc_written)
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))
{
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)
core->m_registers[15] = address + ((core->m_cpsr & ARMCore::CPSR_T) ? 2 : 4);
}
+7
View File
@@ -157,6 +157,8 @@ private:
KeyMiss,
PageMiss,
EmptyEntry,
// MMU faults enabled: every block entry checks its fetch (ARMCore::SetMMUFaultsEnabled).
Checked,
};
void PoisonMemory() override;
@@ -172,6 +174,11 @@ private:
u32* first_descriptor_value_out, const u8** second_descriptor_out,
u32* second_descriptor_value_out);
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 EmitARMMultiplyLong(u32 instruction);
bool CanEmitARMDataProcessing(u32 instruction) const;
@@ -192,6 +192,14 @@ bool LoadBootstrapELF(std::span<const u8> image, ARMBus& bus, BootstrapELFInfo*
parsed.fault_spsr = address;
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 =
@@ -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");
}
// 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)
{
@@ -19,6 +19,10 @@ struct BootstrapELFInfo
u32 fault_code = 0;
u32 fault_lr = 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.
+2
View File
@@ -144,6 +144,8 @@ bool Starlet::Init(const std::string& dump_directory, std::string* error)
// construct it only after the complete physical bus exists.
m_core = std::make_unique<ARMCore>(*m_memory,
Config::Get(Config::MAIN_WII_STARLET_JIT_MEMORY_PROFILING));
// m_core is declared after m_memory, so it is destroyed first.
m_memory->AttachInterruptLines(m_core.get());
#if defined(_M_X86_64)
m_core->SetJitEnabled(Config::Get(Config::MAIN_WII_STARLET_JIT));
#else
+47 -3
View File
@@ -333,6 +333,9 @@ constexpr u32 HW_AHBPROT = HW_BASE + 0x64;
constexpr u32 HW_TIMER = HW_BASE + 0x10;
constexpr u64 TIMER_CLOCK_DIVISOR = 128;
constexpr u32 HW_ALARM = HW_BASE + 0x14;
constexpr u32 HW_ARMIRQFLAG = HW_BASE + 0x38;
constexpr u32 HW_ARMIRQMASK = HW_BASE + 0x3c;
constexpr u32 HW_ARMFIQMASK = HW_BASE + 0x40;
constexpr u32 HW_GPIO_ENABLE = HW_BASE + 0xdc;
constexpr u32 HW_GPIO_OUT = HW_BASE + 0xe0;
constexpr u32 HW_GPIO_DIR = HW_BASE + 0xe4;
@@ -864,6 +867,7 @@ void StarletMemory::Reset()
ResetOHCIController(controller);
m_arm_cycles = 0;
m_timer_offset = 0;
m_alarm_matched = false;
m_boot0_mapped = true;
m_sram_split_mode = false;
// Retail Hollywood production revision. Early firmware branches on this
@@ -1068,6 +1072,7 @@ void StarletMemory::DoState(PointerWrap& p)
p.Do(m_seeprom_write_all);
p.Do(m_arm_cycles);
p.Do(m_timer_offset);
p.Do(m_alarm_matched);
p.Do(m_initialized);
p.Do(m_boot0_mapped);
p.Do(m_sram_split_mode);
@@ -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
{
// 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 ticks_until_alarm = alarm - old_timer;
if (ticks_until_alarm != 0 && ticks_until_alarm <= elapsed_timer_ticks)
{
m_alarm_matched = true;
m_system.GetWiiIPC().SetStarletInterrupt(INT_CAUSE_TIMER, true);
}
if (m_nand_cycles_until_completion != 0)
{
@@ -4099,9 +4137,11 @@ void StarletMemory::Write8(u32 address, u8 value)
{
// HW_ALARM is a comparator, not an interrupt acknowledgement register.
// Original Starlet software acknowledges the timer separately through
// HW_ARMIRQFLAG. Programming the comparator to the current counter value
// creates an immediate match.
if (word == GetTimer())
// HW_ARMIRQFLAG. Programming the comparator re-arms it: a past match no
// longer holds the cause (KeepMatchedAlarmAsserted). Programming it to the
// current counter value creates an immediate match.
m_alarm_matched = word == GetTimer();
if (m_alarm_matched)
m_system.GetWiiIPC().SetStarletInterrupt(INT_CAUSE_TIMER, true);
}
else if (word_address == HW_SPARE0)
@@ -4147,6 +4187,8 @@ void StarletMemory::Write8(u32 address, u8 value)
word_address == HW_AHBPROT || word_address == HW_RESETS)
{
m_system.GetWiiIPC().WriteStarletRegister(word_address - HW_BASE, word);
KeepMatchedAlarmAsserted(word_address);
UpdateInterruptLines(word_address);
if (word_address == HW_BASE + 0x0c && (word & 0x09) != 0)
{
// The physical CPUs are concurrent. Give Broadway a scheduling boundary
@@ -4229,6 +4271,8 @@ void StarletMemory::Write32(u32 address, u32 value)
{
WriteRegister(address, value);
m_system.GetWiiIPC().WriteStarletRegister(address - HW_BASE, value);
KeepMatchedAlarmAsserted(address);
UpdateInterruptLines(address);
// Phase 11 H1: an IPC reply (ARMCTRL Y1) audits the watched ranges.
if (!m_watch_ranges.empty() && address == HW_BASE + 0x0c && (value & 1) != 0)
WatchAudit();
@@ -82,6 +82,13 @@ public:
// that touches one of those physical ranges. The ARM JIT sends the accesses that hit a watched
// range to its bus slow path, with the instruction address. `core` gives that address.
void SetWatchCore(const ARMCore* core);
// The core whose IRQ and FIQ lines follow Hollywood's ARM interrupt registers. A write to
// HW_ARMIRQFLAG, HW_ARMIRQMASK or HW_ARMFIQMASK re-samples both lines at once, as the
// level-sensitive Hollywood output follows them on hardware, instead of at the next scheduler
// slice: an acknowledged or masked interrupt is never taken again in between. Raising a cause
// still reaches the line at the next slice. The owner of both attaches the core once it exists
// and destroys it before the memory.
void AttachInterruptLines(ARMCore* core);
void Reset();
void DoState(PointerWrap& p);
@@ -110,6 +117,10 @@ public:
private:
// Synthetic NAND fixtures exercise startup pairing without any console dumps or keys.
friend class StarletPairingTest;
void UpdateInterruptLines(u32 address);
void KeepMatchedAlarmAsserted(u32 address);
ARMCore* m_interrupt_core = nullptr;
friend class StarletDMATest;
static constexpr u32 NAND_PAGE_DATA_SIZE = 0x800;
@@ -347,6 +358,8 @@ private:
bool m_seeprom_write_all = false;
u64 m_arm_cycles = 0;
u32 m_timer_offset = 0;
// The counter has reached HW_ALARM since it was last written.
bool m_alarm_matched = false;
bool m_initialized = false;
// Phase 11 F1 write watch (REV_WATCH); empty when off.
std::vector<std::pair<u32, u32>> m_watch_ranges;
+1 -1
View File
@@ -97,7 +97,7 @@ struct CompressAndDumpStateArgs
static Common::WorkQueueThreadSP<CompressAndDumpStateArgs> s_compress_and_dump_thread;
// Don't forget to increase this after doing changes on the savestate system
constexpr u32 STATE_VERSION = 196; // Correct Hollywood DI/reset pending interrupt bit mapping.
constexpr u32 STATE_VERSION = 197; // Hollywood's alarm stays matched until HW_ALARM is rewritten.
// Increase this if the StateExtendedHeader definition changes
constexpr u32 EXTENDED_HEADER_VERSION = 1; // Last changed in PR 12217
+9
View File
@@ -16,6 +16,15 @@ add_dolphin_test(ESFormatsTest IOS/ES/FormatsTest.cpp)
add_dolphin_test(StarletARMCoreTest IOS/Starlet/ARMCoreTest.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(StarletSRAMReadTest IOS/Starlet/SRAMReadTest.cpp)
add_dolphin_test(StarletSRAMWriteTest IOS/Starlet/SRAMWriteTest.cpp)
@@ -6,7 +6,9 @@
#include <array>
#include <bit>
#include <chrono>
#include <initializer_list>
#include <iostream>
#include <utility>
#include <vector>
#include <gtest/gtest.h>
@@ -566,6 +568,208 @@ TEST(StarletRegisters, WideTimerAndInterruptAccessesMatchHardwareSemantics)
EXPECT_EQ(memory.Read32(arm_irq_mask), 0x800619efu);
}
TEST(StarletInterrupts, InterruptRegisterWritesResampleTheAttachedCoreLines)
{
constexpr u32 hardware_base = 0x0d800000;
constexpr u32 arm_irq_flag = hardware_base + 0x38;
constexpr u32 arm_irq_mask = hardware_base + 0x3c;
constexpr u32 arm_fiq_mask = hardware_base + 0x40;
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
auto& ipc = system.GetWiiIPC();
ipc.Reset();
StarletMemory memory(system);
memory.Reset();
ARMCore core(memory);
memory.AttachInterruptLines(&core);
// A latched, unmasked timer interrupt, sampled at a scheduler slice's start.
memory.Write32(arm_irq_mask, INT_CAUSE_TIMER);
ipc.SetStarletInterrupt(INT_CAUSE_TIMER, true);
core.SetIRQLine(ipc.IsStarletIRQAsserted());
ASSERT_TRUE(core.IsIRQLineAsserted());
// Acknowledging it lowers the line at once, not at the next slice.
memory.Write32(arm_irq_flag, INT_CAUSE_TIMER);
EXPECT_FALSE(core.IsIRQLineAsserted());
// Raising a cause still waits for the slice; masking and unmasking a latched cause do not.
ipc.SetStarletInterrupt(INT_CAUSE_TIMER, true);
EXPECT_FALSE(core.IsIRQLineAsserted());
memory.Write32(arm_irq_mask, 0);
EXPECT_FALSE(core.IsIRQLineAsserted());
memory.Write32(arm_irq_mask, INT_CAUSE_TIMER);
EXPECT_TRUE(core.IsIRQLineAsserted());
// The byte-assembled write path acknowledges as the wide one does.
for (u32 i = 0; i < 4; ++i)
memory.Write8(arm_irq_flag + i, static_cast<u8>(INT_CAUSE_TIMER >> (24 - 8 * i)));
EXPECT_FALSE(core.IsIRQLineAsserted());
// An acknowledgement re-samples the FIQ line from the same flags.
memory.Write32(arm_fiq_mask, INT_CAUSE_TIMER);
ipc.SetStarletInterrupt(INT_CAUSE_TIMER, true);
core.SetFIQLine(ipc.IsStarletFIQAsserted());
ASSERT_TRUE(core.IsFIQLineAsserted());
memory.Write32(arm_irq_flag, INT_CAUSE_TIMER);
EXPECT_FALSE(core.IsFIQLineAsserted());
}
TEST(StarletInterrupts, FIQMaskWritesResampleTheAttachedCoreLine)
{
constexpr u32 hardware_base = 0x0d800000;
constexpr u32 arm_irq_flag = hardware_base + 0x38;
constexpr u32 arm_fiq_mask = hardware_base + 0x40;
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
auto& ipc = system.GetWiiIPC();
ipc.Reset();
StarletMemory memory(system);
memory.Reset();
ARMCore core(memory);
memory.AttachInterruptLines(&core);
// A latched cause routed to no FIQ, sampled at a scheduler slice's start.
ipc.SetStarletInterrupt(INT_CAUSE_TIMER, true);
core.SetFIQLine(ipc.IsStarletFIQAsserted());
ASSERT_FALSE(core.IsFIQLineAsserted());
// Unmasking it as an FIQ raises the line at once; masking it lowers the line at once.
memory.Write32(arm_fiq_mask, INT_CAUSE_TIMER);
EXPECT_TRUE(core.IsFIQLineAsserted());
EXPECT_FALSE(core.IsIRQLineAsserted());
memory.Write32(arm_fiq_mask, 0);
EXPECT_FALSE(core.IsFIQLineAsserted());
// The byte-assembled write path re-samples the same way.
for (u32 i = 0; i < 4; ++i)
memory.Write8(arm_fiq_mask + i, static_cast<u8>(INT_CAUSE_TIMER >> (24 - 8 * i)));
EXPECT_TRUE(core.IsFIQLineAsserted());
memory.Write32(arm_irq_flag, INT_CAUSE_TIMER);
EXPECT_FALSE(core.IsFIQLineAsserted());
}
TEST(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)
{
// 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.
#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 IOS::LLE
@@ -4,6 +4,7 @@
#include <array>
#include <string>
#include <unordered_map>
#include <utility>
#include <gtest/gtest.h>
@@ -232,5 +233,80 @@ TEST(ARMCoreTest, Armv5DoublewordTransfersAndAlignmentAbort)
EXPECT_EQ(core.GetRegister(14), TEXT_BASE + 8);
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 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
+41
View File
@@ -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
+40
View File
@@ -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
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
@@ -16,9 +18,14 @@
#include "Core/IOS/Starlet/BootstrapELF.h"
#include "Core/IOS/Starlet/StarletMemory.h"
#include "Core/System.h"
#include "ProbeBudget.h"
#include "ProbeRing.h"
#include "StarOSPanicCheck.h"
namespace
{
using namespace StarletBootstrapProbe;
enum class ExpectedOutcome
{
Idle,
@@ -31,6 +38,9 @@ enum class ExpectedOutcome
// Exit status of a run stopped by a Dolphin alert (1: failed expectation, 2: usage).
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
// 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)
@@ -40,7 +50,8 @@ bool FailOnAlert(const char* caption, const char* text, bool, Common::MsgType)
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);
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);
// Declared after memory, so destroyed first.
memory.AttachInterruptLines(&core);
core.SetJitEnabled(jit);
core.Reset(info.entry);
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_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.SetFIQLine(system.GetWiiIPC().IsStarletFIQAsserted());
core.RunCycles(4096);
core.RunCycles(SLICE_CYCLES);
const u32 fault_code = memory.Read32(info.fault_code);
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 &&
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)
continue;
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;
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';
return false;
}
@@ -148,20 +212,47 @@ bool Run(const std::string& path, bool jit, ExpectedOutcome expected)
int main(int argc, char** argv)
{
Common::RegisterMsgAlertHandler(FailOnAlert);
if (argc < 2 || argc > 4)
if (argc < 2 || argc > 8)
{
std::cerr << "Usage: StarletBootstrapProbe <ELF> [--jit] "
"[--expect-panic|--expect-fatal-data|--expect-undefined-fault|--expect-phase2]\n";
std::cerr << "Usage: StarletBootstrapProbe <ELF> [--jit] [--mmu-faults] "
"[--sbr1-record|--sbr1-absent] "
"[--expect-panic|--expect-fatal-data|--expect-undefined-fault|--expect-phase2] "
"[--cycles <n>]\n";
return 2;
}
bool jit = false;
bool mmu_faults = false;
SBR1Mode sbr1 = SBR1Mode::None;
ExpectedOutcome expected = ExpectedOutcome::Idle;
bool outcome_selected = false;
std::optional<u64> cycles;
for (int index = 2; index < argc; ++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;
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" ||
option == "--expect-undefined-fault" || option == "--expect-phase2")
{
@@ -189,7 +280,46 @@ int main(int argc, char** argv)
auto& system = Core::System::GetInstance();
system.SetIsWii(true);
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();
return passed ? 0 : 1;
}