diff --git a/.gitattributes b/.gitattributes index be86e49bbb..7f4521a7bf 100644 --- a/.gitattributes +++ b/.gitattributes @@ -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 diff --git a/Source/Core/Core/IOS/Starlet/ARMCore.h b/Source/Core/Core/IOS/Starlet/ARMCore.h index eed1ed4c35..45897c58ba 100644 --- a/Source/Core/Core/IOS/Starlet/ARMCore.h +++ b/Source/Core/Core/IOS/Starlet/ARMCore.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #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> 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; diff --git a/Source/Core/Core/IOS/Starlet/ARMJitX64.cpp b/Source/Core/Core/IOS/Starlet/ARMJitX64.cpp index 1a65f113db..82f6999b31 100644 --- a/Source/Core/Core/IOS/Starlet/ARMJitX64.cpp +++ b/Source/Core/Core/IOS/Starlet/ARMJitX64.cpp @@ -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* 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& 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& 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 diff --git a/Source/Core/Core/IOS/Starlet/ARMJitX64.h b/Source/Core/Core/IOS/Starlet/ARMJitX64.h index f31b12b3cb..35e0a84bd2 100644 --- a/Source/Core/Core/IOS/Starlet/ARMJitX64.h +++ b/Source/Core/Core/IOS/Starlet/ARMJitX64.h @@ -250,6 +250,8 @@ private: std::array m_fast_entry_next_victim{}; std::array m_fast_translations{}; u8* m_fastmem_base = nullptr; + // Phase 11 F1: watched physical ranges, routed to the bus slow path. + std::vector> m_watch_ranges; u8* m_sram_base = nullptr; const bool* m_boot0_mapped = nullptr; const bool* m_sram_split_mode = nullptr; diff --git a/Source/Core/Core/IOS/Starlet/Starlet.cpp b/Source/Core/Core/IOS/Starlet/Starlet.cpp index ef6ffae722..ad29974df0 100644 --- a/Source/Core/Core/IOS/Starlet/Starlet.cpp +++ b/Source/Core/Core/IOS/Starlet/Starlet.cpp @@ -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; diff --git a/Source/Core/Core/IOS/Starlet/StarletMemory.cpp b/Source/Core/Core/IOS/Starlet/StarletMemory.cpp index 4e3b081571..0e18d09528 100644 --- a/Source/Core/Core/IOS/Starlet/StarletMemory.cpp +++ b/Source/Core/Core/IOS/Starlet/StarletMemory.cpp @@ -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 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> 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 buffer) const Common::PerformanceTrace::Scope trace(Common::PerformanceTrace::Region::DMAWrite); if (buffer.empty()) return; + if (!m_watch_ranges.empty()) + WatchDMA(address, static_cast(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(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. diff --git a/Source/Core/Core/IOS/Starlet/StarletMemory.h b/Source/Core/Core/IOS/Starlet/StarletMemory.h index 46a3b5cf36..d3faed301f 100644 --- a/Source/Core/Core/IOS/Starlet/StarletMemory.h +++ b/Source/Core/Core/IOS/Starlet/StarletMemory.h @@ -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> 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> 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; diff --git a/docs/StarletOS_Checkpoint.json b/docs/StarletOS_Checkpoint.json index cf46e49348..949287c0df 100644 --- a/docs/StarletOS_Checkpoint.json +++ b/docs/StarletOS_Checkpoint.json @@ -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" + ] } ] } diff --git a/docs/StarletOS_Handoff.md b/docs/StarletOS_Handoff.md index 90e8030d2b..6b2e980d2a 100644 --- a/docs/StarletOS_Handoff.md +++ b/docs/StarletOS_Handoff.md @@ -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 LR ` or + `REVWATCH DMA addr/size PC `. 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.