Compare commits

..
Author SHA1 Message Date
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
15 changed files with 986 additions and 15 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:
+109
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
@@ -36,9 +37,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)
@@ -1407,6 +1423,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);
@@ -2252,6 +2278,8 @@ 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);
u32 loaded_pc = 0;
bool pc_loaded = false;
@@ -2309,6 +2337,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;
+12
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;
@@ -305,8 +311,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);
@@ -363,6 +373,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;
+29 -1
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.
@@ -1163,7 +1164,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 +1478,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));
@@ -2255,6 +2267,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 +2406,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 +2503,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
+2
View File
@@ -250,6 +250,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;
+12
View File
@@ -128,6 +128,17 @@ 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.
@@ -139,6 +150,7 @@ bool Starlet::Init(const std::string& dump_directory, std::string* error)
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;
+200 -1
View File
@@ -462,6 +462,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)
@@ -792,6 +939,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");
@@ -1025,6 +1192,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 +1337,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 +1390,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 +3710,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 +3833,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 +3869,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 +3944,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;
@@ -3973,6 +4159,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 +4198,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 +4229,9 @@ void StarletMemory::Write32(u32 address, u32 value)
{
WriteRegister(address, value);
m_system.GetWiiIPC().WriteStarletRegister(address - HW_BASE, value);
// 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 +4452,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 +4692,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));
+26 -1
View File
@@ -72,6 +72,16 @@ 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);
void Reset();
void DoState(PointerWrap& p);
@@ -85,6 +95,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;
@@ -202,7 +213,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);
@@ -337,6 +348,20 @@ private:
u64 m_arm_cycles = 0;
u32 m_timer_offset = 0;
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;
@@ -1755,6 +1755,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);
@@ -1,13 +1,16 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cstdlib>
#include <fstream>
#include <iostream>
#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"
@@ -25,6 +28,18 @@ enum class ExpectedOutcome
Phase2
};
// Exit status of a run stopped by a Dolphin alert (1: failed expectation, 2: usage).
constexpr int ALERT_EXIT_STATUS = 3;
// 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, ExpectedOutcome expected)
{
std::ifstream file(path, std::ios::binary | std::ios::ate);
@@ -132,6 +147,7 @@ bool Run(const std::string& path, bool jit, ExpectedOutcome expected)
int main(int argc, char** argv)
{
Common::RegisterMsgAlertHandler(FailOnAlert);
if (argc < 2 || argc > 4)
{
std::cerr << "Usage: StarletBootstrapProbe <ELF> [--jit] "
@@ -166,5 +182,14 @@ 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();
const bool passed = Run(argv[1], jit, expected);
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.