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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user