IOS: add x64 Starlet JIT with guarded fastmem

This commit is contained in:
2026-08-28 13:22:30 +02:00
parent b85667435c
commit 9389e38f75
14 changed files with 4835 additions and 19 deletions
+8
View File
@@ -3,6 +3,8 @@ param(
[string]$UserDirectory = '.starlet_user3',
[switch]$DisableJIT,
[switch]$Wait
)
@@ -17,6 +19,12 @@ if (-not (Test-Path -LiteralPath $dolphinPath)) {
$dolphin = Start-Process -FilePath $dolphinPath -ArgumentList @(
'-u', $userPath,
'-n', '0000000100000002',
'-C', "Dolphin.Core.WiiStarletJIT=$(-not $DisableJIT)",
# Keep the next BootMii run diagnostic rather than observational. BootMii reports its loader
# stages and panic codes through GPIO bits 16-23; the Starlet bus logs those writes under IOS.
'-C', 'Logger.Options.WriteToFile=True',
'-C', 'Logger.Options.Verbosity=4',
'-C', 'Logger.Logs.IOS=True',
'-v', 'D3D',
'-p', 'win32'
) -WorkingDirectory $repoRoot -PassThru
+2
View File
@@ -612,6 +612,8 @@ target_sources(core PRIVATE AchievementApprovedHash.h)
if(_M_X86_64)
target_sources(core PRIVATE
IOS/Starlet/ARMJitX64.cpp
IOS/Starlet/ARMJitX64.h
DSP/Jit/x64/DSPEmitter.cpp
DSP/Jit/x64/DSPEmitter.h
DSP/Jit/x64/DSPJitArithmetic.cpp
+1
View File
@@ -257,6 +257,7 @@ const Info<std::string> MAIN_GPU_DETERMINISM_MODE{{System::Main, "Core", "GPUDet
"auto"};
const Info<s32> MAIN_OVERRIDE_BOOT_IOS{{System::Main, "Core", "OverrideBootIOS"}, -1};
const Info<bool> MAIN_WII_IOS_LLE{{System::Main, "Core", "WiiIOSLLE"}, false};
const Info<bool> MAIN_WII_STARLET_JIT{{System::Main, "Core", "WiiStarletJIT"}, true};
GPUDeterminismMode GetGPUDeterminismMode()
{
+1
View File
@@ -167,6 +167,7 @@ extern const Info<DiscIO::Region> MAIN_FALLBACK_REGION;
extern const Info<bool> MAIN_REAL_WII_REMOTE_REPEAT_REPORTS;
extern const Info<s32> MAIN_OVERRIDE_BOOT_IOS;
extern const Info<bool> MAIN_WII_IOS_LLE;
extern const Info<bool> MAIN_WII_STARLET_JIT;
extern const Info<std::string> MAIN_WII_NUS_SHOP_URL;
extern const Info<bool> MAIN_WII_WIILINK_ENABLE;
+417 -7
View File
@@ -8,8 +8,14 @@
#include <bit>
#include <cassert>
#include <limits>
#include <utility>
#include "Common/ChunkFile.h"
#if defined(_M_X86_64)
// ARMCore owns ARMJitX64 by value through unique_ptr construction in this translation unit, so a
// private JIT layout change must rebuild this file as well as the emitter.
#include "Core/IOS/Starlet/ARMJitX64.h"
#endif
namespace IOS::LLE
{
@@ -30,6 +36,19 @@ ARMCore::ARMCore(ARMBus& bus) : m_bus(bus)
Reset();
}
ARMCore::~ARMCore() = default;
void ARMCore::SetJitEnabled(bool enabled)
{
#if defined(_M_X86_64)
if (enabled && !m_jit)
m_jit = std::make_unique<ARMJitX64>(*this);
m_jit_enabled = enabled;
#else
m_jit_enabled = false;
#endif
}
void ARMCore::Reset(u32 reset_vector)
{
m_registers.fill(0);
@@ -57,11 +76,19 @@ void ARMCore::Reset(u32 reset_vector)
m_waiting_for_interrupt = false;
m_waiting_for_memory_poll = false;
m_yield_requested = false;
m_slice_fast_forward_requested = false;
m_memory_poll_address = 0;
m_pc_written = false;
m_instruction_address = m_registers[15];
m_last_undefined_instruction = 0;
m_executed_instructions = 0;
m_memory_poll_entry_count = 0;
m_jit_fallback_instruction_count = 0;
m_jit_fallback_samples.clear();
m_jit_fallback_interval_count = 0;
m_jit_fallback_interval_samples.clear();
m_next_profile_instruction = PROFILE_SAMPLE_INTERVAL;
m_pc_samples.clear();
}
u32 ARMCore::GetRegister(size_t index) const
@@ -192,7 +219,7 @@ u32 ARMCore::TranslateVirtualAddress(u32 address) const
case 3: // 4 KiB small page; extended small pages share this mapping shape.
return cache_translation((second_level & 0xfffff000) | (modified_address & 0xfff));
default:
return modified_address;
return cache_translation(modified_address);
}
}
case 2: // 1 MiB section.
@@ -210,14 +237,16 @@ u32 ARMCore::TranslateVirtualAddress(u32 address) const
case 3: // 1 KiB tiny page.
return cache_translation((second_level & 0xfffffc00) | (modified_address & 0x3ff));
default:
return modified_address;
return cache_translation(modified_address);
}
}
default:
// Fault entry. Abort delivery and access-permission checks are
// intentionally introduced after basic page-table translation; identity
// fallback keeps diagnostics observable meanwhile.
return modified_address;
// fallback keeps diagnostics observable meanwhile. Cache that provisional translation just
// like a mapped page: real software must invalidate the TLB after changing a page-table entry,
// and without this cache the JIT would side-exit forever on every identity access.
return cache_translation(modified_address);
}
}
@@ -229,6 +258,10 @@ void ARMCore::InvalidateTLB()
entry.generation = 0;
m_tlb_generation = 1;
}
#if defined(_M_X86_64)
if (m_jit)
m_jit->InvalidateTranslationContext();
#endif
}
u32 ARMCore::FetchARMInstruction(u32 address)
@@ -277,6 +310,10 @@ void ARMCore::InvalidateInstructionCache()
entry.generation = 0;
m_instruction_cache_generation = 1;
}
#if defined(_M_X86_64)
if (m_jit)
m_jit->Clear();
#endif
}
u8 ARMCore::ReadByte(u32 address) const
@@ -552,6 +589,53 @@ bool ARMCore::TryEnterThumbMemoryPoll(u16 branch_instruction)
m_memory_poll_address = address;
m_waiting_for_memory_poll = true;
++m_memory_poll_entry_count;
return true;
}
bool ARMCore::TryEnterARMSliceStablePoll(u32 branch_instruction)
{
// IOS's early timer delay has this ARM shape (the middle literal load initializes the base only
// on entry): LDR value,[base]; B compare; literal/base setup; CMP value,target; Bcc loop. The
// timer cannot advance again until the bus receives AdvanceCycles at the slice boundary, making
// every remaining iteration in this slice observationally redundant.
const u32 condition = branch_instruction >> 28;
if ((branch_instruction & 0x0e000000) != 0x0a000000 || condition >= 0xe ||
!m_pc_written || HasUnmaskedInterrupt())
{
return false;
}
const u32 loop_address = m_registers[15];
if (loop_address + 16 != m_instruction_address)
return false;
const u32 load = FetchARMInstruction(loop_address);
const u32 skip = FetchARMInstruction(loop_address + 4);
const u32 compare = FetchARMInstruction(loop_address + 12);
if ((load & 0xfff00000) != 0xe5900000 || (skip & 0xff000000) != 0xea000000 ||
(compare & 0xfff00ff0) != 0xe1500000)
{
return false;
}
const s32 skip_offset = SignExtend((skip & 0x00ffffff) << 2, 26);
if (loop_address + 12 != loop_address + 12 + static_cast<u32>(skip_offset))
return false;
const u32 load_destination = (load >> 12) & 0xf;
if (((compare >> 16) & 0xf) != load_destination)
return false;
const u32 base = ReadRegisterOperand((load >> 16) & 0xf);
const u32 poll_address = base + (load & 0xfff);
const u32 physical_address = TranslateVirtualAddress(poll_address);
if (!m_bus.IsSliceStablePollAddress(physical_address, sizeof(u32)))
return false;
m_memory_poll_address = poll_address;
m_waiting_for_memory_poll = true;
++m_memory_poll_entry_count;
return true;
}
@@ -563,14 +647,24 @@ int ARMCore::StepInternal(bool advance_bus)
// entry.
if (m_waiting_for_memory_poll)
{
if (!HasUnmaskedInterrupt() && Read32(m_memory_poll_address) == 0)
const u32 physical_poll_address = TranslateVirtualAddress(m_memory_poll_address);
if (m_bus.IsSliceStablePollAddress(physical_poll_address, sizeof(u32)))
{
// Step() advances the device clock itself, so only RunCycles can collapse the rest of a
// scheduler slice. Resume normally here.
m_waiting_for_memory_poll = false;
}
else if (!HasUnmaskedInterrupt() && Read32(m_memory_poll_address) == 0)
{
if (advance_bus)
m_bus.AdvanceCycles(1);
return 1;
}
else
{
m_waiting_for_memory_poll = false;
}
}
if (m_waiting_for_interrupt && !m_irq_line && !m_fiq_line)
{
if (advance_bus)
@@ -623,6 +717,7 @@ int ARMCore::StepInternal(bool advance_bus)
{
ExecuteARM(instruction);
}
TryEnterARMSliceStablePoll(instruction);
}
if (!m_pc_written)
@@ -648,6 +743,16 @@ u64 ARMCore::RunCycles(u64 cycle_budget)
{
if (m_waiting_for_memory_poll)
{
const u32 physical_poll_address = TranslateVirtualAddress(m_memory_poll_address);
if (!HasUnmaskedInterrupt() &&
m_bus.IsSliceStablePollAddress(physical_poll_address, sizeof(u32)))
{
// This MMIO value cannot change before AdvanceCycles below. Consume the unused slice and
// retry the guest loop against the newly advanced device state next time.
m_waiting_for_memory_poll = false;
cycles = cycle_budget;
break;
}
if (HasUnmaskedInterrupt() || Read32(m_memory_poll_address) != 0)
{
m_waiting_for_memory_poll = false;
@@ -663,7 +768,32 @@ u64 ARMCore::RunCycles(u64 cycle_budget)
cycles = cycle_budget;
break;
}
#if defined(_M_X86_64)
if (m_jit_enabled && m_jit && !HasUnmaskedInterrupt())
{
const u32 executed = m_jit->Run(cycle_budget - cycles);
if (executed != 0)
{
cycles += executed;
SampleExecutionPC();
if (m_slice_fast_forward_requested)
{
m_slice_fast_forward_requested = false;
cycles = cycle_budget;
break;
}
continue;
}
}
#endif
cycles += static_cast<u64>(StepInternal(false));
SampleExecutionPC();
if (m_slice_fast_forward_requested)
{
m_slice_fast_forward_requested = false;
cycles = cycle_budget;
break;
}
}
// Starlet interrupt lines are sampled by the scheduler at slice boundaries.
// Updating timers and peripherals once here is therefore architecturally
@@ -674,6 +804,281 @@ u64 ARMCore::RunCycles(u64 cycle_budget)
return cycles;
}
void ARMCore::SampleExecutionPC()
{
if (m_executed_instructions < m_next_profile_instruction)
return;
const u32 key = m_registers[15] | ((m_cpsr & CPSR_T) != 0 ? 1U : 0U);
++m_pc_samples[key];
m_next_profile_instruction += PROFILE_SAMPLE_INTERVAL;
}
std::vector<ARMCore::HotPCSample> ARMCore::GetHotPCSamples(size_t maximum_count)
{
std::vector<std::pair<u32, u64>> sorted(m_pc_samples.begin(), m_pc_samples.end());
std::ranges::sort(sorted, {}, [](const auto& entry) { return entry.second; });
if (sorted.size() > maximum_count)
sorted.resize(maximum_count);
std::vector<HotPCSample> result;
result.reserve(sorted.size());
for (auto it = sorted.rbegin(); it != sorted.rend(); ++it)
{
const bool thumb = (it->first & 1) != 0;
const u32 address = it->first & ~1U;
result.push_back(
{.address = address,
.instruction = thumb ? FetchThumbInstruction(address) : FetchARMInstruction(address),
.samples = it->second,
.thumb = thumb});
}
return result;
}
u64 ARMCore::GetJitExecutedInstructions() const
{
#if defined(_M_X86_64)
return m_jit ? m_jit->GetExecutedInstructions() : 0;
#else
return 0;
#endif
}
u64 ARMCore::GetJitNativeExecutedInstructions() const
{
#if defined(_M_X86_64)
// Kept out of line so changes to the x64 JIT's diagnostic layout rebuild this translation unit.
return m_jit ? m_jit->GetNativeExecutedInstructions() : 0;
#else
return 0;
#endif
}
size_t ARMCore::GetJitCompiledBlockCount() const
{
#if defined(_M_X86_64)
return m_jit ? m_jit->GetCompiledBlockCount() : 0;
#else
return 0;
#endif
}
u64 ARMCore::GetJitBlockExecutionCount() const
{
#if defined(_M_X86_64)
return m_jit ? m_jit->GetBlockExecutionCount() : 0;
#else
return 0;
#endif
}
u64 ARMCore::GetJitAddressTranslationCount() const
{
#if defined(_M_X86_64)
return m_jit ? m_jit->GetAddressTranslationCount() : 0;
#else
return 0;
#endif
}
u64 ARMCore::GetJitSlowReadCount() const
{
#if defined(_M_X86_64)
return m_jit ? m_jit->GetSlowReadCount() : 0;
#else
return 0;
#endif
}
u64 ARMCore::GetJitSlowWriteCount() const
{
#if defined(_M_X86_64)
return m_jit ? m_jit->GetSlowWriteCount() : 0;
#else
return 0;
#endif
}
u64 ARMCore::GetJitSlowRAMAccessCount() const
{
#if defined(_M_X86_64)
return m_jit ? m_jit->GetSlowRAMAccessCount() : 0;
#else
return 0;
#endif
}
u64 ARMCore::GetJitSlowSRAMAccessCount() const
{
#if defined(_M_X86_64)
return m_jit ? m_jit->GetSlowSRAMAccessCount() : 0;
#else
return 0;
#endif
}
u64 ARMCore::GetJitSlowMMIOAccessCount() const
{
#if defined(_M_X86_64)
return m_jit ? m_jit->GetSlowMMIOAccessCount() : 0;
#else
return 0;
#endif
}
u64 ARMCore::GetJitSlowOtherAccessCount() const
{
#if defined(_M_X86_64)
return m_jit ? m_jit->GetSlowOtherAccessCount() : 0;
#else
return 0;
#endif
}
u64 ARMCore::GetJitSlowSRAMLowAccessCount() const
{
#if defined(_M_X86_64)
return m_jit ? m_jit->GetSlowSRAMLowAccessCount() : 0;
#else
return 0;
#endif
}
u64 ARMCore::GetJitSlowSRAMHighAccessCount() const
{
#if defined(_M_X86_64)
return m_jit ? m_jit->GetSlowSRAMHighAccessCount() : 0;
#else
return 0;
#endif
}
u64 ARMCore::GetJitSlowSRAMReadAccessCount() const
{
#if defined(_M_X86_64)
return m_jit ? m_jit->GetSlowSRAMReadAccessCount() : 0;
#else
return 0;
#endif
}
u64 ARMCore::GetJitSlowSRAMWriteAccessCount() const
{
#if defined(_M_X86_64)
return m_jit ? m_jit->GetSlowSRAMWriteAccessCount() : 0;
#else
return 0;
#endif
}
u64 ARMCore::GetJitSlowSRAMPageAccessCount(size_t page) const
{
#if defined(_M_X86_64)
return m_jit ? m_jit->GetSlowSRAMPageAccessCount(page) : 0;
#else
return 0;
#endif
}
u64 ARMCore::GetJitSlowSRAMPageReadAccessCount(size_t page) const
{
#if defined(_M_X86_64)
return m_jit ? m_jit->GetSlowSRAMPageReadAccessCount(page) : 0;
#else
return 0;
#endif
}
u64 ARMCore::GetJitSlowSRAMPageWriteAccessCount(size_t page) const
{
#if defined(_M_X86_64)
return m_jit ? m_jit->GetSlowSRAMPageWriteAccessCount(page) : 0;
#else
return 0;
#endif
}
void ARMCore::RecordJitFallback(u32 address, bool thumb)
{
++m_jit_fallback_instruction_count;
++m_jit_fallback_interval_count;
// Sampling keeps profiling effectively free even when hundreds of millions of instructions
// use an interpreter side exit.
if ((m_jit_fallback_instruction_count & 0xfff) == 0)
{
++m_jit_fallback_samples[address | (thumb ? 1U : 0U)];
++m_jit_fallback_interval_samples[address | (thumb ? 1U : 0U)];
}
}
std::vector<ARMCore::HotPCSample> ARMCore::GetHotJitFallbackSamples(size_t maximum_count)
{
std::vector<std::pair<u32, u64>> sorted(m_jit_fallback_samples.begin(),
m_jit_fallback_samples.end());
std::ranges::sort(sorted, {}, [](const auto& entry) { return entry.second; });
if (sorted.size() > maximum_count)
sorted.erase(sorted.begin(), sorted.end() - maximum_count);
std::vector<HotPCSample> result;
result.reserve(sorted.size());
for (auto it = sorted.rbegin(); it != sorted.rend(); ++it)
{
const bool thumb = (it->first & 1) != 0;
const u32 address = it->first & ~1U;
result.push_back(
{.address = address,
.instruction = thumb ? FetchThumbInstruction(address) : FetchARMInstruction(address),
.samples = it->second,
.thumb = thumb});
}
return result;
}
ARMCore::HotPCSample ARMCore::GetCurrentPCSample()
{
const bool thumb = (m_cpsr & CPSR_T) != 0;
return GetInstructionSample(m_registers[15], thumb);
}
ARMCore::HotPCSample ARMCore::GetInstructionSample(u32 address, bool thumb)
{
return {.address = address,
.instruction = thumb ? FetchThumbInstruction(address) : FetchARMInstruction(address),
.samples = 1,
.thumb = thumb};
}
u64 ARMCore::TakeJitFallbackIntervalCount()
{
return std::exchange(m_jit_fallback_interval_count, 0);
}
std::vector<ARMCore::HotPCSample>
ARMCore::TakeHotJitFallbackIntervalSamples(size_t maximum_count)
{
std::vector<std::pair<u32, u64>> sorted(m_jit_fallback_interval_samples.begin(),
m_jit_fallback_interval_samples.end());
m_jit_fallback_interval_samples.clear();
std::ranges::sort(sorted, {}, [](const auto& entry) { return entry.second; });
if (sorted.size() > maximum_count)
sorted.erase(sorted.begin(), sorted.end() - maximum_count);
std::vector<HotPCSample> result;
result.reserve(sorted.size());
for (auto it = sorted.rbegin(); it != sorted.rend(); ++it)
{
const bool thumb = (it->first & 1) != 0;
const u32 address = it->first & ~1U;
result.push_back(
{.address = address,
.instruction = thumb ? FetchThumbInstruction(address) : FetchARMInstruction(address),
.samples = it->second,
.thumb = thumb});
}
return result;
}
void ARMCore::DoState(PointerWrap& p)
{
p.DoArray(m_registers);
@@ -696,6 +1101,7 @@ void ARMCore::DoState(PointerWrap& p)
p.Do(m_waiting_for_interrupt);
p.Do(m_waiting_for_memory_poll);
p.Do(m_yield_requested);
p.Do(m_slice_fast_forward_requested);
p.Do(m_memory_poll_address);
p.Do(m_pc_written);
p.Do(m_instruction_address);
@@ -1928,8 +2334,12 @@ void ARMCore::WriteCP15(u32 opcode1, u32 crn, u32 crm, u32 opcode2, u32 value)
case 6:
m_cp15.fault_address = value;
break;
case 7: // Treat cache maintenance conservatively as a whole instruction-cache
// invalidation.
case 7:
// ARM926 separates I-cache and D-cache maintenance. In particular c7,c10,4 is Drain Write
// Buffer, an ordering operation used heavily by IOS; invalidating translated code for it is
// both architecturally wrong and catastrophically expensive. Only operations which include
// the instruction cache make previously modified instructions visible.
if (crm == 5 || crm == 7)
InvalidateInstructionCache();
break;
case 8: // TLB maintenance.
+75
View File
@@ -5,6 +5,9 @@
#include <array>
#include <cstddef>
#include <memory>
#include <unordered_map>
#include <vector>
#include "Common/CommonTypes.h"
@@ -12,6 +15,10 @@ class PointerWrap;
namespace IOS::LLE
{
#if defined(_M_X86_64)
class ARMJitX64;
#endif
// Byte-addressed bus used by the Starlet ARM core. Keeping endianness in the
// CPU is intentional: ARM926 can change its data endianness through CP15 while
// the underlying devices remain byte addressed.
@@ -45,6 +52,18 @@ public:
}
virtual void AdvanceCycles(u64 cycles) {}
virtual bool IsIdlePollAddress(u32 address, u32 size) const { return false; }
// True only for read-only MMIO counters whose value cannot change until AdvanceCycles runs.
// This lets a side-effect-free guest polling loop yield the remainder of the current scheduler
// slice without skipping any observable device transition.
virtual bool IsSliceStablePollAddress(u32 address, u32 size) const { return false; }
// 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; }
// 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; }
virtual const bool* GetFastmemBoot0Mapped() const { return nullptr; }
virtual const bool* GetFastmemSRAMSplitMode() const { return nullptr; }
};
class ARMCore final
@@ -71,6 +90,14 @@ public:
u32 process_id = 0;
};
struct HotPCSample
{
u32 address = 0;
u32 instruction = 0;
u64 samples = 0;
bool thumb = false;
};
static constexpr u32 CPSR_N = 1U << 31;
static constexpr u32 CPSR_Z = 1U << 30;
static constexpr u32 CPSR_C = 1U << 29;
@@ -82,6 +109,7 @@ public:
static constexpr u32 CPSR_MODE_MASK = 0x1f;
explicit ARMCore(ARMBus& bus);
~ARMCore();
void Reset(u32 reset_vector = 0);
int Step();
@@ -131,8 +159,39 @@ public:
u32 GetLastUndefinedInstruction() const { return m_last_undefined_instruction; }
u64 GetExecutedInstructions() const { return m_executed_instructions; }
void SetJitEnabled(bool enabled);
bool IsJitEnabled() const { return m_jit_enabled; }
u64 GetJitExecutedInstructions() const;
u64 GetJitNativeExecutedInstructions() const;
size_t GetJitCompiledBlockCount() const;
u64 GetJitBlockExecutionCount() const;
u64 GetJitAddressTranslationCount() const;
u64 GetJitSlowReadCount() const;
u64 GetJitSlowWriteCount() const;
u64 GetJitSlowRAMAccessCount() const;
u64 GetJitSlowSRAMAccessCount() const;
u64 GetJitSlowMMIOAccessCount() const;
u64 GetJitSlowOtherAccessCount() const;
u64 GetJitSlowSRAMLowAccessCount() const;
u64 GetJitSlowSRAMHighAccessCount() const;
u64 GetJitSlowSRAMReadAccessCount() const;
u64 GetJitSlowSRAMWriteAccessCount() const;
u64 GetJitSlowSRAMPageAccessCount(size_t page) const;
u64 GetJitSlowSRAMPageReadAccessCount(size_t page) const;
u64 GetJitSlowSRAMPageWriteAccessCount(size_t page) const;
u64 GetJitFallbackInstructionCount() const { return m_jit_fallback_instruction_count; }
u64 GetMemoryPollEntryCount() const { return m_memory_poll_entry_count; }
std::vector<HotPCSample> GetHotPCSamples(size_t maximum_count);
HotPCSample GetCurrentPCSample();
HotPCSample GetInstructionSample(u32 address, bool thumb);
std::vector<HotPCSample> GetHotJitFallbackSamples(size_t maximum_count);
u64 TakeJitFallbackIntervalCount();
std::vector<HotPCSample> TakeHotJitFallbackIntervalSamples(size_t maximum_count);
private:
#if defined(_M_X86_64)
friend class ARMJitX64;
#endif
struct ShiftResult
{
u32 value;
@@ -169,6 +228,9 @@ private:
int StepInternal(bool advance_bus);
bool HasUnmaskedInterrupt() const;
bool TryEnterThumbMemoryPoll(u16 branch_instruction);
bool TryEnterARMSliceStablePoll(u32 branch_instruction);
void RecordJitFallback(u32 address, bool thumb);
void SampleExecutionPC();
u16 Read16(u32 address) const;
u32 Read32(u32 address) const;
@@ -251,10 +313,23 @@ private:
bool m_waiting_for_interrupt = false;
bool m_waiting_for_memory_poll = false;
bool m_yield_requested = false;
bool m_slice_fast_forward_requested = false;
u32 m_memory_poll_address = 0;
bool m_pc_written = false;
u32 m_instruction_address = 0;
u32 m_last_undefined_instruction = 0;
u64 m_executed_instructions = 0;
u64 m_memory_poll_entry_count = 0;
u64 m_jit_fallback_instruction_count = 0;
std::unordered_map<u32, u64> m_jit_fallback_samples;
u64 m_jit_fallback_interval_count = 0;
std::unordered_map<u32, u64> m_jit_fallback_interval_samples;
static constexpr u64 PROFILE_SAMPLE_INTERVAL = 8192;
u64 m_next_profile_instruction = PROFILE_SAMPLE_INTERVAL;
std::unordered_map<u32, u64> m_pc_samples;
bool m_jit_enabled = false;
#if defined(_M_X86_64)
std::unique_ptr<ARMJitX64> m_jit;
#endif
};
} // namespace IOS::LLE
File diff suppressed because it is too large Load Diff
+212
View File
@@ -0,0 +1,212 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <cstddef>
#include <unordered_map>
#include <vector>
#include "Common/CommonTypes.h"
#include "Common/x64Emitter.h"
namespace IOS::LLE
{
class ARMCore;
// Starlet JIT for x86-64 hosts. Like Dolphin's Broadway JIT, translated blocks run below one
// persistent native dispatcher. Hot block lookup therefore stays in generated code instead of
// crossing C++ and the host ABI at every ARM basic block. Instructions which are not translated
// still use the exact interpreter implementation so MMIO and privileged ordering stay intact.
class ARMJitX64 final : public Gen::X64CodeBlock
{
public:
explicit ARMJitX64(ARMCore& core);
~ARMJitX64() override;
ARMJitX64(const ARMJitX64&) = delete;
ARMJitX64& operator=(const ARMJitX64&) = delete;
u32 Run(u64 cycle_budget);
void Clear();
void InvalidateTranslationContext();
u64 GetExecutedInstructions() const { return m_executed_instructions; }
u64 GetNativeExecutedInstructions() const { return m_native_executed_instructions; }
size_t GetCompiledBlockCount() const { return m_blocks.size(); }
u64 GetBlockExecutionCount() const { return m_block_execution_count; }
u64 GetAddressTranslationCount() const { return m_address_translation_count; }
u64 GetSlowReadCount() const { return m_slow_read_count; }
u64 GetSlowWriteCount() const { return m_slow_write_count; }
u64 GetSlowRAMAccessCount() const { return m_slow_ram_read_count + m_slow_ram_write_count; }
u64 GetSlowSRAMAccessCount() const
{
return m_slow_sram_read_count + m_slow_sram_write_count;
}
u64 GetSlowMMIOAccessCount() const
{
return m_slow_mmio_read_count + m_slow_mmio_write_count;
}
u64 GetSlowOtherAccessCount() const
{
return m_slow_other_read_count + m_slow_other_write_count;
}
u64 GetSlowSRAMLowAccessCount() const { return m_slow_sram_low_access_count; }
u64 GetSlowSRAMHighAccessCount() const { return m_slow_sram_high_access_count; }
u64 GetSlowSRAMReadAccessCount() const { return m_slow_sram_read_count; }
u64 GetSlowSRAMWriteAccessCount() const { return m_slow_sram_write_count; }
u64 GetSlowSRAMPageAccessCount(size_t page) const
{
return GetSlowSRAMPageReadAccessCount(page) + GetSlowSRAMPageWriteAccessCount(page);
}
u64 GetSlowSRAMPageReadAccessCount(size_t page) const
{
return page < m_slow_sram_page_read_access_count.size() ?
m_slow_sram_page_read_access_count[page] :
0;
}
u64 GetSlowSRAMPageWriteAccessCount(size_t page) const
{
return page < m_slow_sram_page_write_access_count.size() ?
m_slow_sram_page_write_access_count[page] :
0;
}
private:
using RunEntry = u64 (*)(u32);
enum class SRAMFastmemAccess
{
None,
Read,
Write,
};
struct Block
{
const u8* entry = nullptr;
u32 instruction_count = 0;
u32 native_instruction_count = 0;
bool runnable = false;
};
struct FastEntry
{
u32 key = 0xffffffff;
u32 padding = 0;
const u8* entry = nullptr;
};
void PoisonMemory() override;
void GenerateDispatcher();
static const u8* Dispatch(ARMJitX64* jit);
Block* GetOrCompileBlock(u32 address);
Block CompileBlock(u32 address, bool thumb);
bool EmitDirectARM(u32 instruction, u32 address, bool* terminal, bool* dispatcher_exit);
bool CanEmitARMDataProcessing(u32 instruction) const;
bool EmitARMMemory(u32 instruction, u32 address);
bool EmitARMHalfwordMemory(u32 instruction, u32 address);
bool EmitARMBlockTransfer(u32 instruction, u32 address, bool* terminal);
void EmitARMDataProcessing(u32 instruction);
bool EmitDirectThumb(u16 instruction, u32 address, bool* terminal);
bool EmitThumbMemory(u16 instruction, u32 address);
void EmitConditionResult(u32 condition);
void EmitExchangeBranch(Gen::OpArg target, bool link, u32 return_address);
void EmitThumbShiftImmediate(u16 instruction);
void EmitFastmemAddress(std::vector<Gen::FixupBranch>* slow_paths, u32 access_size,
u32 range_size = 0,
SRAMFastmemAccess sram_access = SRAMFastmemAccess::None,
bool arm_unaligned_word = false);
void EmitThumbMemorySlowPath(const std::vector<Gen::FixupBranch>& slow_paths, u16 instruction,
u32 address, Gen::FixupBranch direct_done);
void EmitARMMemorySlowPath(const std::vector<Gen::FixupBranch>& slow_paths, u32 instruction,
u32 address, Gen::FixupBranch direct_done);
void EmitThumbAddSub(u16 instruction);
void EmitThumbImmediate(u16 instruction);
void EmitThumbALU(u16 instruction);
void EmitLogicalFlags(Gen::X64Reg result);
void EmitLogicalFlagsWithCarry(Gen::X64Reg result, Gen::X64Reg carry);
void EmitArithmeticFlags(bool subtraction);
void EmitFallbackThumb(u16 instruction, u32 address);
void EmitFallbackARM(u32 instruction, u32 address);
void EmitBlockExit(u32 instruction_count, u32 native_instruction_count,
bool dispatcher_exit = false);
void LoadRegisterCache();
void FlushRegisterCache();
Gen::OpArg MRegister(u32 index) const;
Gen::OpArg MStoredRegister(u32 index) const;
Gen::OpArg MCPSR() const;
Gen::OpArg MInstructionAddress() const;
Gen::OpArg MPCWritten() const;
Gen::OpArg MWaitingForInterrupt() const;
Gen::OpArg MWaitingForMemoryPoll() const;
Gen::OpArg MYieldRequested() const;
Gen::OpArg MExecutedInstructions() const;
static void FallbackThumb(ARMJitX64* jit, u16 instruction, u32 address);
static void FallbackARM(ARMJitX64* jit, u32 instruction, u32 address);
static u32 ReadSPSR(ARMJitX64* jit);
static void WritePSR(ARMJitX64* jit, u32 spsr, u32 field_mask, u32 value);
static void ExecuteUserBankBlockTransfer(ARMJitX64* jit, u32 instruction, u32 address);
static void ExecuteThumbPushPop(ARMJitX64* jit, u16 instruction, u32 address);
static void ExceptionReturn(ARMJitX64* jit, u32 target);
static u32 ReadMemorySlow(ARMJitX64* jit, u32 physical_address, u32 access_size,
u32 byte_offset);
static void WriteMemorySlow(ARMJitX64* jit, u32 physical_address, u32 access_size, u32 value);
static u32 TranslateAddress(ARMJitX64* jit, u32 address);
static void EnterUndefinedARM(ARMJitX64* jit, u32 instruction, u32 address);
static void EnterSVCARM(ARMJitX64* jit, u32 address);
static void EnterSVCThumb(ARMJitX64* jit, u32 address);
static constexpr size_t CODE_SIZE = 32 * 1024 * 1024;
static constexpr u32 MAX_BLOCK_INSTRUCTIONS = 32;
static constexpr size_t FAST_ENTRY_COUNT = 1 << 16;
ARMCore& m_core;
std::unordered_map<u64, Block> m_blocks;
std::array<FastEntry, FAST_ENTRY_COUNT> m_fast_entries{};
u8* m_fastmem_base = nullptr;
u8* m_sram_base = nullptr;
const bool* m_boot0_mapped = nullptr;
const bool* m_sram_split_mode = nullptr;
RunEntry m_run_entry = nullptr;
const u8* m_dispatcher = nullptr;
const u8* m_dispatcher_exit = nullptr;
u8* m_block_code_begin = nullptr;
u64 m_executed_instructions = 0;
u64 m_native_executed_instructions = 0;
u64 m_block_execution_count = 0;
u64 m_address_translation_count = 0;
u64 m_slow_read_count = 0;
u64 m_slow_write_count = 0;
u64 m_slow_ram_read_count = 0;
u64 m_slow_ram_write_count = 0;
u64 m_slow_sram_read_count = 0;
u64 m_slow_sram_write_count = 0;
u64 m_slow_mmio_read_count = 0;
u64 m_slow_mmio_write_count = 0;
u64 m_slow_other_read_count = 0;
u64 m_slow_other_write_count = 0;
u64 m_slow_sram_low_access_count = 0;
u64 m_slow_sram_high_access_count = 0;
std::array<u64, 32> m_slow_sram_page_read_access_count{};
std::array<u64, 32> m_slow_sram_page_write_access_count{};
bool m_is_running = false;
bool m_clear_pending = false;
s32 m_registers_offset = 0;
s32 m_cpsr_offset = 0;
s32 m_instruction_address_offset = 0;
s32 m_pc_written_offset = 0;
s32 m_waiting_for_interrupt_offset = 0;
s32 m_waiting_for_memory_poll_offset = 0;
s32 m_yield_requested_offset = 0;
s32 m_executed_instructions_offset = 0;
s32 m_process_id_offset = 0;
s32 m_tlb_generation_offset = 0;
u32 m_compile_instruction_count = 0;
u32 m_compile_native_instruction_count = 0;
bool m_register_cache_active = false;
};
} // namespace IOS::LLE
+223 -2
View File
@@ -7,11 +7,17 @@
#include <memory>
#include "Common/ChunkFile.h"
#include "Common/GekkoDisassembler.h"
#include "Common/Logging/Log.h"
#include "Common/Timer.h"
#include "Core/Config/MainSettings.h"
#include "Core/CoreTiming.h"
#include "Core/HW/ProcessorInterface.h"
#include "Core/HW/WII_IPC.h"
#include "Core/IOS/Starlet/ARMCore.h"
#include "Core/IOS/Starlet/StarletMemory.h"
#include "Core/PowerPC/MMU.h"
#include "Core/PowerPC/PowerPC.h"
#include "Core/System.h"
namespace IOS::LLE
@@ -27,6 +33,8 @@ bool Starlet::Init(const std::string& dump_directory, std::string* error)
// Construct the complete physical bus (including its persistent controller
// reset state) before the CPU so the ARM core always observes native-width
// memory accesses and the same Hollywood device state.
// Allocate the concrete bus here; its device-state members are part of the
// allocation size and must stay in lockstep with StarletMemory's definition.
m_memory = std::make_unique<StarletMemory>(m_system);
if (!m_memory->Init(dump_directory, error))
{
@@ -36,11 +44,23 @@ bool Starlet::Init(const std::string& dump_directory, std::string* error)
// ARMCore owns generation-tagged software TLB and instruction caches, so
// construct it only after the complete physical bus exists.
m_core = std::make_unique<ARMCore>(*m_memory);
#if defined(_M_X86_64)
m_core->SetJitEnabled(Config::Get(Config::MAIN_WII_STARLET_JIT));
#else
m_core->SetJitEnabled(false);
#endif
m_core->Reset(StarletMemory::BOOT_ROM_BASE);
m_next_jit_diagnostic_instruction = 25'000'000;
m_next_realtime_diagnostic_ms = Common::Timer::NowMs() + 1000;
m_last_realtime_ppc_pc = 0;
m_realtime_ppc_pc_streak = 0;
m_ppc_context_logged = false;
m_jit_context_logged = false;
m_run_event = m_system.GetCoreTiming().RegisterEvent("StarletLLE", RunCallback);
m_initialized = true;
m_system.GetCoreTiming().ScheduleEvent(0, m_run_event);
INFO_LOG_FMT(IOS, "Starlet LLE started at PC {:#010x}", m_core->GetRegister(15));
INFO_LOG_FMT(IOS, "Starlet LLE started at PC {:#010x} ({})", m_core->GetRegister(15),
m_core->IsJitEnabled() ? "JIT" : "interpreter");
return true;
}
@@ -48,6 +68,30 @@ void Starlet::Shutdown()
{
if (m_run_event)
m_system.GetCoreTiming().RemoveEvent(m_run_event);
if (m_core && m_core->GetJitExecutedInstructions() != 0)
{
INFO_LOG_FMT(IOS, "Starlet JIT executed {} instructions ({} native, {:.1f}%)",
m_core->GetJitExecutedInstructions(), m_core->GetJitNativeExecutedInstructions(),
100.0 * m_core->GetJitNativeExecutedInstructions() /
m_core->GetJitExecutedInstructions());
for (const ARMCore::HotPCSample& sample : m_core->GetHotPCSamples(16))
{
INFO_LOG_FMT(IOS, "Starlet hot PC {:#010x} {} instruction={:#010x} samples={}",
sample.address, sample.thumb ? "Thumb" : "ARM", sample.instruction,
sample.samples);
}
INFO_LOG_FMT(IOS,
"Starlet JIT compiled {} blocks, used {} fallback instructions and entered "
"{} idle memory polls",
m_core->GetJitCompiledBlockCount(), m_core->GetJitFallbackInstructionCount(),
m_core->GetMemoryPollEntryCount());
for (const ARMCore::HotPCSample& sample : m_core->GetHotJitFallbackSamples(16))
{
INFO_LOG_FMT(IOS, "Starlet fallback PC {:#010x} {} instruction={:#010x} samples={}",
sample.address, sample.thumb ? "Thumb" : "ARM", sample.instruction,
sample.samples);
}
}
m_initialized = false;
m_run_event = nullptr;
m_core.reset();
@@ -139,13 +183,190 @@ void Starlet::RunSlice(s64 cycles_late)
// Active and sleeping states use different scheduling quanta so busy IOS code
// amortizes host callbacks while a sleeping core retains prompt
// external-interrupt wakeups.
const bool ipc_handshake_active = (m_system.GetWiiIPC().ReadStarletRegister(0x0c) & 0x0f) != 0;
const u32 ipc_arm_ctrl = m_system.GetWiiIPC().ReadStarletRegister(0x0c);
// In ARMCTRL, X1/X2 are the two inputs produced by Broadway (bits 2 and 1). Only those
// require the tighter interleave while Starlet is consuming a request. Y1/Y2 are Starlet's
// outputs; keeping the 256-cycle quantum until Broadway acknowledges them can leave the whole
// console permanently on the expensive IPC path even though Starlet has already yielded at the
// write which exposed the reply.
const bool ipc_handshake_active = (ipc_arm_ctrl & 0x06) != 0;
const u64 arm_cycles =
m_core->IsWaitingForExternalEvent() ?
ARM_IDLE_SLICE_CYCLES :
(ipc_handshake_active ? ARM_IPC_SLICE_CYCLES : ARM_ACTIVE_SLICE_CYCLES);
m_core->RunCycles(arm_cycles);
const u64 now_ms = Common::Timer::NowMs();
if (now_ms >= m_next_realtime_diagnostic_ms)
{
m_next_realtime_diagnostic_ms = now_ms + 1000;
const PowerPC::PowerPCState& ppc = m_system.GetPPCState();
const PowerPC::TryReadInstResult instruction = m_system.GetMMU().TryReadInstruction(ppc.pc);
const auto& pi = m_system.GetProcessorInterface();
INFO_LOG_FMT(IOS,
"Broadway live: PC={:#010x} NPC={:#010x} instruction-valid={} "
"instruction={:#010x} MSR={:#010x} "
"exceptions={:#010x} downcount={} r1={:#010x} LR={:#010x} CTR={:#010x} "
"pi-irq={:#010x}/{:#010x} "
"ppc-ctrl={:#04x} ppc-irq={:#010x}/{:#010x} arm-ctrl={:#04x} "
"arm-irq={:#010x}/{:#010x} Starlet-PC={:#010x} wait-int={} wait-mem={}",
ppc.pc, ppc.npc, instruction.valid, instruction.hex, ppc.msr.Hex, ppc.Exceptions,
ppc.downcount, ppc.gpr[1], LR(ppc), CTR(ppc),
pi.GetCause(), pi.GetMask(),
m_system.GetWiiIPC().ReadStarletRegister(0x04),
m_system.GetWiiIPC().ReadStarletRegister(0x30),
m_system.GetWiiIPC().ReadStarletRegister(0x34), ipc_arm_ctrl,
m_system.GetWiiIPC().ReadStarletRegister(0x38),
m_system.GetWiiIPC().ReadStarletRegister(0x3c), m_core->GetRegister(15),
m_core->IsWaitingForInterrupt(), m_core->IsWaitingForMemoryPoll());
if (ppc.pc == m_last_realtime_ppc_pc)
++m_realtime_ppc_pc_streak;
else
{
m_last_realtime_ppc_pc = ppc.pc;
m_realtime_ppc_pc_streak = 1;
m_ppc_context_logged = false;
}
if (!m_ppc_context_logged && m_realtime_ppc_pc_streak >= 3 && ppc.pc != 0xfffffffc)
{
INFO_LOG_FMT(IOS,
"Broadway repeated-PC context: PC={:#010x} LR={:#010x} CTR={:#010x} "
"CR={:#010x} XER={:#010x} PI={:#010x}/{:#010x}",
ppc.pc, LR(ppc), CTR(ppc), ppc.cr.Get(), ppc.spr[SPR_XER], pi.GetCause(),
pi.GetMask());
for (u32 base = 0; base < 32; base += 4)
{
INFO_LOG_FMT(IOS, "Broadway GPR r{}={:#010x} r{}={:#010x} r{}={:#010x} r{}={:#010x}",
base, ppc.gpr[base], base + 1, ppc.gpr[base + 1], base + 2,
ppc.gpr[base + 2], base + 3, ppc.gpr[base + 3]);
}
const u32 context_start = (ppc.pc - 0x40) & ~u32{3};
const u32 context_end = (ppc.pc + 0x80) & ~u32{3};
for (u32 address = context_start; address <= context_end; address += 4)
{
const PowerPC::TryReadInstResult op = m_system.GetMMU().TryReadInstruction(address);
if (op.valid)
{
INFO_LOG_FMT(IOS, "Broadway code {:#010x}: {:#010x} {}", address, op.hex,
Common::GekkoDisassembler::Disassemble(op.hex, address));
}
}
const u32 lr_start = (LR(ppc) - 0x20) & ~u32{3};
const u32 lr_end = (LR(ppc) + 0x20) & ~u32{3};
for (u32 address = lr_start; address <= lr_end; address += 4)
{
const PowerPC::TryReadInstResult op = m_system.GetMMU().TryReadInstruction(address);
if (op.valid)
{
INFO_LOG_FMT(IOS, "Broadway LR code {:#010x}: {:#010x} {}", address, op.hex,
Common::GekkoDisassembler::Disassemble(op.hex, address));
}
}
m_ppc_context_logged = true;
}
}
const u64 executed = m_core->GetExecutedInstructions();
if (m_core->IsJitEnabled() && executed >= m_next_jit_diagnostic_instruction)
{
const u64 jit_executed = m_core->GetJitExecutedInstructions();
const u64 jit_native = m_core->GetJitNativeExecutedInstructions();
const u64 block_executions = m_core->GetJitBlockExecutionCount();
const u64 interval_fallbacks = m_core->TakeJitFallbackIntervalCount();
INFO_LOG_FMT(IOS,
"Starlet live JIT: instructions={} native={:.1f}% fallbacks={} blocks={} "
"block-runs={} insns-per-block={:.1f} translations={} slow-reads={} "
"slow-writes={} slow-regions=ram:{}/sram:{}/mmio:{}/other:{} "
"interval-fallbacks={} idle-polls={} ipc-armctrl={:#04x} "
"PC={:#010x} "
"CPSR={:#010x} irqflag={:#010x} irqmask={:#010x} fiqmask={:#010x}",
jit_executed, jit_executed == 0 ? 0.0 : 100.0 * jit_native / jit_executed,
m_core->GetJitFallbackInstructionCount(), m_core->GetJitCompiledBlockCount(),
block_executions,
block_executions == 0 ? 0.0 : static_cast<double>(jit_executed) / block_executions,
m_core->GetJitAddressTranslationCount(), m_core->GetJitSlowReadCount(),
m_core->GetJitSlowWriteCount(),
m_core->GetJitSlowRAMAccessCount(), m_core->GetJitSlowSRAMAccessCount(),
m_core->GetJitSlowMMIOAccessCount(), m_core->GetJitSlowOtherAccessCount(),
interval_fallbacks, m_core->GetMemoryPollEntryCount(), ipc_arm_ctrl,
m_core->GetRegister(15), m_core->GetCPSR(),
m_system.GetWiiIPC().ReadStarletRegister(0x38),
m_system.GetWiiIPC().ReadStarletRegister(0x3c),
m_system.GetWiiIPC().ReadStarletRegister(0x40));
std::array<u32, 4> hot_sram_read_pages{};
std::array<u64, 4> hot_sram_read_page_counts{};
std::array<u32, 4> hot_sram_write_pages{};
std::array<u64, 4> hot_sram_write_page_counts{};
const auto rank_sram_page = [](u32 page, u64 count, auto& pages, auto& counts) {
for (size_t rank = 0; rank < counts.size(); ++rank)
{
if (count <= counts[rank])
continue;
for (size_t move = counts.size() - 1; move > rank; --move)
{
counts[move] = counts[move - 1];
pages[move] = pages[move - 1];
}
counts[rank] = count;
pages[rank] = page;
break;
}
};
for (u32 page = 0; page < 32; ++page)
{
rank_sram_page(page, m_core->GetJitSlowSRAMPageReadAccessCount(page), hot_sram_read_pages,
hot_sram_read_page_counts);
rank_sram_page(page, m_core->GetJitSlowSRAMPageWriteAccessCount(page),
hot_sram_write_pages, hot_sram_write_page_counts);
}
INFO_LOG_FMT(IOS,
"Starlet slow SRAM aliases=low:{}/high:{} reads:{}/writes:{} "
"hot-reads={:#04x}:{}, {:#04x}:{}, {:#04x}:{}, {:#04x}:{} "
"hot-writes={:#04x}:{}, {:#04x}:{}, {:#04x}:{}, {:#04x}:{}",
m_core->GetJitSlowSRAMLowAccessCount(),
m_core->GetJitSlowSRAMHighAccessCount(),
m_core->GetJitSlowSRAMReadAccessCount(),
m_core->GetJitSlowSRAMWriteAccessCount(), hot_sram_read_pages[0],
hot_sram_read_page_counts[0], hot_sram_read_pages[1],
hot_sram_read_page_counts[1], hot_sram_read_pages[2],
hot_sram_read_page_counts[2], hot_sram_read_pages[3],
hot_sram_read_page_counts[3], hot_sram_write_pages[0],
hot_sram_write_page_counts[0], hot_sram_write_pages[1],
hot_sram_write_page_counts[1], hot_sram_write_pages[2],
hot_sram_write_page_counts[2], hot_sram_write_pages[3],
hot_sram_write_page_counts[3]);
const ARMCore::HotPCSample current = m_core->GetCurrentPCSample();
INFO_LOG_FMT(IOS, "Starlet current PC {:#010x} {} instruction={:#010x}", current.address,
current.thumb ? "Thumb" : "ARM", current.instruction);
if (!m_jit_context_logged && current.address == 0xffff0758)
{
for (u32 address = 0xffff0738; address <= 0xffff0778; address += 4)
{
const ARMCore::HotPCSample sample = m_core->GetInstructionSample(address, false);
INFO_LOG_FMT(IOS, "Starlet stuck context {:#010x} instruction={:#010x}", sample.address,
sample.instruction);
}
for (u32 address = 0xffff49a0; address <= 0xffff49e0; address += 4)
{
const ARMCore::HotPCSample sample = m_core->GetInstructionSample(address, false);
INFO_LOG_FMT(IOS, "Starlet stuck target {:#010x} instruction={:#010x}", sample.address,
sample.instruction);
}
m_jit_context_logged = true;
}
for (const ARMCore::HotPCSample& sample :
m_core->TakeHotJitFallbackIntervalSamples(6))
{
INFO_LOG_FMT(IOS,
"Starlet interval fallback PC {:#010x} {} instruction={:#010x} samples={}",
sample.address, sample.thumb ? "Thumb" : "ARM", sample.instruction,
sample.samples);
}
m_next_jit_diagnostic_instruction = executed + 25'000'000;
}
const u64 broadway_cycles = arm_cycles * BROADWAY_CLOCK / ARM_CLOCK;
const s64 next = std::max<s64>(1, static_cast<s64>(broadway_cycles) - cycles_late);
m_system.GetCoreTiming().ScheduleEvent(next, m_run_event);
+6
View File
@@ -79,6 +79,12 @@ private:
std::unique_ptr<StarletMemory> m_memory;
std::unique_ptr<ARMCore> m_core;
CoreTiming::EventType* m_run_event = nullptr;
u64 m_next_jit_diagnostic_instruction = 25'000'000;
u64 m_next_realtime_diagnostic_ms = 0;
u32 m_last_realtime_ppc_pc = 0;
u32 m_realtime_ppc_pc_streak = 0;
bool m_ppc_context_logged = false;
bool m_jit_context_logged = false;
bool m_initialized = false;
};
} // namespace IOS::LLE
@@ -400,6 +400,51 @@ constexpr u32 GPIO_EEP_CLK = 0x800;
constexpr u32 GPIO_EEP_MOSI = 0x1000;
constexpr u32 GPIO_EEP_MISO = 0x2000;
constexpr u32 GPIO_VALID_MASK = 0x00ffffff;
constexpr u32 GPIO_DEBUG_MASK = 0x00ff0000;
constexpr std::string_view GetBootMiiDebugCodeMeaning(u8 code)
{
// BootMii's public loader and stubs deliberately publish these values on GPIO[23:16]. Some
// values are reused by more than one layer, so keep the descriptions broad and preserve the
// exact byte in the log.
switch (code)
{
case 0x03:
return "hardware setup begin";
case 0x12:
return "SD mount failed (following byte is the FAT error)";
case 0x42:
return "loader entry";
case 0x43:
return "loader stack ready";
case 0x44:
return "loader BSS cleared";
case 0x73:
return "hardware setup complete";
case 0xc0:
return "stub bypass/power-button path";
case 0xc1:
return "stub fallback payload loaded";
case 0xc8:
return "fall back to NAND boot2";
case 0xc9:
return "handoff to NAND boot2";
case 0xe3:
return "panic: invalid ELF header";
case 0xe4:
return "panic: ELF has no program headers";
case 0xf0:
return "BootMii loader main";
case 0xf1:
return "loader/stub initialized";
case 0xf2:
return "SD payload selected";
case 0xf3:
return "handoff to ARM payload";
default:
return "unclassified";
}
}
constexpr u32 SRNPROT_SRAM_SPLIT_MODE = 1U << 5;
constexpr u32 BOOT0_DISABLE = 1U << 12;
@@ -775,6 +820,37 @@ bool StarletMemory::IsIdlePollAddress(u32 address, u32 size) const
(IsSRAMWindowAddress(address) && IsSRAMWindowAddress(last_address));
}
bool StarletMemory::IsSliceStablePollAddress(u32 address, u32 size) const
{
// The Hollywood timer is derived solely from m_arm_cycles, which changes in AdvanceCycles at
// the end of an ARM scheduler slice. Re-reading it inside the same slice cannot observe a new
// value, so a side-effect-free timer loop may yield the unused instructions exactly.
return address == HW_TIMER && size == sizeof(u32);
}
u8* StarletMemory::GetFastmemBase() const
{
return m_system.GetMemory().GetPhysicalBase();
}
u8* StarletMemory::GetFastmemSRAMBase() const
{
// The JIT admits only measured SRAM pages and directions, and checks the live boot0 overlay and
// A/B split state before dereferencing this view. Register-list transfers and all nonprofiled
// writes remain on the exact bus path until each additional class has a full-chain regression.
return const_cast<u8*>(m_sram.data());
}
const bool* StarletMemory::GetFastmemBoot0Mapped() const
{
return &m_boot0_mapped;
}
const bool* StarletMemory::GetFastmemSRAMSplitMode() const
{
return &m_sram_split_mode;
}
bool StarletMemory::IsMemoryAddress(u32 address)
{
return address < Memory::MEM1_SIZE_RETAIL ||
@@ -4278,6 +4354,15 @@ void StarletMemory::HandleOTPCommand(u32 command)
void StarletMemory::HandleGPIOWrite(u32 value)
{
const u8 old_debug_code = static_cast<u8>((m_gpio_out & GPIO_DEBUG_MASK) >> 16);
const u8 debug_code = static_cast<u8>((value & GPIO_DEBUG_MASK) >> 16);
if (debug_code != old_debug_code)
{
const Starlet* const starlet = m_system.GetStarlet();
INFO_LOG_FMT(IOS, "Starlet GPIO debug port: code={:#04x} stage='{}' PC={:#010x}", debug_code,
GetBootMiiDebugCodeMeaning(debug_code), starlet ? starlet->GetPC() : 0);
}
const bool old_cs = (m_gpio_out & GPIO_EEP_CS) != 0;
const bool old_clock = (m_gpio_out & GPIO_EEP_CLK) != 0;
const bool chip_selected = (value & GPIO_EEP_CS) != 0;
@@ -61,6 +61,11 @@ public:
void Write32(u32 address, u32 value) override;
void AdvanceCycles(u64 cycles) override;
bool IsIdlePollAddress(u32 address, u32 size) const override;
bool IsSliceStablePollAddress(u32 address, u32 size) const override;
u8* GetFastmemBase() const override;
u8* GetFastmemSRAMBase() const override;
const bool* GetFastmemBoot0Mapped() const override;
const bool* GetFastmemSRAMSplitMode() const override;
u64 GetCycles() const { return m_arm_cycles; }
std::optional<u32> TryReadBroadwayResetInstruction(u32 address) const;
File diff suppressed because it is too large Load Diff
+98 -6
View File
@@ -63,7 +63,7 @@ Dolphin CoreTiming (Broadway clock domain, 729 MHz)
+-- WFI idle: 72,900 Broadway cycles --> 24,300 Starlet cycles
|
v
ARMv5TE interpreter
ARMv5TE interpreter or x64 JIT
+ software TLB/I-cache
|
+-------------------------------+------------------------------+
@@ -118,6 +118,13 @@ X2 and immediately submit the next request with X1 before Starlet is scheduled a
timer, NAND, OHCI, and Wiimote clocks once per 4,096-cycle scheduler slice instead of once per
interpreted instruction; external IRQ delivery remains bounded to about 16.9 microseconds and
WFI polling to 100 microseconds.
- An x86-64 Starlet JIT translates the observed ARM and Thumb integer, branch, interworking and
memory-transfer subset. Its inline generation-tagged TLB and direct fastmem paths cover ordinary
MEM1/MEM2 accesses plus a measured, direction-specific subset of single SRAM reads. Every SRAM
write, register-list transfer, TLB miss, MMIO access, protected boot0 overlay, invalid SRAM
aperture or unsupported instruction remains an architectural side exit: registers are flushed,
the exact interpreter/device operation runs, and the dispatcher re-samples IRQ/FIQ, IPC yield,
CP15 and translation state before another native block executes.
- Big-endian Starlet address space, 96 KiB of physical SRAM (64 KiB bank A plus 32 KiB bank B)
exposed through the hardware's unusual 128 KiB windows, plus shared MEM1/MEM2 access.
- Raw NAND reads, chip identification/status, Wii ECC generation, ECC-enabled page programming
@@ -135,7 +142,9 @@ X2 and immediately submit the next request with X1 before Starlet is scheduled a
read, write-enable/disable, word write/erase, and whole-array write/erase commands in COW memory.
- Hollywood's Starlet GPIO bank, including enable/output/direction/input, ownership, straps,
interrupt level/mask, and write-one-to-clear interrupt flags. POWER and EJECT have their idle
levels and the EEPROM MISO pin remains connected to the existing serial model.
levels and the EEPROM MISO pin remains connected to the existing serial model. Writes to the
public BootMii diagnostic byte on GPIO bits 23:16 are logged as stage codes with the Starlet PC;
this observes the firmware's own progress/panic channel without modifying its control flow.
- Immediate AHM memory-flush acknowledgement, the indirect DDR/SEQ/BIST register banks used by
boot1 training, and the hardware-controlled boot0 ROM overlay/SRAM-bank swap.
- PPC/ARM IPC mailboxes, Starlet-side access to both control registers, and Broadway
@@ -305,6 +314,18 @@ An isolated boot probe using the local, mutually matching dumps has executed thi
`IPC_BOOT2_RUN(1, 2)` immediately. Cold-boot BootMii and HBC -> IOS254 -> MINI -> BootMii both
remain in the original interactive menu, and keyboard-backed GameCube navigation was validated
without modifying `armboot.bin`, `ppcboot.elf`, or `bootmii.ini`.
22. Directional JIT profiling separated slow SRAM reads from writes and counted them per 4 KiB
aperture page. During the IOS-to-Menu phase, about 71% of slow SRAM accesses were writes;
pages `0x00` and `0x19` dominated those writes, while page `0x1e` dominated the remaining
reads. Single reads from the previously validated `0x00`/`0x12`/`0x14`/`0x19` pages and the
newly measured `0x1e` page now use guarded split-bank fastmem. Direct writes to `0x00`/`0x19`
passed synthetic bounds and canary tests but reproducibly stopped BootMii at its public
`hardware setup begin` GPIO marker, so every SRAM write was restored to the exact bus path.
The resulting build passed all 61 targeted Starlet/JIT tests and a fresh cold-boot regression:
BootMii completed hardware setup, the Wii selection re-entered NAND boot2, original IOS released
Broadway 54.1 seconds later, and the populated System Menu rendered without a crash or firmware
patch. The still-dominant exact writes are a measured coherency problem, not permission to skip
their bus semantics.
The probe never prints ROM, NAND, key, or firmware instruction bytes. The committed unit suite
covers ARM-to-Thumb loads into PC, high Starlet exception vectors, privileged `LDM ... ^` user-bank
@@ -354,10 +375,12 @@ The next firmware stages need substantially more hardware fidelity:
enumeration, Bluetooth HCI/ACL, and one paired remote's L2CAP/HID input path are implemented.
- Remaining IOS reload/reset edge cases, device timing, and scheduler accuracy needed by
timing-sensitive original exploits.
- Performance: the Menu's original IOS idle path now runs at full speed through safe RAM/SRAM poll
fast-forwarding. ARM-heavy boot and transient driver workloads still use the interpreter and can
take longer than real hardware; a block cache or ARM JIT remains the next performance frontier
for sustained workloads that do not enter the scheduler's idle loop.
- Performance: the Menu's original IOS idle path runs at full speed through safe RAM/SRAM poll
fast-forwarding, and the x64 Starlet JIT now executes most ordinary ARM/Thumb instructions
natively. Boot and transient driver workloads still cross many architectural side exits for
MMIO, CP15 operations and the not-yet-native instruction subset. Those exits must be reduced by
proven instruction translations or register-specific MMIO stubs, never by treating every device
access as a resumable C helper.
Until those items are implemented, this is an end-to-end experimental LLE implementation rather
than a drop-in replacement for Dolphin's mature IOS HLE mode.
@@ -389,11 +412,80 @@ than a drop-in replacement for Dolphin's mature IOS HLE mode.
- [ARM926EJ-S Technical Reference Manual](https://developer.arm.com/documentation/ddi0198/latest/)
for ARMv5TE, CP15, banked-register, exception, and interworking semantics.
## JIT correctness and BootMii performance model
BootMii is two distinct Starlet programs before its Broadway UI appears. The boot2-style loader
performs Hollywood setup, mounts the external SD controller and loads `/bootmii/armboot.bin`.
MINI then configures the ARM926 MMU/caches and IRQs, initializes NAND, IPC and SDHC, loads
`/bootmii/ppcboot.elf`, writes the Broadway EXI reset stub, releases the reset lines and finally
sleeps in its IPC loop. MINI has no Wi-Fi stack. Consequently, Wi-Fi SDIO traffic observed later
belongs to Nintendo IOS/System Menu startup and is not a valid optimization target for a black
screen inside BootMii.
The public loader also gives deterministic stage telemetry. `debug_output()` writes one byte to
GPIO bits 23:16: `42/43/44` cover entry, stack and BSS setup; `F0/F1` enter and finish loader setup;
`F2/F3` select and hand off the SD payload; `C8/C9` fall back and hand off to NAND boot2; `E3/E4`
are ELF-loader panics. A panic then alternates its error byte with zero around repeated 500 ms
timer delays. Therefore a trace fixed at the loader's `udelay` loop for millions of scheduler
slices is a panic, not evidence that SDHC or the GUI merely needs more time.
The JIT follows the same invariant used by Dolphin's Broadway JIT and QEMU TCG: a slow access can
resume inside a translated block only when all guest architectural state at that instruction is
recoverable and the helper cannot change scheduling, interrupt, translation or code-cache state.
Dolphin flushes registers before its safe slow loads/stores and checks memory exceptions; QEMU
records a host-PC to guest-PC/state map so faults restore the precise instruction boundary. The
Starlet JIT does not yet have that per-instruction recovery metadata. Generic MMIO therefore exits
to the dispatcher. Future performance work can specialize proven side-effect-free registers
(for example read-only status or timer reads), while complex writes, IRQ acknowledgements, IPC and
reset transitions must remain exits.
The ARM926 manual is also explicit about the boundaries relevant here: wait-for-interrupt drains
the write buffer and sleeps until IRQ/FIQ/debug; disabling and re-enabling the MMU preserves TLB
contents; D-cache clean, write-buffer drain, I-cache invalidation and TLB invalidation are separate
CP15 operations. Collapsing all `c7` maintenance into a code-cache flush is both inaccurate and
slow, while ignoring the instruction-cache operations breaks self-modifying boot code.
### IOS syscall execution and evaluated acceleration strategies
IOS has two superficially similar software-exception ABIs. Normal kernel calls are deliberately
undefined ARM words of the form `0xE6000010 | (syscall_number << 5)`. The Undefined vector saves
the complete thread context, extracts bits 12:5, switches to System mode and dispatches through the
IOS-version-specific syscall table. ARM/Thumb `SVC 0xAB` is a separate RealView semihosting ABI;
production IOS retains essentially only debug-string output (`r0 = 4`, string in `r1`). Replacing
either path with host-side IOS HLE would skip the original scheduler, message queues, permission
checks and exploit-relevant kernel behavior, so LLE keeps the guest handlers. The JIT only enters
the architecturally correct Undefined/Supervisor exception directly, avoiding a redundant generic
instruction-decoder fallback.
The acceleration options were evaluated as follows:
| Method | Expected value here | Decision |
|---|---|---|
| Per-instruction interpreter | Reference accuracy, very low throughput | Keep as the exact fallback/oracle only |
| IOS syscall HLE | Very fast for ordinary titles | Reject for original IOS and exploit compatibility |
| QEMU TCG/Unicorn ARM core | Mature ARM system translation | Valuable reference, but integrating Dolphin memory, Hollywood MMIO, CoreTiming, precise cache invalidation and dual-CPU IPC would duplicate most of the current machine model |
| LLVM/whole-function recompilation | Strong global optimization | Excessive compile latency and difficult precise MMIO/exception recovery during boot and self-modifying code |
| Custom basic-block JIT | Shares Dolphin memory and timing directly | Selected; continue with measured native coverage and exact side exits |
| Native ARM predication | Removes common IOS conditional-ALU fallbacks | Implemented for every already-validated data-processing form |
| Native Undefined/SVC entry | Removes decoder fallback at each software exception | Implemented without bypassing the guest kernel handler |
| Direct block chaining | Removes dispatcher lookup on hot edges | Next high-value CPU optimization, provided TLB/I-cache invalidation unlinks every affected edge |
| Wider register allocation / traces | Reduces repeated guest-register loads and stores | Medium-term; add only after branch/memory profiles identify stable hot traces |
| Register-specific MMIO fast paths | Can remove very hot safe status reads | Only after sampled effective-address traces; generic MMIO continuation is forbidden |
This ordering follows the same split documented by
[QEMU TCG](https://github.com/qemu/qemu/blob/master/docs/devel/tcg.rst): RAM/ROM accesses use cached
host offsets, MMIO calls device code, and direct block chains must be removable when translated
pages change. The current microbenchmark reaches roughly two billion simple translated ARM
instructions per host second, while complete IOS traces are orders of magnitude slower. The raw
x86 emitter is therefore not the limiting component; remaining work is native coverage, side-exit
frequency, guest scheduling/idle detection and device-access cost.
## Code map
| Area | Files |
|---|---|
| ARM CPU | `Core/IOS/Starlet/ARMCore.{h,cpp}` |
| x86-64 ARM/Thumb JIT | `Core/IOS/Starlet/ARMJitX64.{h,cpp}` |
| Starlet scheduler/lifetime | `Core/IOS/Starlet/Starlet.{h,cpp}` |
| Address space and devices | `Core/IOS/Starlet/StarletMemory.{h,cpp}` |
| Bluetooth pairing and Wii Remote HID | `Core/IOS/USB/Bluetooth/{BTBase,WiimoteDevice}.{h,cpp}` |