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]>
This commit is contained in:
Yaya48andClaude Opus 5.5 committed 2026-10-06 20:21:09 +02:00
1 parent 81ebec368b
commit 76a133f3a4
9 files changed
+161 -6

No files matched your search

+3
View File
@@ -11,6 +11,9 @@ Source/Core/Core/ConfigManager.cpp -text
Source/Core/Core/HW/HW.cpp -text
Source/Core/Core/HW/WII_IPC.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
+6
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;
@@ -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.
@@ -2255,6 +2256,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 +2395,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 +2492,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;
+1
View File
@@ -150,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;
@@ -462,6 +462,54 @@ 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);
}
}
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) const
{
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);
}
void StarletMemory::WatchDMA(u32 address, u32 size) const
{
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);
}
bool StarletMemory::Init(const std::string& dump_directory, std::string* error)
@@ -1045,6 +1093,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
@@ -1185,6 +1238,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, 1);
if (address >= SRAM_BASE && address < SRAM_BASE + SRAM_WINDOW_SIZE)
{
const u32 offset = address - SRAM_BASE;
@@ -1236,13 +1291,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, 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()
@@ -3780,6 +3839,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;
@@ -3993,6 +4054,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))
@@ -4030,6 +4093,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.
@@ -77,6 +77,11 @@ public:
// 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);
@@ -90,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;
@@ -342,6 +348,13 @@ 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;
bool WatchHit(u32 address, u32 size) const;
void WatchCPU(u32 address, u32 size, u32 value) const;
void WatchDMA(u32 address, u32 size) const;
bool m_nand_read_only = false;
bool m_boot0_mapped = true;
bool m_sram_split_mode = false;
+33 -6
View File
@@ -56,6 +56,21 @@
"sha256": "40d45aed7c5edb4540105dcdee4dd8dcde88c21ba7f3279eff0a9564f2d02a29",
"bytes": 75597
},
{
"path": "Source/Core/Core/IOS/Starlet/ARMCore.h",
"sha256": "765bc1ebe9554f9821fd649a94a74f27d2b811383b73286062bf382d6a15aee6",
"bytes": 14356
},
{
"path": "Source/Core/Core/IOS/Starlet/ARMJitX64.cpp",
"sha256": "2aa934e97097b25fdf760a9a97849ee066439f1ea7ad231b9329611feeb24e23",
"bytes": 109011
},
{
"path": "Source/Core/Core/IOS/Starlet/ARMJitX64.h",
"sha256": "7e99ddee84b738bbb1e72aae0c8e2ee31335499c9eeb9585dfe5725688ad8c9e",
"bytes": 13979
},
{
"path": "Source/Core/Core/IOS/Starlet/BootstrapELF.cpp",
"sha256": "344e6e38922b64fa58d9755c2df5fd752b00cf3dc73114809f6e9cada724c42a",
@@ -68,8 +83,8 @@
},
{
"path": "Source/Core/Core/IOS/Starlet/Starlet.cpp",
"sha256": "9654635b4892f1e07bb72256cbf34b7bebd89eee8389154af946b47eab011f29",
"bytes": 26516
"sha256": "2d10d03f2434f03a58f0a8e5f6cdab93ee8e8028d94ad519d137e739b3a99467",
"bytes": 26556
},
{
"path": "Source/Core/Core/IOS/Starlet/Starlet.h",
@@ -78,13 +93,13 @@
},
{
"path": "Source/Core/Core/IOS/Starlet/StarletMemory.cpp",
"sha256": "da5267d0cede011e74ed56155999ec721a562680c07b9c928f56fcea191f20c9",
"bytes": 190485
"sha256": "b6896d66410e08629b1dee568f94326a1caab1fa2740ad8c886f50b4a7d494de",
"bytes": 192641
},
{
"path": "Source/Core/Core/IOS/Starlet/StarletMemory.h",
"sha256": "954cf8d3253eeebe6910ef9fb92137e3a551d04551ed8f1fbfc6fff907c591ce",
"bytes": 15322
"sha256": "3005d026366a27d28e2bab0ee6cdbb9fffc347e10a384b77f19e23db1ec09ca0",
"bytes": 16146
},
{
"path": "Source/Core/DolphinNoGUI/PlatformHeadless.cpp",
@@ -151,6 +166,18 @@
"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"
]
}
]
}
+21
View File
@@ -172,3 +172,24 @@ and for IOCTLVs whose first input vector is 64 bytes (READ_DIR, GET_USAGE);
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.