Compare commits

..
23 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
Yaya48andClaude Opus 5.5 f9193539fb StarletBootstrapProbe: back MEM1 and MEM2 as on a retail Wii
The probe never initialised Dolphin's MEM1 and MEM2, so any Starlet
access there was an access to unmapped memory: StarOS reads its SBR1
record header at 0x00200000 on every boot, which raised two alerts
(Unknown Pointer, then Invalid range in CopyFromEmu) and read 0. The
probe now initialises both, zeroed, at their retail sizes (24 and
64 MiB), and shuts them down after the run. The read still returns 0.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 17:49:56 +02:00
Yaya48andClaude Opus 5.5 61ebfffa97 StarletBootstrapProbe: fail on a Dolphin alert instead of opening a dialog
With no handler registered, Dolphin shows an alert in a modal MessageBox
on Windows, and the probe waits until someone closes it. A run that hits
an alert then looks like a hang (StarOS EXP-001). The probe now writes the
alert to stderr and exits at once with status 3.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 17:35:58 +02:00
Yaya48andClaude Opus 5.5 ba86a7486c IOS LLE: optional Broadway hardware window after a WiiConnect24 shutdown reply (StarletOS phase 11 J1)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-06 22:36:20 +02:00
Yaya48andClaude Opus 5.5 3d12571127 IOS LLE: execute ARMv5TE enhanced DSP instructions (StarletOS phase 11 I1)
SMLAxy, SMLAWy, SMULWy, SMLALxy, SMULxy, QADD, QSUB, QDADD and QDSUB were
decoded as compare data processing that writes no register; the ARM926EJ-S
implements them. The interpreter now executes them with the Q flag and the
JIT leaves them to it. Adds ARMv5TEEnhancedDSPInstructions.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-06 22:16:53 +02:00
Yaya48andClaude Opus 5.5 ba5b6de0d0 IOS LLE: shadow comparison for REV_WATCH (StarletOS phase 11 H1)
Reads and IPC-reply audits compare each watched range with what the
Starlet and the devices wrote, revealing other writers; register-list
transfers in a watched virtual range log their translation.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-06 22:08:04 +02:00
Yaya48andClaude Opus 5.5 76a133f3a4 IOS LLE: optional Starlet write watch REV_WATCH (StarletOS phase 11 F1)
Logs Starlet CPU stores (instruction address, LR) and device DMA writes
in the given physical ranges. The ARM JIT keeps fastmem but sends accesses
overlapping a watched range to its bus slow path, recording the
instruction address there. Nothing changes without the variable.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-06 20:21:09 +02:00
Yaya48andClaude Opus 5.5 81ebec368b IOS LLE: IPC trace prints filesystem IOCTL paths (StarletOS phase 11 M1e)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-06 19:44:05 +02:00
Yaya48andClaude Opus 5.5 3ce435786c IOS LLE: optional emulated Wii Remotes in bootstrap mode (StarletOS phase 11 M1)
With REV_BOOTSTRAP_WIIMOTE=1 a bootstrap-ELF session gets the emulated Wii
Remotes of the original-IOS mode, using the identities paired in the
attached NAND's SYSCONF. The NAND is never rewritten for it; without the
variable nothing changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-06 19:25:04 +02:00
Yaya48andClaude Opus 5.5 09fba9bbb5 docs: StarletOS X11 window build and the phase 11 reference launch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-06 19:10:35 +02:00
Yaya48andClaude Opus 5.5 cd8c8cedca IOS LLE: optional IPC request/reply metadata trace (StarletOS phase 11 L2)
With DOLPHIN_IPC_TRACE=1 and the original IOS on the emulated Starlet, log
each request Broadway posts (X1: command, descriptor, argument words, an
OPEN's path) and each reply Starlet posts (Y1: original command, result).
Buffer contents are never read; nothing changes without the variable.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-06 18:40:56 +02:00
Yaya48 78738093ec docs: record Linux Starlet bootstrap qualification 2026-10-03 23:38:28 +02:00
26 changed files with 2931 additions and 64 deletions

No files matched your search

+6
View File
@@ -9,7 +9,13 @@ Source/Core/Core/Config/MainSettings.cpp -text
Source/Core/Core/Config/MainSettings.h -text
Source/Core/Core/ConfigManager.cpp -text
Source/Core/Core/HW/HW.cpp -text
Source/Core/Core/HW/WII_IPC.cpp -text
Source/Core/Core/HW/WII_IPC.h -text
Source/Core/Core/PowerPC/MMU.cpp -text
Source/Core/Core/IOS/Starlet/ARMCore.cpp -text
Source/Core/Core/IOS/Starlet/ARMCore.h -text
Source/Core/Core/IOS/Starlet/ARMJitX64.cpp -text
Source/Core/Core/IOS/Starlet/ARMJitX64.h -text
Source/Core/Core/IOS/Starlet/BootstrapELF.cpp -text
Source/Core/Core/IOS/Starlet/BootstrapELF.h -text
Source/Core/Core/IOS/Starlet/Starlet.cpp -text
+153
View File
@@ -3,6 +3,10 @@
#include "Core/HW/WII_IPC.h"
#include <algorithm>
#include <cstdlib>
#include <string>
#include "Common/ChunkFile.h"
#include "Common/CommonTypes.h"
#include "Common/Logging/Log.h"
@@ -10,6 +14,7 @@
#include "Core/CoreTiming.h"
#include "Core/HW/DVD/DVDInterface.h"
#include "Core/HW/MMIO.h"
#include "Core/HW/Memmap.h"
#include "Core/HW/ProcessorInterface.h"
#include "Core/IOS/IOS.h"
#include "Core/IOS/Starlet/Starlet.h"
@@ -142,6 +147,149 @@ void WiiIPC::Shutdown()
{
}
namespace
{
// StarletOS phase 11 (proposal L2): with DOLPHIN_IPC_TRACE=1 while the emulated Starlet runs
// (the original IOS or a bootstrap ELF), each IPC request Broadway posts (X1) and each reply
// Starlet posts (Y1) is logged with its metadata only: the command, the descriptor and the five
// argument words (an OPEN's path), then the reply's result. Proposal M1e adds the paths of the
// filesystem IOCTLs. No other buffer content is read.
bool IPCTraceEnabled()
{
static const bool enabled = [] {
const char* value = std::getenv("DOLPHIN_IPC_TRACE");
return value != nullptr && value[0] == '1';
}();
return enabled;
}
// A NUL-terminated absolute path of printable bytes in a 64-byte field, or "" otherwise.
std::string TracePath(Memory::MemoryManager& memory, u32 address)
{
if (memory.GetPointerForRange(address, 64) == nullptr)
return {};
const std::string path = memory.GetString(address, 64);
if (path.empty() || path.size() >= 64 || path[0] != '/' ||
!std::ranges::all_of(path, [](char c) { return c >= 0x20 && c < 0x7f; }))
return {};
return path;
}
// The paths a filesystem request names, by its input shape: a 64-byte path (DELETE, GET_ATTR),
// a 76-byte attribute record with the path at +6 (CREATE_DIR, CREATE_FILE, SET_ATTR), two
// 64-byte paths (RENAME), or an IOCTLV whose first input vector is a 64-byte path (READ_DIR,
// GET_USAGE). " F -" when the field holds no valid path.
std::string TraceFilesystemPaths(Memory::MemoryManager& memory, u32 command, const u32* arg)
{
const auto field = [&memory](u32 address) {
const std::string path = TracePath(memory, address);
return path.empty() ? std::string("-") : path;
};
if (command == 6)
{
if (arg[2] == 0x40)
return " F " + field(arg[1]);
if (arg[2] == 0x4c)
return " F " + field(arg[1] + 6);
if (arg[2] == 0x80)
return " F " + field(arg[1]) + " > " + field(arg[1] + 0x40);
}
else if (command == 7 && arg[1] != 0 && memory.GetPointerForRange(arg[3], 8) != nullptr &&
memory.Read_U32(arg[3] + 4) == 0x40)
{
return " F " + field(memory.Read_U32(arg[3]));
}
return {};
}
// Phase 11 J1: the shutdown request whose reply opens the hardware window, and the window's
// access count.
u32 s_hardware_trigger = 0;
u32 s_hardware_accesses = 0;
constexpr u32 HARDWARE_WINDOW_LIMIT = 20000;
void CloseHardwareWindow(const char* why)
{
if (!g_starletos_hardware_window)
return;
g_starletos_hardware_window = false;
NOTICE_LOG_FMT(WII_IPC, "HWTRACE close {} after {} accesses", why, s_hardware_accesses);
}
void TraceIPCRequest(Core::System& system, u32 address)
{
if (StarletOSHardwareTraceRequested())
CloseHardwareWindow("at the next request");
auto& memory = system.GetMemory();
if (memory.GetPointerForRange(address, 0x20) == nullptr)
{
NOTICE_LOG_FMT(WII_IPC, "IPCTRACE RQ {:08x} unreadable", address);
return;
}
const u32 command = memory.Read_U32(address);
const u32 fd = memory.Read_U32(address + 8);
u32 arg[5];
for (u32 i = 0; i < 5; ++i)
arg[i] = memory.Read_U32(address + 0xc + 4 * i);
std::string path;
if (command == 1 && memory.GetPointerForRange(arg[0], 64) != nullptr)
path = " P " + memory.GetString(arg[0], 64);
else
path = TraceFilesystemPaths(memory, command, arg);
NOTICE_LOG_FMT(WII_IPC, "IPCTRACE RQ {:08x} C {} FD {} A {:08x} {:08x} {:08x} {:08x} {:08x}{}",
address, command, static_cast<s32>(fd), arg[0], arg[1], arg[2], arg[3], arg[4],
path);
if (StarletOSHardwareTraceRequested() && command == 6 && arg[0] == 0x28 && arg[2] == 0x20 &&
arg[4] == 0x20)
{
s_hardware_trigger = address;
}
}
void TraceIPCReply(Core::System& system, u32 address)
{
auto& memory = system.GetMemory();
if (memory.GetPointerForRange(address, 0x10) == nullptr)
{
NOTICE_LOG_FMT(WII_IPC, "IPCTRACE RP {:08x} unreadable", address);
return;
}
NOTICE_LOG_FMT(WII_IPC, "IPCTRACE RP {:08x} C {} RC {}", address, memory.Read_U32(address + 8),
static_cast<s32>(memory.Read_U32(address + 4)));
if (StarletOSHardwareTraceRequested() && s_hardware_trigger != 0 &&
address == s_hardware_trigger)
{
s_hardware_trigger = 0;
s_hardware_accesses = 0;
g_starletos_hardware_window = true;
NOTICE_LOG_FMT(WII_IPC, "HWTRACE open at the reply {:08x}", address);
}
}
} // namespace
bool g_starletos_hardware_window = false;
bool StarletOSHardwareTraceRequested()
{
static const bool requested = [] {
const char* value = std::getenv("DOLPHIN_HW_WINDOW");
return IPCTraceEnabled() && value != nullptr && value[0] == '1';
}();
return requested;
}
void TraceStarletOSHardwareAccess(Core::System& system, bool write, u32 address, u32 size,
u64 value)
{
if (++s_hardware_accesses > HARDWARE_WINDOW_LIMIT)
{
CloseHardwareWindow("at the access limit");
return;
}
NOTICE_LOG_FMT(WII_IPC, "HWTRACE {} {:08x}/{} {:0{}x} PC {:08x}", write ? 'W' : 'R', address,
size, value, size * 2, system.GetPPCState().pc);
}
void WiiIPC::RegisterMMIO(MMIO::Mapping* mmio, u32 base)
{
// Both IPC message registers are general-purpose read/write registers on
@@ -168,6 +316,8 @@ void WiiIPC::RegisterMMIO(MMIO::Mapping* mmio, u32 base)
wii_ipc.m_ppc_irq_flags |= INT_CAUSE_IPC_BROADWAY;
if (wii_ipc.m_ctrl.X1)
{
if (system.GetStarlet() && IPCTraceEnabled() && (val >> 0 & 1))
TraceIPCRequest(system, wii_ipc.m_ppc_msg);
if (system.GetStarlet())
wii_ipc.m_arm_irq_flags |= INT_CAUSE_IPC_STARLET;
else if (system.GetIOS())
@@ -386,6 +536,9 @@ void WiiIPC::WriteStarletRegister(u32 offset, u32 value)
break;
case IPC_ARMCTRL:
m_ctrl.arm(value);
// StarletOS phase 11: Y1 (bit 0) posts the reply whose pointer is in ARMMSG.
if (IPCTraceEnabled() && (value & 1))
TraceIPCReply(m_system, m_arm_msg);
break;
case PPC_IRQFLAG:
m_ppc_irq_flags &= ~value;
+8
View File
@@ -123,6 +123,14 @@ struct CtrlRegister
}
};
// StarletOS phase 11 (J1), emulator only: with DOLPHIN_IPC_TRACE=1 and DOLPHIN_HW_WINDOW=1,
// Broadway's hardware register accesses (MMIO) are logged from the reply to a WiiConnect24
// shutdown request (IOCTL 0x28, 32-byte input and output) until Broadway's next IPC request.
bool StarletOSHardwareTraceRequested();
extern bool g_starletos_hardware_window;
void TraceStarletOSHardwareAccess(Core::System& system, bool write, u32 address, u32 size,
u64 value);
class WiiIPC
{
public:
+522 -30
View File
@@ -12,6 +12,7 @@
#include <utility>
#include "Common/ChunkFile.h"
#include "Common/Logging/Log.h"
#include "Common/PerformanceTrace.h"
#if defined(_M_X86_64)
// ARMCore owns ARMJitX64 by value through unique_ptr construction in this translation unit, so a
@@ -25,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);
@@ -36,9 +58,24 @@ constexpr u32 ModeValue(ARMCore::Mode mode)
ARMCore::ARMCore(ARMBus& bus, bool profile_jit_memory)
: m_bus(bus), m_profile_jit_memory(profile_jit_memory)
{
m_watch_ranges = m_bus.GetWatchRanges();
Reset();
}
void ARMCore::TraceWatchedBlockTransfer(u32 address, u32 size, bool load) const
{
// StarletOS phase 11 (H1): the virtual range of a register-list transfer and the physical
// address the interpreter translates its first word to. A difference shows a transfer that
// does not read or write where the program addressed it.
const bool overlaps = std::ranges::any_of(m_watch_ranges, [&](const auto& range) {
return u64{address} < u64{range.first} + range.second && u64{address} + size > range.first;
});
if (!overlaps)
return;
NOTICE_LOG_FMT(IOS, "REVWATCH {} {:08x}->{:08x}/{} PC {:08x}", load ? "LDM" : "STM", address,
TranslateVirtualAddress(address), size, m_instruction_address);
}
ARMCore::~ARMCore() = default;
void ARMCore::SetJitEnabled(bool enabled)
@@ -189,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 =
@@ -219,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;
@@ -323,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);
@@ -375,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);
@@ -415,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)
@@ -431,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
@@ -753,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
{
@@ -774,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)
@@ -1407,6 +1771,16 @@ void ARMCore::ExecuteARM(u32 instruction)
return;
}
// ARMv5TE enhanced DSP instructions (ARM Architecture Reference Manual, ARMv5TE): SMLAxy,
// SMLAWy, SMULWy, SMLALxy, SMULxy, then QADD, QSUB, QDADD, QDSUB. They sit in the
// data-processing space with a compare opcode and S clear, which ARMv5 reserves for such
// miscellaneous instructions; decoded as data processing they would write no register.
if ((instruction & 0x0f900090) == 0x01000080 || (instruction & 0x0f900ff0) == 0x01000050)
{
ExecuteEnhancedDSP(instruction);
return;
}
if ((instruction & 0x0f8000f0) == 0x00800090)
{
ExecuteMultiplyLong(instruction);
@@ -1434,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;
@@ -1708,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
@@ -1747,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);
@@ -1768,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
@@ -1781,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
@@ -1825,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)
{
@@ -1840,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))
@@ -1877,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)))
@@ -2095,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;
@@ -2121,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;
@@ -2177,6 +2568,8 @@ void ARMCore::ExecuteHalfwordDataTransfer(u32 instruction)
return;
}
if (type == 1 || type == 3)
CheckAlignment(address, 1);
if (load)
{
u32 value;
@@ -2252,6 +2645,9 @@ void ARMCore::ExecuteBlockDataTransfer(u32 instruction)
address = preindex ? base + 4 : base;
else
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;
@@ -2309,6 +2705,87 @@ void ARMCore::ExecuteMultiply(u32 instruction)
SetNZ(result);
}
void ARMCore::ExecuteEnhancedDSP(u32 instruction)
{
// StarletOS phase 11 (I1). The ARM926EJ-S implements these; the Q flag (CPSR bit 27) is sticky
// and set on signed overflow of an accumulation or on saturation.
const auto half = [](u32 value, bool top) {
return static_cast<s64>(static_cast<s16>(top ? value >> 16 : value));
};
const auto saturate = [this](s64 value) {
if (value > std::numeric_limits<s32>::max())
{
m_cpsr |= CPSR_Q;
return static_cast<u32>(std::numeric_limits<s32>::max());
}
if (value < std::numeric_limits<s32>::min())
{
m_cpsr |= CPSR_Q;
return static_cast<u32>(std::numeric_limits<s32>::min());
}
return static_cast<u32>(static_cast<s32>(value));
};
const auto accumulate = [this](s64 sum) {
if (sum != static_cast<s32>(sum))
m_cpsr |= CPSR_Q;
return static_cast<u32>(static_cast<s32>(sum));
};
const u32 op = (instruction >> 21) & 3;
const u32 rm = instruction & 0xf;
if ((instruction & 0xf0) == 0x50)
{
// QADD (0), QSUB (1), QDADD (2), QDSUB (3): Rd = sat(Rm +/- [sat(2 *)] Rn).
const u32 rn = (instruction >> 16) & 0xf;
const u32 rd = (instruction >> 12) & 0xf;
const s64 m = static_cast<s32>(ReadRegisterOperand(rm));
s64 n = static_cast<s32>(ReadRegisterOperand(rn));
if ((op & 2) != 0)
n = static_cast<s32>(saturate(2 * n));
m_registers[rd] = saturate((op & 1) != 0 ? m - n : m + n);
return;
}
// SMLAxy (0), SMLAWy/SMULWy (1), SMLALxy (2), SMULxy (3). Rd (RdHi) is bits 19-16, Rn (RdLo)
// bits 15-12, Rs bits 11-8; x (bit 5) picks Rm's half, y (bit 6) Rs's half.
const u32 rd = (instruction >> 16) & 0xf;
const u32 rn = (instruction >> 12) & 0xf;
const u32 rs = (instruction >> 8) & 0xf;
const bool x = (instruction & (1U << 5)) != 0;
const bool y = (instruction & (1U << 6)) != 0;
const u32 m = ReadRegisterOperand(rm);
const u32 s = ReadRegisterOperand(rs);
switch (op)
{
case 0:
m_registers[rd] =
accumulate(half(m, x) * half(s, y) + static_cast<s32>(ReadRegisterOperand(rn)));
break;
case 1:
{
// The top 32 bits of the 48-bit product of Rm and a halfword of Rs.
const s64 product = (static_cast<s64>(static_cast<s32>(m)) * half(s, y)) >> 16;
if (x) // SMULWy
m_registers[rd] = static_cast<u32>(static_cast<s32>(product));
else // SMLAWy
m_registers[rd] = accumulate(product + static_cast<s32>(ReadRegisterOperand(rn)));
break;
}
case 2:
{
const u64 accumulator =
(static_cast<u64>(ReadRegisterOperand(rd)) << 32) | ReadRegisterOperand(rn);
const u64 result = accumulator + static_cast<u64>(half(m, x) * half(s, y));
m_registers[rn] = static_cast<u32>(result);
m_registers[rd] = static_cast<u32>(result >> 32);
break;
}
default:
m_registers[rd] = static_cast<u32>(static_cast<s32>(half(m, x) * half(s, y)));
break;
}
}
void ARMCore::ExecuteMultiplyLong(u32 instruction)
{
const bool signed_multiply = (instruction & (1U << 22)) != 0;
@@ -2366,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)
{
@@ -2590,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:
@@ -2629,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;
@@ -2643,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
+76 -3
View File
@@ -7,6 +7,7 @@
#include <cstddef>
#include <memory>
#include <unordered_map>
#include <utility>
#include <vector>
#include "Common/CommonTypes.h"
@@ -59,6 +60,9 @@ public:
// Returns Dolphin's 4 GiB physical fastmem view when this bus is backed by Wii RAM. Device,
// boot-ROM and SRAM addresses must still use the bus callbacks.
virtual u8* GetFastmemBase() const { return nullptr; }
// StarletOS phase 11 (F1): physical (start, size) ranges whose accesses must stay on the bus
// even under fastmem, so a write watch sees them with their instruction address.
virtual std::vector<std::pair<u32, u32>> GetWatchRanges() const { return {}; }
// Optional direct Starlet SRAM view. The two state pointers let generated code preserve the
// boot0 overlay and SRAM A/B split mapping exactly while avoiding a host call for ordinary SRAM.
virtual u8* GetFastmemSRAMBase() const { return nullptr; }
@@ -125,6 +129,8 @@ public:
u32 GetRegister(size_t index) const;
void SetRegister(size_t index, u32 value);
u32 GetCPSR() const { return m_cpsr; }
// The address of the instruction being executed (interpreter).
u32 GetInstructionAddress() const { return m_instruction_address; }
void SetCPSR(u32 value);
Mode GetMode() const;
@@ -153,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
{
@@ -166,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;
@@ -240,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
@@ -285,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();
@@ -305,8 +356,12 @@ private:
void ExecuteSingleDataTransfer(u32 instruction);
void ExecuteHalfwordDataTransfer(u32 instruction);
void ExecuteBlockDataTransfer(u32 instruction);
// Phase 11 H1: a register-list transfer overlapping a watched range, with its translation.
void TraceWatchedBlockTransfer(u32 address, u32 size, bool load) const;
void ExecuteMultiply(u32 instruction);
void ExecuteMultiplyLong(u32 instruction);
// ARMv5TE signed halfword multiplies and saturating arithmetic.
void ExecuteEnhancedDSP(u32 instruction);
void ExecuteBranch(u32 instruction);
void ExecuteCoprocessor(u32 instruction);
void ExecutePSRTransfer(u32 instruction);
@@ -346,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;
@@ -363,6 +422,8 @@ private:
u32 m_memory_poll_address = 0;
bool m_pc_written = false;
u32 m_instruction_address = 0;
// Phase 11 H1: the bus's watched ranges (REV_WATCH), empty when off.
std::vector<std::pair<u32, u32>> m_watch_ranges;
u32 m_last_undefined_instruction = 0;
u64 m_executed_instructions = 0;
u64 m_memory_poll_entry_count = 0;
@@ -374,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
+114 -5
View File
@@ -56,6 +56,7 @@ SlowMemoryRegion ClassifySlowMemoryAddress(u32 address)
ARMJitX64::ARMJitX64(ARMCore& core) : m_core(core)
{
m_fastmem_base = m_core.m_bus.GetFastmemBase();
m_watch_ranges = m_core.m_bus.GetWatchRanges();
// SRAM is a plain Hollywood memory array just like MEM1/MEM2. The generated address checks
// below still side-exit for boot0 overlays and the unmapped split-window gap, so only actual
// SRAM reaches this pointer; MMIO and ROM writes retain the exact bus path.
@@ -193,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
@@ -378,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));
@@ -386,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));
@@ -426,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 =
@@ -766,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.
@@ -1163,7 +1232,9 @@ bool ARMJitX64::CanEmitARMDataProcessing(u32 instruction) const
if ((instruction & 0x0fff0ff0) == 0x016f0f10 || (instruction & 0x0f8000f0) == 0x00800090 ||
(instruction & 0x0fc000f0) == 0x00000090 || (instruction & 0x0fb00ff0) == 0x01000090 ||
(instruction & 0x0fbf0fff) == 0x010f0000 || (instruction & 0x0fb0fff0) == 0x0120f000 ||
(instruction & 0x0fb0f000) == 0x0320f000 || (instruction & 0x0e000090) == 0x00000090)
(instruction & 0x0fb0f000) == 0x0320f000 || (instruction & 0x0e000090) == 0x00000090 ||
// ARMv5TE enhanced DSP multiplies and saturating arithmetic: interpreter (StarletOS I1).
(instruction & 0x0f900090) == 0x01000080 || (instruction & 0x0f900ff0) == 0x01000050)
{
return false;
}
@@ -1475,6 +1546,15 @@ bool ARMJitX64::EmitARMBlockTransfer(u32 instruction, u32 address, bool* termina
}
std::vector<FixupBranch> slow_paths;
// StarletOS phase 11 (H1): a transfer whose virtual range overlaps a watched range runs through
// the architectural helper, so the interpreter logs its translation.
for (const auto& [start, size] : m_watch_ranges)
{
MOV(32, R(EDX), R(EAX));
SUB(32, R(EDX), Imm32(start - (range_size - 1)));
CMP(32, R(EDX), Imm32(size + range_size - 1));
slow_paths.push_back(J_CC(CC_B, Jump::Near));
}
if ((m_core.m_cp15.control & 1U) != 0)
{
MOV(32, R(EDX), R(EAX));
@@ -1850,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();
@@ -2255,6 +2337,16 @@ void ARMJitX64::EmitFastmemAddress(std::vector<FixupBranch>* slow_paths, u32 acc
slow_paths->push_back(J_CC(CC_NZ, Jump::Near));
}
// StarletOS phase 11 (F1): an access overlapping a watched range takes the bus slow path.
// Overlap: start - (range_size - 1) <= address < start + size, as one unsigned comparison.
for (const auto& [start, size] : m_watch_ranges)
{
MOV(32, R(EDX), R(EAX));
SUB(32, R(EDX), Imm32(start - (range_size - 1)));
CMP(32, R(EDX), Imm32(size + range_size - 1));
slow_paths->push_back(J_CC(CC_B, Jump::Near));
}
// Return the resolved host pointer in R11. Wii RAM uses Dolphin's 4 GiB fastmem view.
CMP(32, R(EAX), Imm32(Memory::MEM1_SIZE_RETAIL - range_size));
FixupBranch in_mem1 = J_CC(CC_BE, Jump::Near);
@@ -2384,6 +2476,9 @@ void ARMJitX64::EmitThumbMemorySlowPath(const std::vector<FixupBranch>& slow_pat
{
for (const FixupBranch& slow_path : slow_paths)
SetJumpTarget(slow_path);
// Phase 11 F1: the bus write below reports this instruction to the write watch.
if (!m_watch_ranges.empty())
MOV(32, MInstructionAddress(), Imm32(address));
// IOS Thumb code repeatedly accesses SRAM and MMIO. These instructions are already decoded:
// perform the exact bus transaction without re-entering ExecuteThumb and the block dispatcher.
@@ -2478,6 +2573,9 @@ void ARMJitX64::EmitARMMemorySlowPath(const std::vector<FixupBranch>& slow_paths
{
for (const FixupBranch& slow_path : slow_paths)
SetJumpTarget(slow_path);
// Phase 11 F1: the bus write below reports this instruction to the write watch.
if (!m_watch_ranges.empty())
MOV(32, MInstructionAddress(), Imm32(address));
// A normal ARM single-data transfer that misses RAM/SRAM fastmem is still a fully decoded
// instruction. Execute only its physical bus transaction here and stay inside the translated
@@ -2883,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);
}
@@ -2906,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);
}
+9
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;
@@ -250,6 +257,8 @@ private:
std::array<u8, FAST_ENTRY_SET_COUNT> m_fast_entry_next_victim{};
std::array<FastTranslationEntry, FAST_TRANSLATION_ENTRY_COUNT> m_fast_translations{};
u8* m_fastmem_base = nullptr;
// Phase 11 F1: watched physical ranges, routed to the bus slow path.
std::vector<std::pair<u32, u32>> m_watch_ranges;
u8* m_sram_base = nullptr;
const bool* m_boot0_mapped = nullptr;
const bool* m_sram_split_mode = nullptr;
@@ -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.
+14
View File
@@ -128,17 +128,31 @@ bool Starlet::Init(const std::string& dump_directory, std::string* error)
}
INFO_LOG_FMT(IOS, "Starlet bootstrap keys attached");
}
// StarletOS phase 11 (M1), emulator only: the emulated Wii Remotes for a Revolution session,
// with the identity paired in the attached NAND. Off unless asked for.
if (const char* wiimotes = std::getenv("REV_BOOTSTRAP_WIIMOTE"); wiimotes && *wiimotes == '1')
{
if (!m_memory->EnableBootstrapWiimotes(error))
{
m_memory.reset();
return false;
}
INFO_LOG_FMT(IOS, "Starlet bootstrap emulated Wii Remotes enabled");
}
}
// ARMCore owns generation-tagged software TLB and instruction caches, so
// 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
m_core->SetJitEnabled(false);
#endif
m_core->Reset(m_bootstrap ? m_bootstrap->entry : StarletMemory::BOOT_ROM_BASE);
m_memory->SetWatchCore(m_core.get());
if (m_bootstrap)
m_core->SetBigEndian(true);
m_next_jit_diagnostic_instruction = 25'000'000;
+247 -4
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;
@@ -462,6 +465,153 @@ std::string PathInDirectory(const std::string& directory, const char* filename)
StarletMemory::StarletMemory(Core::System& system) : m_system(system)
{
// StarletOS phase 11 (F1): REV_WATCH=start:size[,start:size...], hexadecimal.
if (const char* watch = std::getenv("REV_WATCH"); watch && *watch)
{
for (const std::string& range : SplitString(watch, ','))
{
const std::vector<std::string> parts = SplitString(range, ':');
u32 start = 0;
u32 size = 0;
if (parts.size() == 2 && TryParse(parts[0], &start) && TryParse(parts[1], &size) && size != 0)
m_watch_ranges.emplace_back(start, size);
else
ERROR_LOG_FMT(IOS, "REV_WATCH: ignoring range '{}'", range);
}
for (const auto& [start, size] : m_watch_ranges)
{
NOTICE_LOG_FMT(IOS, "REVWATCH range {:08x}+{:x}", start, size);
m_watch_shadow.emplace_back(size);
m_watch_known.emplace_back(size);
}
}
}
void StarletMemory::SetWatchCore(const ARMCore* core)
{
m_watch_core = core;
}
bool StarletMemory::WatchHit(u32 address, u32 size) const
{
for (const auto& [start, length] : m_watch_ranges)
{
if (u64{address} < u64{start} + length && u64{address} + size > start)
return true;
}
return false;
}
void StarletMemory::WatchCPU(u32 address, u32 size, u32 value)
{
if (!WatchHit(address, size))
return;
NOTICE_LOG_FMT(IOS, "REVWATCH CPU {:08x}/{} {:08x} PC {:08x} LR {:08x}", address, size, value,
m_watch_core ? m_watch_core->GetInstructionAddress() : 0,
m_watch_core ? m_watch_core->GetRegister(14) : 0);
// The bus stores scalars big-endian.
std::array<u8, 4> bytes{};
for (u32 i = 0; i < size; ++i)
bytes[i] = static_cast<u8>(value >> (8 * (size - 1 - i)));
WatchRecord(address, bytes.data(), size);
}
void StarletMemory::WatchDMA(u32 address, const u8* data, u32 size)
{
if (!WatchHit(address, size))
return;
NOTICE_LOG_FMT(IOS, "REVWATCH DMA {:08x}/{:x} PC {:08x}", address, size,
m_watch_core ? m_watch_core->GetInstructionAddress() : 0);
WatchRecord(address, data, size);
}
void StarletMemory::WatchRecord(u32 address, const u8* data, u32 size)
{
for (size_t range = 0; range < m_watch_ranges.size(); ++range)
{
const auto [start, length] = m_watch_ranges[range];
for (u32 i = 0; i < size; ++i)
{
const u32 at = address + i;
if (at >= start && at - start < length)
{
m_watch_shadow[range][at - start] = data[i];
m_watch_known[range][at - start] = 1;
}
}
}
}
void StarletMemory::WatchRead(u32 address, u32 size)
{
// Phase 11 H1: a byte the Starlet reads that differs from what the Starlet or a device last
// wrote there was changed by another writer. Log it with the reader, then adopt the new value.
const u8* memory = m_system.GetMemory().GetPointerForRange(address, size);
if (memory == nullptr)
return;
for (size_t range = 0; range < m_watch_ranges.size(); ++range)
{
const auto [start, length] = m_watch_ranges[range];
u64 read = 0;
u64 expected = 0;
bool differs = false;
for (u32 i = 0; i < size; ++i)
{
const u32 at = address + i;
const bool inside = at >= start && at - start < length;
const u8 shadow = inside ? m_watch_shadow[range][at - start] : memory[i];
read = (read << 8) | memory[i];
expected = (expected << 8) | shadow;
if (inside && m_watch_known[range][at - start] && shadow != memory[i])
{
differs = true;
m_watch_shadow[range][at - start] = memory[i];
}
}
if (differs)
{
NOTICE_LOG_FMT(IOS,
"REVWATCH DIFF {:08x}/{} read {:0{}x} expected {:0{}x} PC {:08x} LR {:08x}",
address, size, read, size * 2, expected, size * 2,
m_watch_core ? m_watch_core->GetInstructionAddress() : 0,
m_watch_core ? m_watch_core->GetRegister(14) : 0);
}
}
}
void StarletMemory::WatchAudit()
{
// Phase 11 H1, at each IPC reply: every run of written bytes that guest memory no longer
// holds (at most 16 runs logged per reply), then adopt the new values.
u32 runs = 0;
for (size_t range = 0; range < m_watch_ranges.size(); ++range)
{
const auto [start, length] = m_watch_ranges[range];
const u8* memory = m_system.GetMemory().GetPointerForRange(start, length);
if (memory == nullptr)
continue;
auto& shadow = m_watch_shadow[range];
const auto& known = m_watch_known[range];
for (u32 i = 0; i < length;)
{
if (!known[i] || shadow[i] == memory[i])
{
++i;
continue;
}
const u32 first = i;
while (i < length && known[i] && shadow[i] != memory[i])
{
shadow[i] = memory[i];
++i;
}
if (++runs <= 16)
{
NOTICE_LOG_FMT(IOS, "REVWATCH AUDIT {:08x}+{:x} now {:02x}", start + first, i - first,
memory[first]);
}
}
}
}
bool StarletMemory::Init(const std::string& dump_directory, std::string* error)
@@ -717,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
@@ -792,6 +943,26 @@ bool StarletMemory::AttachDisposableNAND(const std::string& source, const std::s
return true;
}
bool StarletMemory::EnableBootstrapWiimotes(std::string* error)
{
if (!m_nand)
{
if (error)
*error = "REV_BOOTSTRAP_WIIMOTE needs a bootstrap NAND";
return false;
}
// The identities are chosen before the devices exist (see Init).
if (!InstallEmulatedWiimotePairings(false))
{
if (error)
*error = "could not read the Wii Remote pairings of the bootstrap NAND";
return false;
}
m_initialized = true;
InitializeWiimotes();
return true;
}
bool StarletMemory::AttachKeys(const std::string& path, std::string* error)
{
File::IOFile keys_file(path, "rb");
@@ -901,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);
@@ -937,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).
@@ -967,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)
{
@@ -1025,6 +1230,11 @@ u8* StarletMemory::GetFastmemBase() const
return m_system.GetMemory().GetPhysicalBase();
}
std::vector<std::pair<u32, u32>> StarletMemory::GetWatchRanges() const
{
return m_watch_ranges;
}
u8* StarletMemory::GetFastmemSRAMBase() const
{
// The JIT admits only measured SRAM pages and directions, and checks the live boot0 overlay and
@@ -1165,6 +1375,8 @@ u8 StarletMemory::ReadDMA8(u32 address) const
void StarletMemory::WriteDMA8(u32 address, u8 value)
{
if (!m_watch_ranges.empty() && !m_watch_in_dma_buffer)
WatchDMA(address, &value, 1);
if (address >= SRAM_BASE && address < SRAM_BASE + SRAM_WINDOW_SIZE)
{
const u32 offset = address - SRAM_BASE;
@@ -1216,13 +1428,17 @@ void StarletMemory::WriteDMABuffer(u32 address, std::span<const u8> buffer)
const Common::PerformanceTrace::Scope trace(Common::PerformanceTrace::Region::DMAWrite);
if (buffer.empty())
return;
if (!m_watch_ranges.empty())
WatchDMA(address, buffer.data(), static_cast<u32>(buffer.size()));
if (u8* memory = GetDMAMemoryPointer(address, buffer.size()))
{
std::memcpy(memory, buffer.data(), buffer.size());
return;
}
m_watch_in_dma_buffer = true;
for (size_t i = 0; i < buffer.size(); ++i)
WriteDMA8(address + static_cast<u32>(i), buffer[i]);
m_watch_in_dma_buffer = false;
}
void StarletMemory::ClearRegisterCache()
@@ -3532,6 +3748,8 @@ void StarletMemory::WriteRegister(u32 address, u32 value)
u8 StarletMemory::Read8(u32 address)
{
if (!m_watch_ranges.empty() && WatchHit(address, 1))
WatchRead(address, 1);
if (IsBootROMAddress(address))
return m_boot_rom[GetBootROMOffset(address)];
@@ -3653,6 +3871,8 @@ u8 StarletMemory::Read8(u32 address)
u16 StarletMemory::Read16(u32 address)
{
if (!m_watch_ranges.empty() && WatchHit(address, 2))
WatchRead(address, 2);
// Aligned reads stay inside one SRAM/boot0 mapping segment. Resolve the live mapping once,
// including its holes, before considering unrelated memory and peripheral ranges.
if ((address & 1) == 0 && IsSRAMWindowAddress(address))
@@ -3687,6 +3907,8 @@ u16 StarletMemory::Read16(u32 address)
u32 StarletMemory::Read32(u32 address)
{
if (!m_watch_ranges.empty() && WatchHit(address, 4))
WatchRead(address, 4);
// ARM IOS accesses the Hollywood timer and interrupt controller almost exclusively as aligned
// words. Falling back to ARMBus::Read32 decomposes each access into four virtual Read8 calls;
// every byte then repeats the complete device-range decoder and IPC register switch. Preserve
@@ -3760,6 +3982,8 @@ u32 StarletMemory::Read32(u32 address)
void StarletMemory::Write8(u32 address, u8 value)
{
if (!m_watch_ranges.empty())
WatchCPU(address, 1, value);
if (IsBootROMAddress(address))
return;
@@ -3913,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)
@@ -3961,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
@@ -3973,6 +4201,8 @@ void StarletMemory::Write8(u32 address, u8 value)
void StarletMemory::Write16(u32 address, u16 value)
{
if (!m_watch_ranges.empty())
WatchCPU(address, 2, value);
// An aligned scalar cannot cross an SRAM bank, aperture or boot0-overlay boundary.
// Keep this on the bus (including live mapping/protection), but resolve it only once.
if ((address & 1) == 0 && IsSRAMWindowAddress(address))
@@ -4010,6 +4240,8 @@ void StarletMemory::Write16(u32 address, u16 value)
void StarletMemory::Write32(u32 address, u32 value)
{
if (!m_watch_ranges.empty())
WatchCPU(address, 4, value);
// SRAM is ordinary byte-addressed storage, not MMIO. Aligned words stay inside one
// mapping segment, so there is no need to decode every device or map all four bytes.
// Unaligned/boundary accesses below still use the original byte-exact bus path.
@@ -4039,6 +4271,11 @@ 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();
if (address == HW_BASE + 0x0c && (value & 0x09) != 0)
{
if (Starlet* const starlet = m_system.GetStarlet())
@@ -4259,7 +4496,7 @@ bool StarletMemory::CommitNANDPages(const NANDJournal::Pages& pages)
return true;
}
bool StarletMemory::InstallEmulatedWiimotePairings()
bool StarletMemory::InstallEmulatedWiimotePairings(bool allow_nand_rewrite)
{
using NANDSuperblock = DiscIO::NANDImporter::NANDSuperblock;
using NANDFSTEntry = DiscIO::NANDImporter::NANDFSTEntry;
@@ -4499,6 +4736,12 @@ bool StarletMemory::InstallEmulatedWiimotePairings()
trusted_count);
return true;
}
if (!allow_nand_rewrite)
{
INFO_LOG_FMT(IOS_WIIMOTE, "No paired Wii Remote identity in the raw NAND; keeping the "
"default identities without modifying NAND");
return true;
}
std::ranges::copy(pairing, sysconf.begin() + static_cast<ptrdiff_t>(*pairing_offset));
+39 -1
View File
@@ -72,6 +72,23 @@ public:
// Revolution IOS Phase 04C: bootstrap mode loads no keys, so the OTP reads zero. This
// loads a 1024-byte BootMii keys.bin so OTP reads return the console's own words.
bool AttachKeys(const std::string& path, std::string* error);
// StarletOS phase 11 (M1), emulator only: give a bootstrap-ELF session the emulated Wii
// Remotes of the original-IOS mode, with the identities already paired in the attached NAND's
// SYSCONF (keys needed to decrypt it). Never writes the NAND: without a paired identity the
// remotes keep their default addresses. Call after AttachKeys and a NAND attach.
bool EnableBootstrapWiimotes(std::string* error);
// StarletOS phase 11 (F1), emulator only: REV_WATCH=start:size[,start:size...] (hex) logs
// every Starlet CPU store (with the instruction's address and LR) and every device DMA write
// 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);
@@ -85,6 +102,7 @@ public:
bool IsIdlePollAddress(u32 address, u32 size) const override;
bool IsSliceStablePollAddress(u32 address, u32 size) const override;
u8* GetFastmemBase() const override;
std::vector<std::pair<u32, u32>> GetWatchRanges() const override;
u8* GetFastmemSRAMBase() const override;
const bool* GetFastmemBoot0Mapped() const override;
const bool* GetFastmemSRAMSplitMode() const override;
@@ -99,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;
@@ -202,7 +224,7 @@ private:
void CompleteNANDCommand(u32 command);
bool ReadRawNANDPage(u32 page, NANDPage* raw);
bool CommitNANDPages(const NANDJournal::Pages& pages);
bool InstallEmulatedWiimotePairings();
bool InstallEmulatedWiimotePairings(bool allow_nand_rewrite = true);
bool ReadNANDPage(u32 command);
bool ReadNANDID(u32 command);
bool ReadNANDStatus(u32 command);
@@ -336,7 +358,23 @@ 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;
const ARMCore* m_watch_core = nullptr;
bool m_watch_in_dma_buffer = false;
// Phase 11 H1: each watched range as the Starlet and the devices wrote it, and which of its
// bytes they wrote. Starlet reads and IPC replies compare guest memory with it.
std::vector<std::vector<u8>> m_watch_shadow;
std::vector<std::vector<u8>> m_watch_known;
bool WatchHit(u32 address, u32 size) const;
void WatchCPU(u32 address, u32 size, u32 value);
void WatchDMA(u32 address, const u8* data, u32 size);
void WatchRecord(u32 address, const u8* data, u32 size);
void WatchRead(u32 address, u32 size);
void WatchAudit();
bool m_nand_read_only = false;
bool m_boot0_mapped = true;
bool m_sram_split_mode = false;
+15 -2
View File
@@ -54,6 +54,7 @@
#include "Core/HW/WII_IPC.h"
#include "Core/IOS/Starlet/Starlet.h"
#include "Core/IOS/Starlet/StarletMemory.h"
#include "Core/HW/WII_IPC.h"
#include "Core/PowerPC/GDBStub.h"
#include "Core/PowerPC/JitInterface.h"
#include "Core/PowerPC/PowerPC.h"
@@ -270,8 +271,12 @@ T MMU::ReadFromHardware(u32 em_address)
}
else
{
return static_cast<T>(
m_memory.GetMMIOMapping()->Read<std::make_unsigned_t<T>>(m_system, em_address));
const auto value =
m_memory.GetMMIOMapping()->Read<std::make_unsigned_t<T>>(m_system, em_address);
// StarletOS phase 11 (J1): Broadway's hardware reads in the IPC trace's window.
if (IOS::g_starletos_hardware_window) [[unlikely]]
IOS::TraceStarletOSHardwareAccess(m_system, false, em_address, sizeof(T), value);
return static_cast<T>(value);
}
}
@@ -459,6 +464,10 @@ void MMU::WriteToHardware(u32 em_address, const u32 data, const u32 size)
return;
}
// StarletOS phase 11 (J1): Broadway's hardware writes in the IPC trace's window.
if (IOS::g_starletos_hardware_window) [[unlikely]]
IOS::TraceStarletOSHardwareAccess(m_system, true, em_address, size, data);
switch (size)
{
case 1:
@@ -1485,6 +1494,10 @@ u32 MMU::IsOptimizableMMIOAccess(u32 address, u32 access_size) const
if (m_power_pc.GetMemChecks().HasAny())
return 0;
// StarletOS phase 11 (J1): no inlined MMIO reads while the hardware window may open.
if (IOS::StarletOSHardwareTraceRequested())
return 0;
if (!m_ppc_state.msr.DR)
return 0;
+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.
@@ -1755,6 +1959,83 @@ TEST(StarletARMCore, ARMJitCompilesLongMultiplyFamily)
#endif
}
TEST(StarletARMCore, ARMv5TEEnhancedDSPInstructions)
{
// StarletOS phase 11 (I1): encodings from GNU as (-march=armv5te); results from the ARM
// Architecture Reference Manual. r0 = Rm, r1 = Rs or Rn, r7 = accumulator, r3/r4 results.
struct Case
{
u32 instruction;
u32 r0;
u32 r1;
u32 r3;
u32 r4;
u32 r7;
u32 expected_r3;
u32 expected_r4;
bool expected_q;
};
constexpr std::array cases = {
// smlabb r4, r0, r1, r7: a 88-byte entry 9 of a table (Revolution's RAM layer).
Case{0xe1047180, 9, 88, 0, 0, 0x13780000, 0, 0x13780318, false},
Case{0xe1640180, 0xffff0003, 0x0002fffe, 0, 0, 0, 0, 0xfffffffa, false}, // smulbb
Case{0xe16401a0, 0xffff0003, 0x0002fffe, 0, 0, 0, 0, 2, false}, // smultb
// smlatt r4, r0, r1, r7: 0x3fff0001 + 0x7fffffff overflows, wraps and sets Q.
Case{0xe10471e0, 0x7fff0000, 0x7fff0000, 0, 0, 0x7fffffff, 0, 0xbfff0000, true},
Case{0xe1247180, 0x00020000, 0x00000003, 0, 0, 10, 0, 16, false}, // smlawb
Case{0xe12401e0, 0xfffe0000, 0x00030000, 0, 0, 0, 0, 0xfffffffa, false}, // smulwt
// smlalbb r3, r4, r0, r1: 0x00000000_ffffffff + 1 carries into RdHi.
Case{0xe1443180, 1, 1, 0xffffffff, 0, 0, 0, 1, false},
Case{0xe1014050, 0x7fffffff, 1, 0, 0, 0, 0, 0x7fffffff, true}, // qadd
Case{0xe1214050, 0x80000000, 1, 0, 0, 0, 0, 0x80000000, true}, // qsub
Case{0xe1414050, 1, 0x40000000, 0, 0, 0, 0, 0x7fffffff, true}, // qdadd
Case{0xe1614050, 0, 3, 0, 0, 0, 0, 0xfffffffa, false}, // qdsub
};
for (const Case& test : cases)
{
TestBus interpreter_bus;
ARMCore interpreter(interpreter_bus);
#if defined(_M_X86_64)
TestBus jit_bus;
ARMCore jit(jit_bus);
std::array<ARMCore*, 2> cores = {&interpreter, &jit};
jit_bus.WriteARM(0, test.instruction);
jit_bus.WriteARM(4, 0xeafffffe); // b .
#else
std::array<ARMCore*, 1> cores = {&interpreter};
#endif
interpreter_bus.WriteARM(0, test.instruction);
interpreter_bus.WriteARM(4, 0xeafffffe); // b .
for (ARMCore* core : cores)
{
core->SetCPSR(static_cast<u32>(ARMCore::Mode::System));
core->SetRegister(0, test.r0);
core->SetRegister(1, test.r1);
core->SetRegister(3, test.r3);
core->SetRegister(4, test.r4);
core->SetRegister(7, test.r7);
core->SetRegister(15, 0);
}
#if defined(_M_X86_64)
jit.SetJitEnabled(true);
#endif
for (ARMCore* core : cores)
{
ASSERT_EQ(core->RunCycles(2), 2u);
EXPECT_EQ(core->GetRegister(4), test.expected_r4) << std::hex << test.instruction;
if (test.instruction == 0xe1443180)
EXPECT_EQ(core->GetRegister(3), test.expected_r3);
EXPECT_EQ((core->GetCPSR() & ARMCore::CPSR_Q) != 0, test.expected_q)
<< std::hex << test.instruction;
// The DSP instruction must not touch the condition flags (it is not a compare).
EXPECT_EQ(core->GetCPSR() & (ARMCore::CPSR_N | ARMCore::CPSR_Z | ARMCore::CPSR_C |
ARMCore::CPSR_V),
0u);
}
}
}
TEST(StarletARMCore, FCSESwitchPreservesTaggedTLBTranslations)
{
TestBus bus(0x10000);
@@ -3459,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,21 +1,31 @@
// 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>
#include "Common/MsgHandler.h"
#include "Core/Core.h"
#include "Core/HW/Memmap.h"
#include "Core/HW/WII_IPC.h"
#include "Core/IOS/Starlet/ARMCore.h"
#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,
@@ -25,7 +35,23 @@ enum class ExpectedOutcome
Phase2
};
bool Run(const std::string& path, bool jit, ExpectedOutcome expected)
// 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)
{
std::cout.flush();
std::cerr << "Dolphin alert (" << caption << "): " << text << std::endl;
std::_Exit(ALERT_EXIT_STATUS);
}
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)
@@ -56,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)
{
@@ -75,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);
@@ -124,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;
}
@@ -132,20 +211,48 @@ bool Run(const std::string& path, bool jit, ExpectedOutcome expected)
int main(int argc, char** argv)
{
if (argc < 2 || argc > 4)
Common::RegisterMsgAlertHandler(FailOnAlert);
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")
{
@@ -166,5 +273,53 @@ int main(int argc, char** argv)
return 2;
}
}
return Run(argv[1], jit, expected) ? 0 : 1;
// The Starlet shares MEM1 and MEM2 with Broadway. Back them as on a retail Wii (24 and 64 MiB,
// zeroed): without them, any Starlet access there, such as StarOS reading its SBR1 record
// header at 0x00200000, is an access to unmapped memory and raises an alert.
auto& system = Core::System::GetInstance();
system.SetIsWii(true);
system.GetMemory().Init();
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;
}
+105 -9
View File
@@ -46,10 +46,35 @@
"sha256": "7e9bcfff87f526510aad149e781ae57140e32f7b32beafec00271fbcdb152848",
"bytes": 4526
},
{
"path": "Source/Core/Core/HW/WII_IPC.cpp",
"sha256": "a2244dabdd1c9334a63c8ddd3f09ce3abe22e86198b1fd45b092cd238379a6c0",
"bytes": 24790
},
{
"path": "Source/Core/Core/HW/WII_IPC.h",
"sha256": "886a0ca83ff839013a170efd5d3bb9e32c2a8ad327ca5a78985da27b58ae7cf9",
"bytes": 4770
},
{
"path": "Source/Core/Core/IOS/Starlet/ARMCore.cpp",
"sha256": "40d45aed7c5edb4540105dcdee4dd8dcde88c21ba7f3279eff0a9564f2d02a29",
"bytes": 75597
"sha256": "2559d69e1ec654734f788312cf5f3dca4de63e22e0880626306e8d15a4585648",
"bytes": 80026
},
{
"path": "Source/Core/Core/IOS/Starlet/ARMCore.h",
"sha256": "c3263bebbbdb7cd1119cc74ad328a63ad969136c1755ccf8e06e6549f23da3aa",
"bytes": 14758
},
{
"path": "Source/Core/Core/IOS/Starlet/ARMJitX64.cpp",
"sha256": "724382f4f66442d51382d3249bea68620d7417a6c7f48b0c19efa60e942a183c",
"bytes": 109615
},
{
"path": "Source/Core/Core/IOS/Starlet/ARMJitX64.h",
"sha256": "7e99ddee84b738bbb1e72aae0c8e2ee31335499c9eeb9585dfe5725688ad8c9e",
"bytes": 13979
},
{
"path": "Source/Core/Core/IOS/Starlet/BootstrapELF.cpp",
@@ -63,8 +88,8 @@
},
{
"path": "Source/Core/Core/IOS/Starlet/Starlet.cpp",
"sha256": "78e3b006501af06fbef220d95213690f8a31ed5b3c36699c6e07ba58be3ba852",
"bytes": 26039
"sha256": "2d10d03f2434f03a58f0a8e5f6cdab93ee8e8028d94ad519d137e739b3a99467",
"bytes": 26556
},
{
"path": "Source/Core/Core/IOS/Starlet/Starlet.h",
@@ -73,13 +98,18 @@
},
{
"path": "Source/Core/Core/IOS/Starlet/StarletMemory.cpp",
"sha256": "34900b6c340e412f99ddb3ce6ba17bdb4d85ba7b27b2adc522315b158b675fca",
"bytes": 189757
"sha256": "f46cb37943c0feb9695e5d0541864e06fb47c2cbed7e6d48b03df64006181afd",
"bytes": 196308
},
{
"path": "Source/Core/Core/IOS/Starlet/StarletMemory.h",
"sha256": "cfa3aee13f260590ae9f7cdd48f8f08d1c67e4e6d647ef3eca6e01d472dbdd29",
"bytes": 14873
"sha256": "6d58d81d94b32a647dbc3babea3e943aee307c02d9f1fb8904d8ebaccd22cf05",
"bytes": 16542
},
{
"path": "Source/Core/Core/PowerPC/MMU.cpp",
"sha256": "25c302b6cf1365abca38f4bcf88666dc25c5799a93534da1f55ae0a0c18dbb68",
"bytes": 85125
},
{
"path": "Source/Core/DolphinNoGUI/PlatformHeadless.cpp",
@@ -122,5 +152,71 @@
"unpublishedHistoricalFileVersionsScanned": 67,
"privateFindings": 0,
"newFullBuildOrHardwareTest": "NOT RUN: publication of existing qualified bytes only; see companion phase reports"
}
},
"updates": [
{
"recordedAt": "2026-10-06",
"purpose": "StarletOS Phase 11 proposal L2: IPC request/reply metadata trace while the original IOS runs (DOLPHIN_IPC_TRACE=1); no behavior change otherwise",
"files": [
"Source/Core/Core/HW/WII_IPC.cpp"
]
},
{
"recordedAt": "2026-10-06",
"purpose": "StarletOS Phase 11 proposal M1: REV_BOOTSTRAP_WIIMOTE=1 gives a bootstrap-ELF (Revolution) session the emulated Wii Remotes with the identity paired in the attached NAND; never writes the NAND; off by default",
"files": [
"Source/Core/Core/IOS/Starlet/Starlet.cpp",
"Source/Core/Core/IOS/Starlet/StarletMemory.cpp",
"Source/Core/Core/IOS/Starlet/StarletMemory.h"
]
},
{
"recordedAt": "2026-10-06",
"purpose": "StarletOS Phase 11 proposal M1e: the IPC trace also prints the paths named by filesystem IOCTLs (64-byte path, 76-byte record at +6, rename pair, first IOCTLV vector of 64 bytes); paths only",
"files": [
"Source/Core/Core/HW/WII_IPC.cpp"
]
},
{
"recordedAt": "2026-10-06",
"purpose": "StarletOS Phase 11 proposal F1: REV_WATCH=start:size[,...] logs Starlet CPU stores (instruction address, LR) and device DMA writes in those physical ranges; the ARM JIT routes accesses in watched ranges to its bus slow path and records the instruction address there; off by default",
"files": [
"Source/Core/Core/IOS/Starlet/Starlet.cpp",
"Source/Core/Core/IOS/Starlet/StarletMemory.cpp",
"Source/Core/Core/IOS/Starlet/StarletMemory.h",
"Source/Core/Core/IOS/Starlet/ARMCore.h",
"Source/Core/Core/IOS/Starlet/ARMJitX64.cpp",
"Source/Core/Core/IOS/Starlet/ARMJitX64.h"
]
},
{
"recordedAt": "2026-10-06",
"purpose": "StarletOS Phase 11 proposal H1: with REV_WATCH, a shadow of each watched range (Starlet stores and device DMA); Starlet reads that differ from it (REVWATCH DIFF) and IPC-reply audits (REVWATCH AUDIT) reveal other writers; register-list transfers whose virtual range overlaps a watched range run through the architectural helper and log their translation (REVWATCH LDM/STM)",
"files": [
"Source/Core/Core/IOS/Starlet/ARMCore.cpp",
"Source/Core/Core/IOS/Starlet/ARMCore.h",
"Source/Core/Core/IOS/Starlet/ARMJitX64.cpp",
"Source/Core/Core/IOS/Starlet/StarletMemory.cpp",
"Source/Core/Core/IOS/Starlet/StarletMemory.h"
]
},
{
"recordedAt": "2026-10-06",
"purpose": "StarletOS Phase 11 proposal I1: the ARM interpreter executes the ARMv5TE enhanced DSP instructions (SMLAxy, SMLAWy, SMULWy, SMLALxy, SMULxy, QADD, QSUB, QDADD, QDSUB, Q flag), previously decoded as compare data processing that wrote no register; the JIT leaves them to the interpreter; test ARMv5TEEnhancedDSPInstructions",
"files": [
"Source/Core/Core/IOS/Starlet/ARMCore.cpp",
"Source/Core/Core/IOS/Starlet/ARMCore.h",
"Source/Core/Core/IOS/Starlet/ARMJitX64.cpp"
]
},
{
"recordedAt": "2026-10-06",
"purpose": "StarletOS Phase 11 proposal J1: with DOLPHIN_IPC_TRACE=1 and DOLPHIN_HW_WINDOW=1, Broadway hardware register accesses (MMU MMIO reads and writes, PC) are logged as HWTRACE from the reply to an IOCTL 0x28 32/32 request until the next IPC request; no inlined MMIO reads while requested",
"files": [
"Source/Core/Core/HW/WII_IPC.cpp",
"Source/Core/Core/HW/WII_IPC.h",
"Source/Core/Core/PowerPC/MMU.cpp"
]
}
]
}
+206
View File
@@ -43,3 +43,209 @@ Read AGENTS.md. Record the starting commit, changed behavior, actual tests and
remaining work. Update this handoff and any changed file hashes, commit to this
fork, then update the pinned dependency in StarletOS. Never label a build, a
source comparison or a fake emulator device as a physical Wii observation.
## Linux qualification — 2026-10-03
Starting checkout: master `54f5a74bbf4e3be8418a3e98d006436c16a23e40`, clean;
fetch and fast-forward-only pull found no divergence. All recursive submodules
were initialized at their recorded commits. No runtime/source bytes changed;
this addition records the first build on the destination Linux PC.
- Debian 13 x86_64, GCC 14.2.0, CMake 3.31.6, Ninja 1.12.1. Release build in
`.starlet_linux`: `StarletBootstrapProbe`, `dolphin-nogui`, `tests` succeeded.
Qt, X11/EGL, desktop audio/input, LLVM disassembly and optional integrations
were disabled; headless and tests enabled. The complete configuration is in
the companion `docs/LINUX_QUALIFICATION.md`. This is not a GUI qualification.
- 95 selected tests of bootstrap ELF loading, ARM execution, SRAM, DMA,
timer, register and IPC behavior passed. Google Test also reports two
existing disabled tests; they were not enabled. The rest of Dolphin's
unit suite was not run.
- Ten standalone bootstrap runs passed: idle, panic, fatal data, undefined
instruction and Phase 02 self-test, each in interpreter and JIT. Rust used
the companion's pinned nightly-2026-09-20. The Phase 02 trace still says
IRQ delivery deferred and timer flag relatch untested; no hardware inference.
- SHA-256 of Linux `StarletBootstrapProbe`:
`a536e5125c193d3ae23f3a5028e2f34df0359785d86191500c65ce7bcd9610f9`.
Tests: `6acf5f26206e5afee2b3fbe3fd149b7c7c4dcd1f035d4dc12b89193df1d13a6f`.
NoGUI: `bd0f9420428f07a1c8785c131f430cc0f9eed88a5723e2e56546291fa3b72544`.
- Logs are private/local in companion `target/linux-qualification-20261003/`;
their hashes and commands are summarized in its public traceability report.
All 22 pinned integration file hashes still match. The private Menu profile
and NAND were not used or modified by these standalone tests. No full Menu
replay or physical Wii test is established by this build.
Next: use the companion handoff. Stock-IOS H02B has now been copied to the
identified SD and verified after remount; the console result is pending.
Rebuilding its PPC binary under Linux remains unqualified (the exact devkitPro
packages could not be downloaded: HTTP 403). Preserve the archived probe.
This documentation commit changes the dependency pin, not the built code;
the executables above still report their build's original commit.
## Phase 11 IPC trace for the original IOS — 2026-10-06
Starting checkout: master `78738093ece433e9198213f91d43a3a0d60e3fa2`, clean;
fetch found no divergence. Companion: StarletOS Phase 11 proposal L2
(`docs/PHASE11_PROPOSAL.md`), approved by the user.
- `Source/Core/Core/HW/WII_IPC.cpp`: with the environment variable
`DOLPHIN_IPC_TRACE=1` and the original IOS running on the emulated Starlet
(`WiiIOSLLE`), every request Broadway posts (PPCCTRL X1) is logged as
`IPCTRACE RQ <addr> C <command> FD <fd> A <five argument words>` (an OPEN
adds ` P <path>`), and every reply Starlet posts (ARMCTRL Y1) as
`IPCTRACE RP <addr> C <original command> RC <result>`. Notice level, `WII_IPC`
log type. Metadata only: buffer contents are never read. Without the
variable, or in HLE mode, nothing changes.
- The file is now an exact integration file (`.gitattributes -text`, hash in
`docs/StarletOS_Checkpoint.json`, 23 files).
- Build: `.starlet_linux` `dolphin-emu-nogui` and `tests` rebuilt; the 95
selected Starlet tests (filter in the companion `docs/LINUX_QUALIFICATION.md`)
passed.
- Emulation smoke run (supporting evidence only, private log in the companion
`target/phase11/l2/`): headless, Null video, the companion's Phase 10C NAND
read-only behind a private profile's COW journal, original boot chain to the
System Menu for 5 minutes: 74342 requests and 74337 replies traced (5
asynchronous requests still pending at the stop).
- Limitation: this Linux build has no window platform (X11 disabled; the
system lacks the `xi` development package), so no one can point and click in
it yet. A separate X11 build directory is the next step once `libxi-dev` is
installed; the qualified headless build stays unchanged.
Next: the companion's Phase 11 L2c (the user launches the Mii Channel from the
Menu in a window with the trace on); analysis stays in the companion.
## X11 window build and the reference launch — 2026-10-06
No source change. The user installed `libxi-dev` and `libxrandr-dev`; a
separate build directory `.starlet_linux_x11` (the qualified options with
`ENABLE_X11=ON`, tests off) built `dolphin-nogui` in 2 min 53 s
(SHA-256 `a7d372f47e7265a4ebe20c71bde6f8dd63f22416b422551b960f526a604ebda4`).
The qualified headless `.starlet_linux` build is unchanged. On KDE Plasma
Wayland the window runs through XWayland (`-p x11`, Vulkan); the emulated
Wii Remote reads the mouse through XInput2.
Companion session L2c (emulation, supporting only): original boot chain and
IOS80, the companion's Phase 10C NAND read-only (hash unchanged after), no
Ethernet NAT; the user launched the Mii Channel from the System Menu and it
started. The `IPCTRACE` log shows ES LAUNCH right after the WiiConnect24
shutdown request; analysis is in the companion's Phase 11 STATUS (finding 9).
Known limit for the next companion step: in bootstrap (Revolution) mode the
emulated Wii Remotes are not initialized (`m_initialized` stays false), so
nobody can point at a channel there. Changing that is proposed in the
companion (proposal M1), not done.
## Emulated Wii Remotes for a Revolution session — 2026-10-06
Companion proposal M1 (approved by the user): replay the reference launch
with Revolution instead of the original IOS, to see where Broadway stops.
- `REV_BOOTSTRAP_WIIMOTE=1` (bootstrap-ELF mode, after the NAND and keys
attaches): `StarletMemory::EnableBootstrapWiimotes` selects the identities
paired in the attached NAND's SYSCONF (`BT.DINF`, decrypted with the keys)
and creates the emulated Wii Remotes exactly as the original-IOS mode does.
`InstallEmulatedWiimotePairings(false)` never rewrites SYSCONF: without a
paired identity the remotes keep their default addresses. Without the
variable nothing changes (the remotes stay absent, as before).
- Checkpoint hashes updated for `Starlet.cpp`, `StarletMemory.cpp`,
`StarletMemory.h`.
- Build: `.starlet_linux` (`dolphin-emu-nogui`, `tests`) and
`.starlet_linux_x11`; the 95 selected Starlet tests passed.
- Without the variable, the companion's headless P11-H06 Menu regression
(`tools/regression/phase11_menu.py --step H06 --outcome Phase9Running
--timeout 600`) still passes on this build, NAND source and disposable
copy identical.
Next: the companion's M1b (smoke test with the variable, no window) and M1c
(the user launches the Mii Channel under Revolution in the X11 window).
## Filesystem paths in the IPC trace — 2026-10-06
Companion proposal M1e (approved): under Revolution in the window the System
Menu's save before a launch got -106 on its second CREATE_DIR, and no trace
had the path. `IPCTRACE RQ` lines now end with ` F <path>` for IOCTLs whose
input is a 64-byte path (DELETE, GET_ATTR), a 76-byte attribute record (path
at +6: CREATE_DIR, CREATE_FILE, SET_ATTR) or two paths (` F a > b`, RENAME),
and for IOCTLVs whose first input vector is 64 bytes (READ_DIR, GET_USAGE);
` F -` when that field holds no valid path. Read from memory when X1 is
rung, like the OPEN path; nothing else of the buffers. Checked headless under
Revolution: `/shared2/test2`, `/title/00000001/00000002/data/cache.dat`, the
GET_USAGE directories. 95 Starlet tests passed.
## Write watch for Starlet memory — 2026-10-06
Companion proposal F1 (approved): find what changes Revolution's RAM layer
table between two filesystem requests (the companion's finding 11).
- `REV_WATCH=start:size[,start:size...]` (hexadecimal, physical): every
Starlet CPU store (`StarletMemory::Write8/16/32`) and every device DMA
write (`WriteDMABuffer`, `WriteDMA8`) touching a range is logged as
`REVWATCH CPU addr/size value PC <instruction> LR <r14>` or
`REVWATCH DMA addr/size PC <instruction>`. Off without the variable.
- The ARM JIT keeps fastmem; `ARMBus::GetWatchRanges` gives it the ranges,
`EmitFastmemAddress` sends any access overlapping one to the existing bus
slow path, and the ARM and Thumb slow paths store the instruction address
first (only while watching). Speed under watch is the JIT's (companion
smoke run: the Menu's main menu at 74 s, as without the watch).
- `ARMCore.h` (`GetInstructionAddress`, `GetWatchRanges`), `ARMJitX64.cpp`,
`ARMJitX64.h` are now exact integration files (26 in the checkpoint).
- The 95 selected Starlet tests passed (`.starlet_linux`); `.starlet_linux_x11`
rebuilt. Smoke run with the companion's Revolution P11-H06: 1519 watched
writes on its layer table during start-up, attributed by instruction.
## Shadow comparison for the write watch — 2026-10-06
Companion proposal H1 (approved). The companion's diagnostic image showed
its kernel reading back, from its RAM layer table, an entry different from
the one it had just written, with no other Starlet store or device DMA seen.
- With `REV_WATCH`, `StarletMemory` keeps a shadow of each range, updated by
the Starlet stores and device DMA it logs. A Starlet read whose bytes differ
from the shadow logs `REVWATCH DIFF addr/size read … expected … PC … LR …`
(another writer changed them: Broadway, its GPU, the DSP…); each IPC reply
(ARMCTRL Y1) audits the ranges (`REVWATCH AUDIT addr+len now …`, at most 16
runs per reply). The shadow then adopts the new bytes.
- Register-list transfers whose virtual range overlaps a watched range now
take the JIT's architectural helper, and the interpreter logs
`REVWATCH LDM|STM virtual->physical/size PC …` (its own translation).
- `ARMCore.cpp` keeps its CRLF bytes. 95 Starlet tests passed. Companion
smoke run (P11-H06D, 150 s): no DIFF, no AUDIT, 9 LDM with identical
virtual and physical addresses.
## ARMv5TE enhanced DSP instructions — 2026-10-06
Companion proposal I1 (approved). The companion's Revolution kernel computes
table addresses with `smlabb`; the ARM926EJ-S executes it, but
`ARMCore::ExecuteARM` had no case for the ARMv5TE enhanced DSP instructions,
so `smlabb` fell into data processing as `TST` (no destination written, flags
changed). The kernel then read the wrong RAM-layer entry and refused a
creation -106 ("system files corrupted" in the System Menu), in the fork only.
- `ARMCore::ExecuteEnhancedDSP`: SMLAxy, SMLAWy, SMULWy, SMLALxy, SMULxy,
QADD, QSUB, QDADD, QDSUB, with the sticky Q flag (ARM Architecture
Reference Manual semantics), decoded before the generic decoders.
- `CanEmitARMDataProcessing` excludes these encodings, so the JIT leaves them
to the interpreter.
- Test `StarletARMCore.ARMv5TEEnhancedDSPInstructions`: eleven encodings from
GNU as (`-march=armv5te`), interpreter and JIT, results, Q and NZCV
untouched. 96 selected Starlet tests passed.
- The companion's images use, besides these (16 `smlabb`, 4 `smulbb`), only
`blx`, `clz`, `ldrd`, `strd` among the ARMv5TE additions; those were
already handled.
## Broadway hardware window after the WiiConnect24 shutdown — 2026-10-06
Companion proposal J1 (approved). With the DSP fix, the companion's System
Menu under Revolution reaches ES LAUNCH in the fork, while on the console it
stops right after the WiiConnect24 shutdown reply (IOCTL 0x28), with no IPC
until then. This lists what Broadway touches in that gap.
- `DOLPHIN_HW_WINDOW=1` (with `DOLPHIN_IPC_TRACE=1`): the IPC trace remembers
an IOCTL 0x28 with 32-byte input and output; its reply opens a window, and
`MMU::ReadFromHardware`/`WriteToHardware` log each Broadway MMIO access as
`HWTRACE R|W address/size value PC` until Broadway's next IPC request (or
20000 accesses). `IsOptimizableMMIOAccess` returns 0 while requested, so the
JIT inlines no MMIO read. Nothing changes without the variables.
- `MMU.cpp` and `WII_IPC.h` are now exact integration files (28).
- 96 Starlet tests passed; both builds rebuilt.