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;