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:
1 parent
81ebec368b
commit
76a133f3a4
9 files changed
+161
-6
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user