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 <[email protected]>
This commit is contained in:
Yaya48andClaude Opus 5.5 committed 2026-10-06 22:08:04 +02:00
1 parent 76a133f3a4
commit ba5b6de0d0
7 files changed
+192 -16

No files matched your search

+18
View File
@@ -12,6 +12,7 @@
#include <utility>
#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;
+4
View File
@@ -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<std::pair<u32, u32>> m_watch_ranges;
u32 m_last_undefined_instruction = 0;
u64 m_executed_instructions = 0;
u64 m_memory_poll_entry_count = 0;
@@ -1476,6 +1476,15 @@ bool ARMJitX64::EmitARMBlockTransfer(u32 instruction, u32 address, bool* termina
}
std::vector<FixupBranch> 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));
+112 -4
View File
@@ -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<u8, 4> bytes{};
for (u32 i = 0; i < size; ++i)
bytes[i] = static_cast<u8>(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<const u8> buffer)
if (buffer.empty())
return;
if (!m_watch_ranges.empty())
WatchDMA(address, static_cast<u32>(buffer.size()));
WatchDMA(address, buffer.data(), static_cast<u32>(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())
+9 -2
View File
@@ -352,9 +352,16 @@ private:
std::vector<std::pair<u32, u32>> 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<std::vector<u8>> m_watch_shadow;
std::vector<std::vector<u8>> 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;
+21 -10
View File
@@ -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"
]
}
]
}
+19
View File
@@ -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.