From ba5b6de0d04f8d562a9d78e3979eb055f567d944 Mon Sep 17 00:00:00 2001 From: Yaya48 Date: Tue, 6 Oct 2026 22:08:04 +0200 Subject: [PATCH] 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 --- Source/Core/Core/IOS/Starlet/ARMCore.cpp | 18 +++ Source/Core/Core/IOS/Starlet/ARMCore.h | 4 + Source/Core/Core/IOS/Starlet/ARMJitX64.cpp | 9 ++ .../Core/Core/IOS/Starlet/StarletMemory.cpp | 116 +++++++++++++++++- Source/Core/Core/IOS/Starlet/StarletMemory.h | 11 +- docs/StarletOS_Checkpoint.json | 31 +++-- docs/StarletOS_Handoff.md | 19 +++ 7 files changed, 192 insertions(+), 16 deletions(-) diff --git a/Source/Core/Core/IOS/Starlet/ARMCore.cpp b/Source/Core/Core/IOS/Starlet/ARMCore.cpp index 06dba48cdb..274e5bb1ea 100644 --- a/Source/Core/Core/IOS/Starlet/ARMCore.cpp +++ b/Source/Core/Core/IOS/Starlet/ARMCore.cpp @@ -12,6 +12,7 @@ #include #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) @@ -2252,6 +2268,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; diff --git a/Source/Core/Core/IOS/Starlet/ARMCore.h b/Source/Core/Core/IOS/Starlet/ARMCore.h index 45897c58ba..3b493f7951 100644 --- a/Source/Core/Core/IOS/Starlet/ARMCore.h +++ b/Source/Core/Core/IOS/Starlet/ARMCore.h @@ -311,6 +311,8 @@ 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); void ExecuteBranch(u32 instruction); @@ -369,6 +371,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> m_watch_ranges; u32 m_last_undefined_instruction = 0; u64 m_executed_instructions = 0; u64 m_memory_poll_entry_count = 0; diff --git a/Source/Core/Core/IOS/Starlet/ARMJitX64.cpp b/Source/Core/Core/IOS/Starlet/ARMJitX64.cpp index 82f6999b31..ace927e24d 100644 --- a/Source/Core/Core/IOS/Starlet/ARMJitX64.cpp +++ b/Source/Core/Core/IOS/Starlet/ARMJitX64.cpp @@ -1476,6 +1476,15 @@ bool ARMJitX64::EmitARMBlockTransfer(u32 instruction, u32 address, bool* termina } std::vector 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)); diff --git a/Source/Core/Core/IOS/Starlet/StarletMemory.cpp b/Source/Core/Core/IOS/Starlet/StarletMemory.cpp index 0e18d09528..a0843771ec 100644 --- a/Source/Core/Core/IOS/Starlet/StarletMemory.cpp +++ b/Source/Core/Core/IOS/Starlet/StarletMemory.cpp @@ -476,7 +476,11 @@ StarletMemory::StarletMemory(Core::System& system) : m_system(system) 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); + } } } @@ -495,21 +499,116 @@ bool StarletMemory::WatchHit(u32 address, u32 size) const return false; } -void StarletMemory::WatchCPU(u32 address, u32 size, u32 value) const +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 bytes{}; + for (u32 i = 0; i < size; ++i) + bytes[i] = static_cast(value >> (8 * (size - 1 - i))); + WatchRecord(address, bytes.data(), size); } -void StarletMemory::WatchDMA(u32 address, u32 size) const +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) @@ -1239,7 +1338,7 @@ 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); + WatchDMA(address, &value, 1); if (address >= SRAM_BASE && address < SRAM_BASE + SRAM_WINDOW_SIZE) { const u32 offset = address - SRAM_BASE; @@ -1292,7 +1391,7 @@ void StarletMemory::WriteDMABuffer(u32 address, std::span buffer) if (buffer.empty()) return; if (!m_watch_ranges.empty()) - WatchDMA(address, static_cast(buffer.size())); + WatchDMA(address, buffer.data(), static_cast(buffer.size())); if (u8* memory = GetDMAMemoryPointer(address, buffer.size())) { std::memcpy(memory, buffer.data(), buffer.size()); @@ -3611,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)]; @@ -3732,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)) @@ -3766,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 @@ -4124,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()) diff --git a/Source/Core/Core/IOS/Starlet/StarletMemory.h b/Source/Core/Core/IOS/Starlet/StarletMemory.h index d3faed301f..fc814effa2 100644 --- a/Source/Core/Core/IOS/Starlet/StarletMemory.h +++ b/Source/Core/Core/IOS/Starlet/StarletMemory.h @@ -352,9 +352,16 @@ private: std::vector> 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> m_watch_shadow; + std::vector> m_watch_known; bool WatchHit(u32 address, u32 size) const; - void WatchCPU(u32 address, u32 size, u32 value) const; - void WatchDMA(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; diff --git a/docs/StarletOS_Checkpoint.json b/docs/StarletOS_Checkpoint.json index 949287c0df..6300272b79 100644 --- a/docs/StarletOS_Checkpoint.json +++ b/docs/StarletOS_Checkpoint.json @@ -53,18 +53,18 @@ }, { "path": "Source/Core/Core/IOS/Starlet/ARMCore.cpp", - "sha256": "40d45aed7c5edb4540105dcdee4dd8dcde88c21ba7f3279eff0a9564f2d02a29", - "bytes": 75597 + "sha256": "262946f1fb95250d1aa1ca73c9bc27ef5f83511731bf84c765a41b04fe7ff70a", + "bytes": 76503 }, { "path": "Source/Core/Core/IOS/Starlet/ARMCore.h", - "sha256": "765bc1ebe9554f9821fd649a94a74f27d2b811383b73286062bf382d6a15aee6", - "bytes": 14356 + "sha256": "fa80b0d697e19f0aee178be4b13097ad5466f0bbd0cb9b3e7a74876c045b731f", + "bytes": 14647 }, { "path": "Source/Core/Core/IOS/Starlet/ARMJitX64.cpp", - "sha256": "2aa934e97097b25fdf760a9a97849ee066439f1ea7ad231b9329611feeb24e23", - "bytes": 109011 + "sha256": "30ac3e6f74edd40a76406394ed40d66a1ca5b6b9a17b9699a1534d255320108f", + "bytes": 109425 }, { "path": "Source/Core/Core/IOS/Starlet/ARMJitX64.h", @@ -93,13 +93,13 @@ }, { "path": "Source/Core/Core/IOS/Starlet/StarletMemory.cpp", - "sha256": "b6896d66410e08629b1dee568f94326a1caab1fa2740ad8c886f50b4a7d494de", - "bytes": 192641 + "sha256": "f46cb37943c0feb9695e5d0541864e06fb47c2cbed7e6d48b03df64006181afd", + "bytes": 196308 }, { "path": "Source/Core/Core/IOS/Starlet/StarletMemory.h", - "sha256": "3005d026366a27d28e2bab0ee6cdbb9fffc347e10a384b77f19e23db1ec09ca0", - "bytes": 16146 + "sha256": "6d58d81d94b32a647dbc3babea3e943aee307c02d9f1fb8904d8ebaccd22cf05", + "bytes": 16542 }, { "path": "Source/Core/DolphinNoGUI/PlatformHeadless.cpp", @@ -178,6 +178,17 @@ "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" + ] } ] } diff --git a/docs/StarletOS_Handoff.md b/docs/StarletOS_Handoff.md index 6b2e980d2a..c18e384ca3 100644 --- a/docs/StarletOS_Handoff.md +++ b/docs/StarletOS_Handoff.md @@ -193,3 +193,22 @@ table between two filesystem requests (the companion's finding 11). - 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.