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

+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;