IOS: add x64 Starlet JIT with guarded fastmem
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,13 +647,23 @@ 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;
|
||||
}
|
||||
m_waiting_for_memory_poll = false;
|
||||
else
|
||||
{
|
||||
m_waiting_for_memory_poll = false;
|
||||
}
|
||||
}
|
||||
if (m_waiting_for_interrupt && !m_irq_line && !m_fiq_line)
|
||||
{
|
||||
@@ -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,9 +2334,13 @@ 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.
|
||||
InvalidateInstructionCache();
|
||||
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.
|
||||
InvalidateTLB();
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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
+99
-7
@@ -63,8 +63,8 @@ Dolphin CoreTiming (Broadway clock domain, 729 MHz)
|
||||
+-- WFI idle: 72,900 Broadway cycles --> 24,300 Starlet cycles
|
||||
|
|
||||
v
|
||||
ARMv5TE interpreter
|
||||
+ software TLB/I-cache
|
||||
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}` |
|
||||
|
||||
Reference in New Issue
Block a user