#include "psprecomp/runtime.hpp" #include "psprecomp/common.hpp" #include #include #include #include #include #include #include #include #include #include namespace psprecomp { // See Runtime::invoke_chained_direct(). Diagnostics deliberately make this // sticky: once instrumentation has been requested, correctness matters more // than returning to the production fast path later in the process. bool g_runtime_chain_observers_active = false; std::uint64_t g_runtime_starvation_interval_fast = 0u; std::uint64_t g_runtime_thread_switch_generation_fast = 0u; namespace { using RuntimePostImportHook = void (*)(Runtime &, AllegrexContext &); RuntimePostImportHook g_post_import_hook = nullptr; std::int32_t g_runtime_thread_uid = -1; std::array g_runtime_thread_name{}; std::uint32_t g_runtime_dispatch_pc = 0u; RuntimeHeartbeatHook g_heartbeat_hook = nullptr; std::uint64_t g_heartbeat_interval = 0u; RuntimeStarvationHook g_starvation_hook = nullptr; std::uint64_t g_starvation_interval = 0u; RuntimePreDispatchHook g_pre_dispatch_hook = nullptr; RuntimePostDispatchHook g_post_dispatch_hook = nullptr; RuntimePreChainedCallHook g_pre_chained_call_hook = nullptr; RuntimePostChainedCallHook g_post_chained_call_hook = nullptr; } // Execution counter for a handful of guest addresses, armed by // PSPRECOMP_COUNT_PC as a comma-separated list. // // Answering "is this routine ever reached?" needed an instrument that costs // nothing: the existing chain tracer pushes a frame on every chained call and // perturbed the run enough that it died before reaching the window under // investigation. This is a linear scan over at most eight addresses, and it // covers both entry paths -- the outer dispatch loop and invoke_chained_call -- // because a routine reached only through chaining never appears in the outer // dispatch profile. namespace { constexpr std::size_t kMaxCountedPcs = 8u; std::array g_counted_pcs{}; std::array g_counted_pc_hits{}; std::size_t g_counted_pc_size = 0u; bool g_counted_pcs_loaded = false; void load_counted_pcs() { if (g_counted_pcs_loaded) return; g_counted_pcs_loaded = true; const char *text = std::getenv("PSPRECOMP_COUNT_PC"); if (text == nullptr || *text == '\0') return; const std::string list(text); std::size_t begin = 0u; while (begin < list.size() && g_counted_pc_size < kMaxCountedPcs) { const std::size_t comma = list.find(',', begin); const std::string item = list.substr(begin, comma == std::string::npos ? std::string::npos : comma - begin); char *end = nullptr; const unsigned long value = std::strtoul(item.c_str(), &end, 0); if (end != item.c_str() && *end == '\0') g_counted_pcs[g_counted_pc_size++] = static_cast(value); if (comma == std::string::npos) break; begin = comma + 1u; } if (g_counted_pc_size != 0u) g_runtime_chain_observers_active = true; } inline void count_pc(std::uint32_t pc) noexcept { for (std::size_t index = 0u; index < g_counted_pc_size; ++index) if (g_counted_pcs[index] == pc) { ++g_counted_pc_hits[index]; return; } } } void report_counted_pcs() { for (std::size_t index = 0u; index < g_counted_pc_size; ++index) std::cerr << "[count-pc] pc=" << hex32(g_counted_pcs[index]) << " hits=" << g_counted_pc_hits[index] << "\n"; } void set_runtime_heartbeat_hook(RuntimeHeartbeatHook hook, std::uint64_t interval) noexcept { g_heartbeat_hook = interval != 0u ? hook : nullptr; g_heartbeat_interval = interval; } void set_runtime_starvation_hook(RuntimeStarvationHook hook, std::uint64_t interval) noexcept { g_starvation_hook = interval != 0u ? hook : nullptr; g_starvation_interval = interval; g_runtime_starvation_interval_fast = g_starvation_hook != nullptr ? interval : 0u; } void set_runtime_pre_dispatch_hook(RuntimePreDispatchHook hook) noexcept { g_pre_dispatch_hook = hook; } void set_runtime_post_dispatch_hook(RuntimePostDispatchHook hook) noexcept { g_post_dispatch_hook = hook; } void set_runtime_pre_chained_call_hook(RuntimePreChainedCallHook hook) noexcept { g_pre_chained_call_hook = hook; if (hook != nullptr) g_runtime_chain_observers_active = true; } void set_runtime_post_chained_call_hook(RuntimePostChainedCallHook hook) noexcept { g_post_chained_call_hook = hook; if (hook != nullptr) g_runtime_chain_observers_active = true; } void set_runtime_post_import_hook(RuntimePostImportHook hook) noexcept { g_post_import_hook = hook; } void set_runtime_thread_identity(std::int32_t uid, const std::string &name) noexcept { if (uid != g_runtime_thread_uid) ++g_runtime_thread_switch_generation_fast; g_runtime_thread_uid = uid; g_runtime_thread_name.fill('\0'); const std::size_t count = std::min(name.size(), g_runtime_thread_name.size() - 1u); std::memcpy(g_runtime_thread_name.data(), name.data(), count); } std::int32_t runtime_thread_uid() noexcept { return g_runtime_thread_uid; } const char *runtime_thread_name() noexcept { return g_runtime_thread_name.data(); } std::uint32_t runtime_dispatch_pc() noexcept { return g_runtime_dispatch_pc; } RuntimeExecutionContextToken capture_runtime_execution_context() noexcept { return RuntimeExecutionContextToken{g_runtime_thread_uid, g_runtime_thread_switch_generation_fast}; } bool runtime_execution_context_matches(RuntimeExecutionContextToken token) noexcept { return token.thread_uid == g_runtime_thread_uid && token.switch_generation == g_runtime_thread_switch_generation_fast; } std::uint64_t runtime_thread_switch_generation() noexcept { return g_runtime_thread_switch_generation_fast; } bool runtime_thread_switch_generation_matches(std::uint64_t generation) noexcept { return generation == g_runtime_thread_switch_generation_fast; } bool g_guest_hotspot_profile_enabled = false; std::uint32_t g_guest_hotspot_sample_mask = 0xFFu; std::uint64_t g_guest_hotspot_unit_calls[kUnitProfileCapacity]{}; namespace { struct GuestHotspotPcSlot { std::uint32_t pc{}; std::uint16_t unit{}; std::uint16_t occupied{}; std::uint64_t samples{}; std::uint64_t inclusive_sample_ns{}; }; std::uint64_t g_guest_hotspot_unit_samples[kUnitProfileCapacity]{}; std::uint64_t g_guest_hotspot_unit_ns[kUnitProfileCapacity]{}; std::array g_guest_hotspot_pc_slots{}; std::uint64_t g_guest_hotspot_total_samples = 0u; std::size_t guest_hotspot_hash(std::uint32_t pc) noexcept { std::uint32_t x = pc >> 2u; x ^= x >> 16u; x *= 0x7FEB352Du; x ^= x >> 15u; return static_cast(x) & (kGuestHotspotPcCapacity - 1u); } } void set_guest_hotspot_profile(bool enabled, std::uint32_t sample_shift) noexcept { if (sample_shift > 16u) sample_shift = 16u; g_guest_hotspot_sample_mask = sample_shift == 0u ? 0u : ((1u << sample_shift) - 1u); g_guest_hotspot_profile_enabled = enabled; } std::uint64_t guest_hotspot_clock_ns() noexcept { return static_cast(std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch()).count()); } void guest_hotspot_record_sample(std::uint32_t unit, std::uint32_t pc, std::uint64_t elapsed_ns) noexcept { if (!g_guest_hotspot_profile_enabled || unit >= kUnitProfileCapacity) return; ++g_guest_hotspot_unit_samples[unit]; g_guest_hotspot_unit_ns[unit] += elapsed_ns; ++g_guest_hotspot_total_samples; if (pc == 0u) return; std::size_t slot = guest_hotspot_hash(pc); for (std::size_t probe = 0u; probe < 12u; ++probe) { GuestHotspotPcSlot &entry = g_guest_hotspot_pc_slots[(slot + probe) & (kGuestHotspotPcCapacity - 1u)]; if (entry.occupied == 0u) { entry.pc = pc; entry.unit = static_cast(unit); entry.occupied = 1u; } if (entry.pc == pc && entry.unit == unit) { ++entry.samples; entry.inclusive_sample_ns += elapsed_ns; return; } } // The table is intentionally fixed and allocation-free. Extremely unlikely // collision overflow drops only this sampled PC; unit totals remain exact. } GuestHotspotSnapshot consume_guest_hotspot_profile(std::size_t unit_limit, std::size_t pc_limit) { GuestHotspotSnapshot out; out.sample_stride = g_guest_hotspot_sample_mask + 1u; out.total_samples = g_guest_hotspot_total_samples; for (std::size_t unit = 0u; unit < kUnitProfileCapacity; ++unit) { const std::uint64_t calls = g_guest_hotspot_unit_calls[unit]; const std::uint64_t samples = g_guest_hotspot_unit_samples[unit]; const std::uint64_t ns = g_guest_hotspot_unit_ns[unit]; out.total_unit_calls += calls; if (calls != 0u || samples != 0u) out.units.push_back(GuestHotspotUnitEntry{static_cast(unit), calls, samples, ns}); g_guest_hotspot_unit_calls[unit] = 0u; g_guest_hotspot_unit_samples[unit] = 0u; g_guest_hotspot_unit_ns[unit] = 0u; } for (GuestHotspotPcSlot &slot : g_guest_hotspot_pc_slots) { if (slot.occupied != 0u && slot.samples != 0u) out.pcs.push_back(GuestHotspotPcEntry{slot.unit, slot.pc, slot.samples, slot.inclusive_sample_ns}); slot = {}; } g_guest_hotspot_total_samples = 0u; const auto estimated_cost = [stride = static_cast(out.sample_stride)](const auto &e) { return e.samples == 0u ? 0u : e.inclusive_sample_ns * stride; }; std::sort(out.units.begin(), out.units.end(), [&](const auto &a, const auto &b) { const auto ac = estimated_cost(a), bc = estimated_cost(b); if (ac != bc) return ac > bc; return a.calls > b.calls; }); std::sort(out.pcs.begin(), out.pcs.end(), [&](const auto &a, const auto &b) { const auto ac = estimated_cost(a), bc = estimated_cost(b); if (ac != bc) return ac > bc; return a.samples > b.samples; }); if (out.units.size() > unit_limit) out.units.resize(unit_limit); if (out.pcs.size() > pc_limit) out.pcs.resize(pc_limit); return out; } Runtime::Runtime(std::uint32_t ram_size) : memory_(ram_size) { // Most commercial PSP titles use a few hundred import stubs. Seed a small // binding table so first use of a late-numbered import does not reallocate // in the middle of guest execution; larger profiles can still grow it. import_bindings_.resize(256u, nullptr); hle_histogram_enabled_ = std::getenv("PSPRECOMP_HLE_HISTOGRAM") != nullptr; g_unit_profile_enabled = std::getenv("PSPRECOMP_UNIT_PROFILE") != nullptr; #if defined(PSPRECOMP_AOT_PRODUCTION_FASTPATHS) track_dispatch_counters_ = false; #else track_dispatch_counters_ = std::getenv("PSPRECOMP_REPORT_DISPATCH_COUNT") != nullptr || std::getenv("PSPRECOMP_PROFILE_DISPATCH") != nullptr || std::getenv("PSPRECOMP_COUNT_PC") != nullptr; #endif // PSPRECOMP_NO_CHAIN disables cross-unit chaining outright; // PSPRECOMP_CHAIN_DEPTH tunes how deep it may nest without a rebuild. // // Do not raise this without measuring native stack use per chained frame. // 1024 was tried on the theory that exceeding the limit is a cliff -- the // chain refuses, the native stack unwinds to Runtime::run, and the target is // re-entered through the guest-PC table -- and that bench.bat already passed // 1024 by hand. It crashed the game during boot. A benchmark does not reach // the call depths gameplay does, and generated frames are not small: each // one carries the unit's locals, and inlining the memory fast path at every // call site (/Ob1) made them larger still. 48 is the value that runs. chain_depth_limit_ = 48u; if (const char *depth = std::getenv("PSPRECOMP_CHAIN_DEPTH")) { char *end = nullptr; const unsigned long value = std::strtoul(depth, &end, 0); if (end != depth && *end == '\0' && value <= 4096u) chain_depth_limit_ = static_cast(value); } if (std::getenv("PSPRECOMP_NO_CHAIN") != nullptr) chain_depth_limit_ = 0u; } namespace { // The six registers the dispatch error messages print are rarely enough to tell // which operand produced a bad guest address. Note that the hot-register cache // (r2/r4-r7/r29/r31) is only materialized back into AllegrexContext by the // generated outer wrapper on a normal return -- an exception unwinds past that // flush, so exactly those six read stale here. Every other register, including // the callee-saved ones, is current. void append_gpr_dump(std::ostringstream &message, const AllegrexContext &ctx) { static const char *const kGprNames[32] = { "zero", "at", "v0", "v1", "a0", "a1", "a2", "a3", "t0", "t1", "t2", "t3", "t4", "t5", "t6", "t7", "s0", "s1", "s2", "s3", "s4", "s5", "s6", "s7", "t8", "t9", "k0", "k1", "gp", "sp", "fp", "ra"}; message << "\n gpr:"; for (std::size_t index = 0u; index < 32u; ++index) { if (index % 8u == 0u) message << "\n "; message << ' ' << kGprNames[index] << '=' << hex32(ctx.gpr[index]); } message << "\n (r2/r4-r7/r29/r31 may be stale: hot-register cache not flushed on unwind)"; } } bool Runtime::run_starvation_boundary(AllegrexContext &ctx) { const std::uint64_t interval = g_runtime_starvation_interval_fast; if (g_starvation_hook == nullptr || interval == 0u) return true; // Preserve excess work if a context switch deferred several boundaries. dispatches_since_import_ -= std::min(dispatches_since_import_, interval); const RuntimeExecutionContextToken before = capture_runtime_execution_context(); g_starvation_hook(*this, ctx); const bool same_context = runtime_execution_context_matches(before); if (!same_context) chain_context_invalidated_ = true; return same_context; } bool Runtime::account_dispatch_work(AllegrexContext &ctx, bool allow_preemption) { #if !defined(PSPRECOMP_AOT_PRODUCTION_FASTPATHS) if (track_dispatch_counters_) ++dispatch_work_count_; #endif const std::uint64_t interval = g_runtime_starvation_interval_fast; if (interval == 0u) return true; ++dispatches_since_import_; if (!allow_preemption || dispatches_since_import_ < interval) return true; return run_starvation_boundary(ctx); } bool Runtime::invoke_chained_call(AllegrexContext &ctx, GuestMemory::AotFastView *shared_aot_mem) { #if !defined(PSPRECOMP_AOT_PRODUCTION_FASTPATHS) count_pc(ctx.pc); #endif if (chain_depth_ >= chain_depth_limit_) return false; // Indirect jalr/vcall targets usually land in ordinary generated code too. // Prefer the tiny dense unit table. Units overlapped by a host/import // replacement are poisoned and fall back to exact PC lookup. Ownership is // still guarded on every native chain frame because a nested descendant // can cross the scheduler boundary even when this immediate unit is clean. // dynamic JR/JALR chains used to canonicalize the same target // twice: once in lookup_generated_unit() and again to locate the compact // direct-entry function. Do the dense generated-unit probe once and reuse // its unit index for both function and entry lookup. RecompiledFunction function = nullptr; RecompiledEntryFunction entry_function = nullptr; if (generated_unit_layout_valid_ && generated_unit_span_ != 0u) { const std::uint32_t canonical_pc = memory_.canonical(ctx.pc); if (canonical_pc >= generated_unit_base_) { const std::uint32_t unit_delta = canonical_pc - generated_unit_base_; const std::uint32_t unit_index = generated_unit_span_ == 16384u ? (unit_delta >> 14u) : (unit_delta / generated_unit_span_); if (unit_index < kGeneratedUnitFastCapacity) { function = generated_units_[unit_index]; if (function != nullptr && shared_aot_mem != nullptr && generated_unit_disabled_[unit_index] == 0u) entry_function = generated_unit_entries_[unit_index]; } } } if (function == nullptr) { const std::uint32_t delta = memory_.canonical(ctx.pc) - direct_base_; if ((delta & 3u) != 0u) return false; const std::size_t index = static_cast(delta) / 4u; if (index >= direct_chainable_.size()) return false; function = direct_chainable_[index]; if (function == nullptr) return false; } const std::uint32_t target_pc = ctx.pc; std::uint32_t guest_hotspot_unit = static_cast(kUnitProfileCapacity); bool guest_hotspot_sample = false; std::uint64_t guest_hotspot_start_ns = 0u; if (g_guest_hotspot_profile_enabled && generated_unit_layout_valid_ && generated_unit_span_ != 0u) { const std::uint32_t canonical_pc = memory_.canonical(target_pc); if (canonical_pc >= generated_unit_base_) { const std::uint32_t delta = canonical_pc - generated_unit_base_; const std::uint32_t unit_index = generated_unit_span_ == 16384u ? (delta >> 14u) : (delta / generated_unit_span_); if (unit_index < kUnitProfileCapacity) { guest_hotspot_unit = unit_index; ++g_guest_hotspot_unit_calls[unit_index]; const std::uint64_t ticket = ++guest_hotspot_ticket_; guest_hotspot_sample = (ticket & static_cast(g_guest_hotspot_sample_mask)) == 0u; if (guest_hotspot_sample) guest_hotspot_start_ns = guest_hotspot_clock_ns(); } } } const std::uint32_t native_depth = chain_depth_; // Always guard execution-context ownership. Even a clean generated unit can // reach a nested direct chain whose scheduler boundary switches PSP thread. // guarded every native chain frame; accidentally weakened // that invariant for the dense generated-unit path. // Generation alone is sufficient: it increments on every PSP thread // ownership change. Avoid constructing/checking a two-field token on every // dynamic native chain boundary in the city hot path. const std::uint64_t caller_generation = g_runtime_thread_switch_generation_fast; #if !defined(PSPRECOMP_AOT_PRODUCTION_FASTPATHS) if (g_pre_chained_call_hook != nullptr) g_pre_chained_call_hook(*this, ctx, target_pc, native_depth); #endif struct DepthGuard { std::uint32_t &depth; explicit DepthGuard(std::uint32_t &value) : depth(value) { ++depth; } ~DepthGuard() { --depth; } } guard(chain_depth_); if (entry_function != nullptr && shared_aot_mem != nullptr) { entry_function(*this, ctx, 0u, *shared_aot_mem); } else { function(*this, ctx); } if (guest_hotspot_sample) { const std::uint64_t end_ns = guest_hotspot_clock_ns(); guest_hotspot_record_sample(guest_hotspot_unit, target_pc, end_ns >= guest_hotspot_start_ns ? end_ns - guest_hotspot_start_ns : 0u); } #if !defined(PSPRECOMP_AOT_PRODUCTION_FASTPATHS) if (g_post_chained_call_hook != nullptr) g_post_chained_call_hook(*this, ctx, target_pc, native_depth); if (track_dispatch_counters_) ++chained_dispatches_; #endif // Dynamic targets may enter a profile/native function which switches PSP // ownership without passing through run_starvation_boundary(). Keep the // generation guard here even in production; compile-time direct chains use // chain_context_invalidated_ and retain their cheaper hot path. if (caller_generation != g_runtime_thread_switch_generation_fast) { (void)account_dispatch_work(ctx, false); return false; } return account_dispatch_work(ctx, true); } bool Runtime::invoke_chained_unit(AllegrexContext &ctx, std::uint32_t unit_index, GuestMemory::AotFastView *shared_aot_mem) { #if !defined(PSPRECOMP_AOT_PRODUCTION_FASTPATHS) count_pc(ctx.pc); #endif if (chain_depth_ >= chain_depth_limit_) return false; if (unit_index >= kGeneratedUnitFastCapacity || !generated_unit_layout_valid_) return false; RecompiledFunction function = generated_units_[unit_index]; if (function == nullptr) return false; if (g_unit_profile_enabled) ++g_unit_profile_counts[unit_index]; const std::uint32_t target_pc = ctx.pc; bool guest_hotspot_sample = false; std::uint64_t guest_hotspot_start_ns = 0u; if (g_guest_hotspot_profile_enabled) { ++g_guest_hotspot_unit_calls[unit_index]; const std::uint64_t ticket = ++guest_hotspot_ticket_; guest_hotspot_sample = (ticket & static_cast(g_guest_hotspot_sample_mask)) == 0u; if (guest_hotspot_sample) guest_hotspot_start_ns = guest_hotspot_clock_ns(); } const std::uint32_t native_depth = chain_depth_; const std::uint64_t caller_generation = g_runtime_thread_switch_generation_fast; #if !defined(PSPRECOMP_AOT_PRODUCTION_FASTPATHS) if (g_pre_chained_call_hook != nullptr) g_pre_chained_call_hook(*this, ctx, target_pc, native_depth); #endif struct DepthGuard { std::uint32_t &depth; explicit DepthGuard(std::uint32_t &value) : depth(value) { ++depth; } ~DepthGuard() { --depth; } } guard(chain_depth_); if (shared_aot_mem != nullptr && generated_unit_entries_[unit_index] != nullptr) { generated_unit_entries_[unit_index](*this, ctx, 0u, *shared_aot_mem); } else { function(*this, ctx); } if (guest_hotspot_sample) { const std::uint64_t end_ns = guest_hotspot_clock_ns(); guest_hotspot_record_sample(unit_index, target_pc, end_ns >= guest_hotspot_start_ns ? end_ns - guest_hotspot_start_ns : 0u); } #if !defined(PSPRECOMP_AOT_PRODUCTION_FASTPATHS) if (g_post_chained_call_hook != nullptr) g_post_chained_call_hook(*this, ctx, target_pc, native_depth); if (track_dispatch_counters_) ++chained_dispatches_; #endif // A descendant scheduler boundary may have switched the PSP context while // this frame was active. Never let a stale native caller resume it. if (caller_generation != g_runtime_thread_switch_generation_fast) { (void)account_dispatch_work(ctx, false); return false; } return account_dispatch_work(ctx, true); } void Runtime::register_generated_unit(std::uint32_t unit_index, std::uint32_t unit_address, std::uint32_t unit_span, RecompiledFunction function, RecompiledEntryFunction entry_function) { if (function == nullptr || unit_span == 0u || !generated_unit_layout_valid_) return; if (unit_index >= kGeneratedUnitFastCapacity) return; const std::uint32_t canonical_address = memory_.canonical(unit_address); const std::uint64_t offset = static_cast(unit_index) * unit_span; if (canonical_address < offset) return; const std::uint32_t base = canonical_address - static_cast(offset); if (generated_unit_span_ == 0u) { generated_unit_base_ = base; generated_unit_span_ = unit_span; } else if (generated_unit_base_ != base || generated_unit_span_ != unit_span) { // Mixed automatic-codegen layouts are unsupported. Permanently disable // the optional dense path for this Runtime; exact PC dispatch remains. generated_units_.fill(nullptr); generated_unit_disabled_.fill(0u); generated_unit_base_ = 0u; generated_unit_span_ = 0u; generated_unit_layout_valid_ = false; return; } if (generated_unit_disabled_[unit_index] == 0u) generated_units_[unit_index] = function; generated_unit_entries_[unit_index] = entry_function; } bool Runtime::invoke_isolated_aot(std::uint32_t address, AllegrexContext &ctx) { const RecompiledFunction function = lookup_function(address); if (function == nullptr) return false; ctx.pc = address; function(*this, ctx); ctx.gpr[0] = 0u; return true; } std::vector> Runtime::hle_histogram() const { std::vector> entries(hle_histogram_.begin(), hle_histogram_.end()); std::sort(entries.begin(), entries.end(), [](const auto &left, const auto &right) { if (left.second != right.second) return left.second > right.second; return left.first < right.first; }); return entries; } bool g_unit_profile_enabled = false; std::uint64_t g_unit_profile_counts[kUnitProfileCapacity]{}; void report_unit_profile(std::size_t limit) { if (!g_unit_profile_enabled) return; std::vector> entries; std::uint64_t total = 0u; for (std::size_t index = 0; index < kUnitProfileCapacity; ++index) { if (g_unit_profile_counts[index] == 0u) continue; entries.emplace_back(g_unit_profile_counts[index], index); total += g_unit_profile_counts[index]; } std::sort(entries.begin(), entries.end(), std::greater<>()); std::cerr << "[unit-profile] units=" << entries.size() << " calls=" << total << "\n"; std::uint64_t running = 0u; for (std::size_t i = 0; i < std::min(limit, entries.size()); ++i) { running += entries[i].first; std::cerr << "[unit-profile] unit=" << std::setw(4) << std::setfill('0') << entries[i].second << std::setfill(' ') << " calls=" << entries[i].first << " share=" << (100.0 * static_cast(entries[i].first) / static_cast(total ? total : 1u)) << "% cumulative=" << (100.0 * static_cast(running) / static_cast(total ? total : 1u)) << "%\n"; } } void Runtime::report_hle_histogram(std::size_t limit) const { if (!hle_histogram_enabled_) return; const auto entries = hle_histogram(); std::uint64_t total = 0u; for (const auto &entry : entries) total += entry.second; std::cerr << "[hle-histogram] unique=" << entries.size() << " calls=" << total << "\n"; for (std::size_t index = 0; index < std::min(limit, entries.size()); ++index) { const auto &entry = entries[index]; std::cerr << "[hle-histogram] " << entry.first << " count=" << entry.second << " name=" << nids_.resolve(entry.first.substr(0, entry.first.find(':')), static_cast( std::strtoul(entry.first.substr(entry.first.find(':') + 1u).c_str(), nullptr, 16))) .value_or("unknown") << "\n"; } } NidRegistry &Runtime::nids() noexcept { return nids_; } const NidRegistry &Runtime::nids() const noexcept { return nids_; } void Runtime::register_function(std::uint32_t address, RecompiledFunction function, std::string name) { if (!function) throw Error("Attempted to register a null recompiled function"); functions_[address] = FunctionEntry{function, std::move(name)}; const std::uint32_t c = memory_.canonical(address); if ((c & 3u) != 0u || c < GuestMemory::kPhysicalBase) return; // The direct table covers only the window that actually holds registered // code. Spanning the whole 32 MiB of guest RAM made it a 64 MiB array that // every outer dispatch probed at an effectively random offset, so the hot // path took a cache/TLB miss per dispatch. Growing on demand keeps it at // the size of the executable image instead. constexpr std::size_t chunk = 1024u * 1024u / 4u; const auto shift_forward = [](std::vector &table, std::size_t slots) { std::vector grown(table.size() + slots, nullptr); std::copy(table.begin(), table.end(), grown.begin() + static_cast(slots)); table = std::move(grown); }; if (direct_functions_.empty()) { direct_base_ = c & ~0xFFFFFu; // 1 MiB-aligned window start direct_functions_.assign(chunk, nullptr); direct_chainable_.assign(chunk, nullptr); } else if (c < direct_base_) { const std::size_t slots = static_cast(direct_base_ - (c & ~0xFFFFFu)) / 4u; shift_forward(direct_functions_, slots); shift_forward(direct_chainable_, slots); direct_base_ = c & ~0xFFFFFu; } const std::size_t index = static_cast(c - direct_base_) / 4u; if (index >= direct_functions_.size()) { const std::size_t grown = ((index / chunk) + 1u) * chunk; direct_functions_.resize(grown, nullptr); direct_chainable_.resize(grown, nullptr); } direct_functions_[index] = function; // Import wrappers and HLE entries are deliberately excluded: chaining must // never run one inline, because they are where thread switches happen. // Assign unconditionally -- an import stub can also be covered by a unit, // and a later import registration has to clear the earlier unit pointer or // chaining would keep running the raw code instead of the wrapper. const auto ®istered = functions_[address].name; const bool chainable = registered.starts_with("recomp_unit_"); direct_chainable_[index] = chainable ? function : nullptr; if (!chainable && generated_unit_span_ != 0u && c >= generated_unit_base_) { const std::uint32_t unit_delta = c - generated_unit_base_; const std::size_t unit_index = static_cast(unit_delta / generated_unit_span_); if (unit_index < kGeneratedUnitFastCapacity) { generated_unit_disabled_[unit_index] = 1u; generated_units_[unit_index] = nullptr; } } } Runtime::RecompiledFunction Runtime::lookup_function(std::uint32_t address) const noexcept { const std::uint32_t c = memory_.canonical(address); const std::uint32_t delta = c - direct_base_; if ((delta & 3u) == 0u) { const std::size_t index = static_cast(delta) / 4u; if (index < direct_functions_.size()) return direct_functions_[index]; } const auto found = functions_.find(address); return found != functions_.end() ? found->second.function : nullptr; } Runtime::RecompiledFunction Runtime::lookup_generated_unit(std::uint32_t address) const noexcept { if (!generated_unit_layout_valid_ || generated_unit_span_ == 0u) return nullptr; const std::uint32_t c = memory_.canonical(address); if (c < generated_unit_base_) return nullptr; const std::uint32_t delta = c - generated_unit_base_; const std::size_t unit_index = generated_unit_span_ == 16384u ? static_cast(delta >> 14u) : static_cast(delta / generated_unit_span_); if (unit_index >= kGeneratedUnitFastCapacity) return nullptr; return generated_units_[unit_index]; } const Runtime::FunctionEntry *Runtime::lookup_entry(std::uint32_t address) const noexcept { const auto found = functions_.find(address); return found != functions_.end() ? &found->second : nullptr; } std::string Runtime::hle_key(std::string_view library, std::uint32_t nid) { std::string key; key.reserve(library.size() + 11u); key.append(library); key.push_back(':'); key.append(hex32(nid)); return key; } void Runtime::register_hle(std::string library, std::uint32_t nid, HleFunction function) { hle_[std::move(library)][nid] = std::move(function); } bool Runtime::has_function(std::uint32_t address) const { return lookup_function(address) != nullptr; } std::size_t Runtime::function_count() const noexcept { return functions_.size(); } void Runtime::set_game_root(std::filesystem::path root) { game_root_ = std::filesystem::weakly_canonical(std::move(root)); } const std::filesystem::path &Runtime::game_root() const noexcept { return game_root_; } std::filesystem::path Runtime::translate_path(const std::string &psp_path) const { std::string relative = psp_path; const auto colon = relative.find(':'); if (colon != std::string::npos) relative.erase(0, colon + 1u); while (!relative.empty() && (relative.front() == '/' || relative.front() == '\\')) relative.erase(relative.begin()); std::replace(relative.begin(), relative.end(), '\\', '/'); std::filesystem::path clean; for (const auto &part : std::filesystem::path(relative)) { if (part == "..") throw Error("Rejected PSP path traversal: " + psp_path); if (part != ".") clean /= part; } return game_root_ / clean; } void Runtime::run(std::uint32_t entry, std::uint64_t max_dispatches) { stopped_ = false; stop_reason_.clear(); cpu_.pc = entry; load_counted_pcs(); const bool profile_dispatch = std::getenv("PSPRECOMP_PROFILE_DISPATCH") != nullptr; const auto parse_environment_u64 = [](const char *name, std::uint64_t fallback = 0u) { const char *text = std::getenv(name); if (text == nullptr || *text == '\0') return fallback; char *end = nullptr; const unsigned long long value = std::strtoull(text, &end, 0); return end != text && *end == '\0' ? static_cast(value) : fallback; }; const std::uint64_t profile_dispatch_start = parse_environment_u64("PSPRECOMP_PROFILE_DISPATCH_START"); const std::uint64_t profile_dispatch_count = parse_environment_u64("PSPRECOMP_PROFILE_DISPATCH_COUNT"); std::unordered_map dispatch_counts; if (profile_dispatch) dispatch_counts.reserve(4096u); struct DispatchSnapshot { std::uint32_t pc{}; std::uint32_t a0{}; std::uint32_t a1{}; std::uint32_t a2{}; std::uint32_t a3{}; std::uint32_t sp{}; std::uint32_t ra{}; std::int32_t thread_uid{-1}; std::array thread_name{}; }; std::array recent_dispatches{}; std::size_t recent_count = 0u; std::size_t recent_next = 0u; const bool trace_on_error = std::getenv("PSPRECOMP_TRACE_ON_ERROR") != nullptr; const bool world_stream_diag = std::getenv("PSPRECOMP_WORLD_STREAM_DIAG") != nullptr; const bool request_alloc_diag = std::getenv("PSPRECOMP_REQUEST_ALLOC_DIAG") != nullptr; const bool world_stream_stop_at_callback = std::getenv("PSPRECOMP_WORLD_STREAM_STOP_AT_CALLBACK") != nullptr; const std::uint32_t world_stream_manager = static_cast( parse_environment_u64("PSPRECOMP_WORLD_STREAM_MANAGER", 0x08E91200u)); bool world_stream_active_known = false; std::uint32_t world_stream_previous_active = 0u; const bool heap_diag = std::getenv("PSPRECOMP_HEAP_DIAG") != nullptr; const bool file_object_diag = std::getenv("PSPRECOMP_FILE_OBJECT_DIAG") != nullptr; const bool file_object_stop_on_null = std::getenv("PSPRECOMP_FILE_OBJECT_STOP_ON_NULL") != nullptr; std::uint32_t trace_pc = 0u; bool trace_pc_enabled = false; if (const char *trace_pc_text = std::getenv("PSPRECOMP_TRACE_PC")) { char *end = nullptr; trace_pc = static_cast(std::strtoul(trace_pc_text, &end, 0)); trace_pc_enabled = end != trace_pc_text && *end == '\0'; } const bool trace_dispatch = std::getenv("PSPRECOMP_TRACE") != nullptr; const bool strict_pc_progress = std::getenv("PSPRECOMP_STRICT_PC_PROGRESS") != nullptr; const bool diagnostic_dispatch = profile_dispatch || trace_on_error || world_stream_diag || request_alloc_diag || world_stream_stop_at_callback || heap_diag || file_object_diag || file_object_stop_on_null || trace_pc_enabled || trace_dispatch; const bool deferred_profile_only = profile_dispatch && profile_dispatch_start != 0u && !trace_on_error && !world_stream_diag && !request_alloc_diag && !world_stream_stop_at_callback && !heap_diag && !file_object_diag && !file_object_stop_on_null && !trace_pc_enabled && !trace_dispatch; if (deferred_profile_only) { const std::uint64_t profile_end = profile_dispatch_count == 0u || profile_dispatch_start > std::numeric_limits::max() - profile_dispatch_count ? max_dispatches : std::min(max_dispatches, profile_dispatch_start + profile_dispatch_count); std::uint64_t dispatch = 0u; auto execute_once = [&]() { const std::uint32_t before = cpu_.pc; chain_context_invalidated_ = false; const std::int32_t dispatch_thread_uid = g_runtime_thread_uid; RecompiledFunction function = lookup_function(before); if (function == nullptr) { stop("No recompiled function registered at " + hex32(before)); return; } g_runtime_dispatch_pc = before; count_pc(before); if (g_pre_dispatch_hook != nullptr) g_pre_dispatch_hook(*this, cpu_, before, dispatch_thread_uid); try { function(*this, cpu_); } catch (const Error &e) { const FunctionEntry *function_entry = lookup_entry(before); const std::string name = function_entry != nullptr ? function_entry->name : "unknown"; std::ostringstream message; message << e.what() << " while executing " << name << " dispatch=" << hex32(before) << " guest_pc=" << hex32(cpu_.pc) << " (a0=" << hex32(cpu_.gpr[4]) << ", a1=" << hex32(cpu_.gpr[5]) << ", a2=" << hex32(cpu_.gpr[6]) << ", a3=" << hex32(cpu_.gpr[7]) << ", sp=" << hex32(cpu_.gpr[29]) << ", ra=" << hex32(cpu_.gpr[31]) << ")"; append_gpr_dump(message, cpu_); throw Error(message.str()); } cpu_.gpr[0] = 0u; if (!stopped_ && g_post_dispatch_hook != nullptr) g_post_dispatch_hook(*this, cpu_, before, dispatch_thread_uid); if (!stopped_) (void)account_dispatch_work(cpu_, true); if (!stopped_ && strict_pc_progress && cpu_.pc == before) { const FunctionEntry *function_entry = lookup_entry(before); const std::string name = function_entry != nullptr ? function_entry->name : "unknown"; stop("Recompiled function returned without changing PC: " + name + " at " + hex32(before)); } }; for (; dispatch < std::min(profile_dispatch_start, max_dispatches) && !stopped_; ++dispatch) execute_once(); for (; dispatch < profile_end && !stopped_; ++dispatch) { ++dispatch_counts[cpu_.pc]; execute_once(); } std::vector> hot(dispatch_counts.begin(), dispatch_counts.end()); std::sort(hot.begin(), hot.end(), [](const auto &left, const auto &right) { return left.second > right.second; }); std::cerr << "[profile-window] start=" << profile_dispatch_start << " executed=" << dispatch << " sampled=" << (dispatch >= profile_dispatch_start ? dispatch - profile_dispatch_start : 0u) << " pc=" << hex32(cpu_.pc) << " unique=" << hot.size() << "\n"; const std::size_t count = std::min(30u, hot.size()); for (std::size_t index = 0; index < count; ++index) { const FunctionEntry *profile_entry = lookup_entry(hot[index].first); std::cerr << "[profile-window] " << hex32(hot[index].first) << " count=" << hot[index].second << " name=" << (profile_entry != nullptr ? profile_entry->name : "unknown") << "\n"; } if (!stopped_) stop("Dispatch profile window complete at " + hex32(cpu_.pc)); return; } if (!diagnostic_dispatch) { const std::uint64_t progress_every = parse_environment_u64("PSPRECOMP_PROGRESS_EVERY"); std::uint64_t next_progress = progress_every; const RuntimeHeartbeatHook heartbeat = g_heartbeat_hook; const std::uint64_t heartbeat_every = heartbeat != nullptr ? g_heartbeat_interval : 0u; std::uint64_t next_heartbeat = heartbeat_every; const RuntimeStarvationHook starvation = g_starvation_hook; const std::uint64_t starvation_every = starvation != nullptr ? g_starvation_interval : 0u; std::uint64_t executed_dispatches = 0u; for (; executed_dispatches < max_dispatches && !stopped_; ++executed_dispatches) { const std::uint32_t before = cpu_.pc; chain_context_invalidated_ = false; const std::int32_t dispatch_thread_uid = g_runtime_thread_uid; // Most outer dispatches are ordinary AOT PCs. Resolve those // through the tiny generated-unit table first; the old per-PC // table spans several MiB and an effectively random probe here can // cost a cache/TLB miss at every outer return. Any unit containing // a host/import override is poisoned at registration and falls // back to exact per-PC dispatch automatically. RecompiledFunction function = lookup_generated_unit(before); if (function == nullptr) function = lookup_function(before); if (function == nullptr) { stop("No recompiled function registered at " + hex32(before)); break; } g_runtime_dispatch_pc = before; count_pc(before); if (g_pre_dispatch_hook != nullptr) g_pre_dispatch_hook(*this, cpu_, before, dispatch_thread_uid); try { function(*this, cpu_); } catch (const Error &e) { const FunctionEntry *function_entry = lookup_entry(before); const std::string name = function_entry != nullptr ? function_entry->name : "unknown"; std::ostringstream message; message << e.what() << " while executing " << name << " dispatch=" << hex32(before) << " guest_pc=" << hex32(cpu_.pc) << " (a0=" << hex32(cpu_.gpr[4]) << ", a1=" << hex32(cpu_.gpr[5]) << ", a2=" << hex32(cpu_.gpr[6]) << ", a3=" << hex32(cpu_.gpr[7]) << ", sp=" << hex32(cpu_.gpr[29]) << ", ra=" << hex32(cpu_.gpr[31]) << ")"; append_gpr_dump(message, cpu_); throw Error(message.str()); } // `$zero` is now enforced at every generated/HLE write site. Do not // dirty the context register cache line after every outer dispatch. if (!stopped_ && g_post_dispatch_hook != nullptr) g_post_dispatch_hook(*this, cpu_, before, dispatch_thread_uid); if (progress_every != 0u && executed_dispatches + 1u >= next_progress) { std::cerr << "[progress] dispatch=" << (executed_dispatches + 1u) << " pc=" << hex32(cpu_.pc) << "\n"; next_progress += progress_every; } if (heartbeat_every != 0u && executed_dispatches + 1u >= next_heartbeat) { heartbeat(executed_dispatches + 1u, cpu_.pc); next_heartbeat += heartbeat_every; } if (starvation_every != 0u && starvation != nullptr) (void)account_dispatch_work(cpu_, true); if (!stopped_ && strict_pc_progress && cpu_.pc == before) { const FunctionEntry *function_entry = lookup_entry(before); const std::string name = function_entry != nullptr ? function_entry->name : "unknown"; stop("Recompiled function returned without changing PC: " + name + " at " + hex32(before)); } } if (std::getenv("PSPRECOMP_REPORT_DISPATCH_COUNT") != nullptr) { std::cerr << "[dispatch-count] executed=" << executed_dispatches << " chained=" << chained_dispatches_ << " total=" << (executed_dispatches + chained_dispatches_) << " pc=" << hex32(cpu_.pc) << "\n"; } if (!stopped_ && max_dispatches != 0u) stop("Dispatch limit reached at " + hex32(cpu_.pc)); return; } for (std::uint64_t dispatch = 0; dispatch < max_dispatches && !stopped_; ++dispatch) { if (profile_dispatch) ++dispatch_counts[cpu_.pc]; const std::uint32_t before = cpu_.pc; chain_context_invalidated_ = false; RecompiledFunction function = lookup_function(before); const FunctionEntry *function_entry = lookup_entry(before); if (function == nullptr) { stop("No recompiled function registered at " + hex32(before)); break; } g_runtime_dispatch_pc = before; count_pc(before); DispatchSnapshot current_snapshot{}; current_snapshot.pc = before; current_snapshot.a0 = cpu_.gpr[4]; current_snapshot.a1 = cpu_.gpr[5]; current_snapshot.a2 = cpu_.gpr[6]; current_snapshot.a3 = cpu_.gpr[7]; current_snapshot.sp = cpu_.gpr[29]; current_snapshot.ra = cpu_.gpr[31]; current_snapshot.thread_uid = g_runtime_thread_uid; current_snapshot.thread_name = g_runtime_thread_name; recent_dispatches[recent_next] = current_snapshot; recent_next = (recent_next + 1u) % recent_dispatches.size(); recent_count = std::min(recent_count + 1u, recent_dispatches.size()); if (before == 0x089345B0u && heap_diag) { constexpr std::uint32_t manager = 0x08BC6500u; std::cerr << "[heapdiag] callback entered manager=" << hex32(manager) << " global=" << hex32(memory_.load32(0x08BADEF8u)) << "\n"; for (std::uint32_t offset = 0; offset < 0x140u; offset += 16u) { std::cerr << "[heapdiag] " << hex32(manager + offset); for (std::uint32_t word = 0; word < 16u; word += 4u) std::cerr << " " << hex32(memory_.load32(manager + offset + word)); std::cerr << "\n"; } std::uint32_t block = memory_.load32(manager + 8u); for (std::uint32_t index = 0u; block != 0u && index < 64u; ++index) { if (!memory_.contains(block, 16u)) { std::cerr << "[heapdiag-free] invalid=" << hex32(block) << "\n"; break; } const std::uint32_t size = memory_.load32(block + 0u); const std::uint32_t prev = memory_.load32(block + 8u); const std::uint32_t next = memory_.load32(block + 12u); std::cerr << "[heapdiag-free] index=" << index << " block=" << hex32(block) << " size=" << hex32(size) << " prev=" << hex32(prev) << " next=" << hex32(next) << " end=" << hex32(block + size) << "\n"; block = next; } } if (request_alloc_diag && (before == 0x089390ACu || before == 0x08939114u || before == 0x08939590u || before == 0x089395D4u || before == 0x0893961Cu || before == 0x089396D8u || before == 0x089397CCu || before == 0x08956258u)) { const RuntimeExecutionContextToken token = capture_runtime_execution_context(); std::uint32_t manager = 0u; if (before == 0x08939590u) manager = cpu_.gpr[4]; else if (before == 0x089395D4u || before == 0x0893961Cu || before == 0x089396D8u) manager = cpu_.gpr[16]; else if ((before == 0x089390ACu || before == 0x08939114u) && memory_.contains(cpu_.gpr[28] + 5908u, 4u)) manager = memory_.load32(cpu_.gpr[28] + 5908u); else if (before == 0x089397CCu) manager = cpu_.gpr[4]; std::cerr << "[reqalloc] pc=" << hex32(before) << " uid=" << token.thread_uid << " gen=" << token.switch_generation << " v0=" << hex32(cpu_.gpr[2]) << " a0=" << hex32(cpu_.gpr[4]) << " a1=" << hex32(cpu_.gpr[5]) << " a2=" << hex32(cpu_.gpr[6]) << " a3=" << hex32(cpu_.gpr[7]) << " t0=" << hex32(cpu_.gpr[8]) << " t1=" << hex32(cpu_.gpr[9]) << " s0=" << hex32(cpu_.gpr[16]) << " s1=" << hex32(cpu_.gpr[17]) << " s2=" << hex32(cpu_.gpr[18]) << " sp=" << hex32(cpu_.gpr[29]) << " ra=" << hex32(cpu_.gpr[31]); if (manager != 0u && memory_.contains(manager + 6912u, 4u)) { const std::uint32_t free_head = memory_.load32(manager + 6900u); const std::uint32_t active_head = memory_.load32(manager + 6908u); std::cerr << " manager=" << hex32(manager) << " free_head=" << hex32(free_head) << " active_head=" << hex32(active_head); } std::uint32_t request = 0u; if (before == 0x089397CCu) request = cpu_.gpr[5]; else if (before == 0x089396D8u) request = cpu_.gpr[18]; else if (before == 0x08956258u || before == 0x08939114u) request = cpu_.gpr[2]; if (request != 0u && memory_.contains(request, 52u)) { std::cerr << " req=" << hex32(request) << " prev=" << hex32(memory_.load32(request + 0u)) << " next=" << hex32(memory_.load32(request + 4u)) << " size=" << memory_.load32(request + 8u) << " source=" << hex32(memory_.load32(request + 16u)) << " offset=" << memory_.load32(request + 20u) << " remaining=" << memory_.load32(request + 24u) << " progressed=" << memory_.load32(request + 28u) << " callback=" << hex32(memory_.load32(request + 48u)); } std::cerr << "\n"; } if (world_stream_diag && (before == 0x08953990u || before == 0x08955134u || before == 0x08955E7Cu || before == 0x08956258u || before == 0x089563C0u || before == 0x08956408u || before == 0x089569C0u || before == 0x089569E0u)) { std::cerr << "[worlddiag] pc=" << hex32(before) << " uid=" << g_runtime_thread_uid << " a0=" << hex32(cpu_.gpr[4]) << " a1=" << hex32(cpu_.gpr[5]) << " ra=" << hex32(cpu_.gpr[31]); if (before == 0x089563C0u && memory_.contains(cpu_.gpr[4], 1040u)) { const std::uint32_t manager = cpu_.gpr[4]; std::cerr << " req=" << hex32(cpu_.gpr[5]) << " f596=" << hex32(memory_.load32(manager + 596u)) << " f600=" << hex32(memory_.load32(manager + 600u)) << " event=" << memory_.load32(manager + 616u) << " active=" << hex32(memory_.load32(manager + 628u)) << " mode=" << memory_.load32(manager + 636u) << " work=" << hex32(memory_.load32(manager + 640u)) << " limit=" << memory_.load32(manager + 648u) << " stack=" << memory_.load32(manager + 1036u); } std::uint32_t manager = world_stream_manager; if (before == 0x08955E7Cu || before == 0x08956258u) manager = cpu_.gpr[17]; else if (before == 0x08956408u) manager = cpu_.gpr[16]; else if (before == 0x089563C0u) manager = cpu_.gpr[4]; if (manager != 0u && memory_.contains(manager + 628u, 4u)) { std::cerr << " manager=" << hex32(manager) << " active_before=" << hex32(memory_.load32(manager + 628u)) << " pending_v0=" << hex32(cpu_.gpr[2]); } std::cerr << "\n"; if (world_stream_stop_at_callback && before == 0x089563C0u) { stop("World-stream diagnostic stop at callback " + hex32(before)); break; } } if (file_object_diag && (before == 0x08938F04u || before == 0x08938F7Cu || before == 0x089394A4u || before == 0x08955DCCu || before == 0x08955DFCu || before == 0x08955E58u || before == 0x08955E7Cu)) { const std::uint32_t manager = before == 0x08955DCCu ? cpu_.gpr[4] : ((before == 0x08955DFCu || before == 0x08955E58u || before == 0x08955E7Cu) ? cpu_.gpr[17] : 0u); const std::uint32_t object = before == 0x08938F7Cu ? cpu_.gpr[4] : (before == 0x089394A4u ? cpu_.gpr[5] : 0u); const bool seek_pc = before == 0x08938F7Cu || before == 0x089394A4u; bool manager_changed = false; static std::uint32_t previous_manager = 0u; static std::uint32_t previous_file_object = 0xFFFFFFFFu; static std::uint32_t previous_active = 0xFFFFFFFFu; static std::uint32_t previous_mode = 0xFFFFFFFFu; std::uint32_t manager_file_object = 0u; std::uint32_t manager_active = 0u; std::uint32_t manager_mode = 0u; if (manager != 0u && memory_.contains(manager, 652u)) { manager_file_object = memory_.load32(manager + 624u); manager_active = memory_.load32(manager + 628u); manager_mode = memory_.load32(manager + 636u); manager_changed = manager != previous_manager || manager_file_object != previous_file_object || manager_active != previous_active || manager_mode != previous_mode; previous_manager = manager; previous_file_object = manager_file_object; previous_active = manager_active; previous_mode = manager_mode; } const bool should_log = before == 0x08938F04u || (seek_pc && object == 0u) || manager_changed; if (should_log) { std::cerr << "[fileobj] pc=" << hex32(before) << " uid=" << g_runtime_thread_uid << " name=" << g_runtime_thread_name.data() << " v0=" << hex32(cpu_.gpr[2]) << " a0=" << hex32(cpu_.gpr[4]) << " a1=" << hex32(cpu_.gpr[5]) << " a2=" << hex32(cpu_.gpr[6]) << " a3=" << hex32(cpu_.gpr[7]) << " s0=" << hex32(cpu_.gpr[16]) << " s1=" << hex32(cpu_.gpr[17]) << " sp=" << hex32(cpu_.gpr[29]) << " ra=" << hex32(cpu_.gpr[31]); if (before == 0x08938F04u && memory_.contains(cpu_.gpr[4])) { try { std::cerr << " path=\"" << memory_.read_c_string(cpu_.gpr[4], 512u) << "\""; } catch (...) {} } if (manager != 0u && memory_.contains(manager, 652u)) { std::cerr << " manager=" << hex32(manager) << " f596=" << hex32(memory_.load32(manager + 596u)) << " f600=" << hex32(memory_.load32(manager + 600u)) << " event=" << memory_.load32(manager + 616u) << " fileobj=" << hex32(manager_file_object) << " active=" << hex32(manager_active) << " mode=" << manager_mode << " work=" << hex32(memory_.load32(manager + 640u)) << " offset=" << memory_.load32(manager + 644u) << " length=" << memory_.load32(manager + 648u); } if (object != 0u && memory_.contains(object, 16u)) { std::cerr << " object=" << hex32(object) << " words=" << hex32(memory_.load32(object + 0u)) << "," << hex32(memory_.load32(object + 4u)) << "," << hex32(memory_.load32(object + 8u)) << "," << hex32(memory_.load32(object + 12u)); } std::cerr << "\n"; } if (file_object_stop_on_null && seek_pc && object == 0u) { stop("File-object diagnostic stop before null seek at " + hex32(before)); break; } } if (trace_pc_enabled && before == trace_pc) { std::cerr << "[trace-pc] " << hex32(before) << " " << (function_entry != nullptr ? function_entry->name : std::string("unknown")) << " v0=" << hex32(cpu_.gpr[2]) << " a0=" << hex32(cpu_.gpr[4]) << " a1=" << hex32(cpu_.gpr[5]) << " a2=" << hex32(cpu_.gpr[6]) << " a3=" << hex32(cpu_.gpr[7]) << " s0=" << hex32(cpu_.gpr[16]) << " s1=" << hex32(cpu_.gpr[17]) << " s2=" << hex32(cpu_.gpr[18]) << " s3=" << hex32(cpu_.gpr[19]) << " sp=" << hex32(cpu_.gpr[29]) << " ra=" << hex32(cpu_.gpr[31]); if (memory_.contains(cpu_.gpr[4])) { try { std::cerr << " a0str=\"" << memory_.read_c_string(cpu_.gpr[4], 256u) << "\""; } catch (...) {} } if (memory_.contains(cpu_.gpr[5])) { try { std::cerr << " a1str=\"" << memory_.read_c_string(cpu_.gpr[5], 256u) << "\""; } catch (...) {} } if (std::getenv("PSPRECOMP_TRACE_MEM") != nullptr) { for (const auto &[label, address] : std::array, 2>{{{"a0mem", cpu_.gpr[4]}, {"a1mem", cpu_.gpr[5]}}}) { if (!memory_.contains(address, 32u)) continue; std::cerr << " " << label << "="; for (std::uint32_t offset = 0; offset < 32u; offset += 4u) std::cerr << (offset == 0u ? "" : ",") << hex32(memory_.load32(address + offset)); } } std::cerr << "\n"; } if (std::getenv("PSPRECOMP_TRACE") != nullptr) { std::cerr << "[dispatch] " << hex32(before) << " " << (function_entry != nullptr ? function_entry->name : std::string("unknown")) << " a0=" << hex32(cpu_.gpr[4]) << " a1=" << hex32(cpu_.gpr[5]) << " a2=" << hex32(cpu_.gpr[6]) << " a3=" << hex32(cpu_.gpr[7]) << " sp=" << hex32(cpu_.gpr[29]) << " ra=" << hex32(cpu_.gpr[31]) << "\n"; } try { function(*this, cpu_); } catch (const Error &e) { if (trace_on_error) { std::cerr << "[recent-dispatches] oldest-to-newest count=" << recent_count << "\n"; const std::size_t first = (recent_next + recent_dispatches.size() - recent_count) % recent_dispatches.size(); for (std::size_t index = 0; index < recent_count; ++index) { const DispatchSnapshot &snapshot = recent_dispatches[(first + index) % recent_dispatches.size()]; std::cerr << "[recent] uid=" << snapshot.thread_uid << " name=" << snapshot.thread_name.data() << " pc=" << hex32(snapshot.pc) << " a0=" << hex32(snapshot.a0) << " a1=" << hex32(snapshot.a1) << " a2=" << hex32(snapshot.a2) << " a3=" << hex32(snapshot.a3) << " sp=" << hex32(snapshot.sp) << " ra=" << hex32(snapshot.ra) << "\n"; } } std::ostringstream message; message << e.what() << " while executing " << (function_entry != nullptr ? function_entry->name : std::string("unknown")) << " dispatch=" << hex32(before) << " guest_pc=" << hex32(cpu_.pc) << " (a0=" << hex32(cpu_.gpr[4]) << ", a1=" << hex32(cpu_.gpr[5]) << ", a2=" << hex32(cpu_.gpr[6]) << ", a3=" << hex32(cpu_.gpr[7]) << ", sp=" << hex32(cpu_.gpr[29]) << ", ra=" << hex32(cpu_.gpr[31]) << ")"; append_gpr_dump(message, cpu_); throw Error(message.str()); } cpu_.gpr[0] = 0u; if (world_stream_diag && world_stream_manager != 0u && memory_.contains(world_stream_manager + 628u, 4u)) { const std::uint32_t active = memory_.load32(world_stream_manager + 628u); if (!world_stream_active_known) { world_stream_previous_active = active; world_stream_active_known = true; } else if (active != world_stream_previous_active) { std::cerr << "[worlddiag-transition] dispatch_pc=" << hex32(before) << " uid_before=" << current_snapshot.thread_uid << " uid_after=" << g_runtime_thread_uid << " manager=" << hex32(world_stream_manager) << " active=" << hex32(world_stream_previous_active) << "->" << hex32(active) << " next_pc=" << hex32(cpu_.pc); if (active != 0u && memory_.contains(active, 52u)) { std::cerr << " source=" << hex32(memory_.load32(active + 16u)) << " offset=" << memory_.load32(active + 20u) << " remaining=" << memory_.load32(active + 24u) << " progressed=" << memory_.load32(active + 28u) << " callback=" << hex32(memory_.load32(active + 48u)); } std::cerr << "\n"; world_stream_previous_active = active; } if (before == 0x08955E7Cu || before == 0x08956258u || before == 0x08956408u || before == 0x089563C0u) { std::cerr << "[worlddiag-after] dispatch_pc=" << hex32(before) << " uid_before=" << current_snapshot.thread_uid << " uid_after=" << g_runtime_thread_uid << " manager=" << hex32(world_stream_manager) << " active_after=" << hex32(active) << " next_pc=" << hex32(cpu_.pc) << "\n"; } } if (!stopped_ && g_post_dispatch_hook != nullptr) g_post_dispatch_hook(*this, cpu_, before, current_snapshot.thread_uid); // Starvation preemption must behave identically under diagnostics, or a // traced run would stall where a production run makes progress. if (g_starvation_hook != nullptr && g_starvation_interval != 0u) (void)account_dispatch_work(cpu_, true); // A recompiled unit may explicitly redispatch to its own entry through // a guest jump table or self-loop. This is legal PSP control flow and // must not be confused with a missing PC write. The dispatch cap still // bounds genuine infinite loops. Keep the old assertion available as // an opt-in diagnostic when investigating generated-code fallthrough. if (!stopped_ && cpu_.pc == before && strict_pc_progress) { if (trace_on_error) { std::cerr << "[unchanged-pc] pc=" << hex32(before) << " function=" << (function_entry != nullptr ? function_entry->name : std::string("unknown")) << " v0=" << hex32(cpu_.gpr[2]) << " a0=" << hex32(cpu_.gpr[4]) << " a1=" << hex32(cpu_.gpr[5]) << " a2=" << hex32(cpu_.gpr[6]) << " a3=" << hex32(cpu_.gpr[7]) << " t0=" << hex32(cpu_.gpr[8]) << " t1=" << hex32(cpu_.gpr[9]) << " t2=" << hex32(cpu_.gpr[10]) << " t3=" << hex32(cpu_.gpr[11]) << " sp=" << hex32(cpu_.gpr[29]) << " ra=" << hex32(cpu_.gpr[31]) << "\n"; } stop("Recompiled function returned without changing PC: " + (function_entry != nullptr ? function_entry->name : std::string("unknown")) + " at " + hex32(before)); } } if (!stopped_ && max_dispatches != 0u) { if (trace_on_error) { std::cerr << "[recent-dispatches] dispatch-limit oldest-to-newest count=" << recent_count << "\n"; const std::size_t first = (recent_next + recent_dispatches.size() - recent_count) % recent_dispatches.size(); for (std::size_t index = 0; index < recent_count; ++index) { const DispatchSnapshot &snapshot = recent_dispatches[(first + index) % recent_dispatches.size()]; std::cerr << "[recent] uid=" << snapshot.thread_uid << " name=" << snapshot.thread_name.data() << " pc=" << hex32(snapshot.pc) << " a0=" << hex32(snapshot.a0) << " a1=" << hex32(snapshot.a1) << " a2=" << hex32(snapshot.a2) << " a3=" << hex32(snapshot.a3) << " sp=" << hex32(snapshot.sp) << " ra=" << hex32(snapshot.ra) << "\n"; } } if (profile_dispatch) { std::vector> hot(dispatch_counts.begin(), dispatch_counts.end()); std::sort(hot.begin(), hot.end(), [](const auto &left, const auto &right) { return left.second > right.second; }); std::cerr << "[profile] dispatch limit pc=" << hex32(cpu_.pc) << " unique=" << hot.size() << "\n"; const std::size_t count = std::min(20u, hot.size()); for (std::size_t index = 0; index < count; ++index) { const auto function = functions_.find(hot[index].first); std::cerr << "[profile] " << hex32(hot[index].first) << " count=" << hot[index].second << " name=" << (function != functions_.end() ? function->second.name : "unknown") << "\n"; } } stop("Dispatch limit reached at " + hex32(cpu_.pc)); } } void Runtime::stop(std::string reason) { stopped_ = true; stop_reason_ = std::move(reason); } bool Runtime::stopped() const noexcept { return stopped_; } const std::string &Runtime::stop_reason() const noexcept { return stop_reason_; } void Runtime::unsupported(std::uint32_t pc, std::uint32_t instruction, const std::string &reason) { stop("Unsupported Allegrex instruction " + hex32(instruction) + " at " + hex32(pc) + ": " + reason); } void Runtime::arithmetic_overflow(std::uint32_t pc, std::uint32_t instruction) { stop("Allegrex arithmetic overflow for " + hex32(instruction) + " at " + hex32(pc)); } void Runtime::register_native_fast_path(std::uint32_t address, NativeFastPath function) { const std::uint32_t canonical_address = memory_.canonical(address); if (!function) { native_fast_paths_.erase(canonical_address); return; } native_fast_paths_[canonical_address] = std::move(function); } void Runtime::invoke_native_fast_path(std::uint32_t address, AllegrexContext &ctx) { const std::uint32_t canonical_address = memory_.canonical(address); const std::uint32_t current_pc = memory_.canonical(ctx.pc); // A profile may use this entry at an indirect call site whose target varies. // If the current target is not the registered native leaf, preserve the old // low-overhead direct-chainable fallback without exposing profile details. if (current_pc != canonical_address) { if (current_pc < direct_base_) return; const std::uint32_t delta = current_pc - direct_base_; if ((delta & 3u) != 0u) return; const std::size_t index = static_cast(delta) / 4u; if (index >= direct_chainable_.size()) return; const RecompiledFunction target_function = direct_chainable_[index]; if (target_function == nullptr) return; target_function(*this, ctx); ctx.gpr[0] = 0u; return; } const auto fast = native_fast_paths_.find(canonical_address); if (fast != native_fast_paths_.end()) { fast->second(*this, ctx); ctx.gpr[0] = 0u; return; } // Missing profile registration is safe: execute the generated AOT body. const RecompiledFunction original = lookup_function(canonical_address); if (original == nullptr) throw Error("Missing AOT function for native fast path " + hex32(canonical_address)); original(*this, ctx); ctx.gpr[0] = 0u; } void Runtime::invoke_import_cached(std::uint32_t slot, std::string_view library, std::uint32_t nid, AllegrexContext &ctx) { if (hle_histogram_enabled_) ++hle_histogram_[hle_key(library, nid)]; const HleFunction *bound = slot < import_bindings_.size() ? import_bindings_[slot] : nullptr; if (bound == nullptr) { const auto library_it = hle_.find(library); const auto function_it = library_it != hle_.end() ? library_it->second.find(nid) : HleLibrary::const_iterator{}; if (library_it == hle_.end() || function_it == library_it->second.end()) { const std::string library_name(library); const auto name = nids_.resolve(library_name, nid).value_or(hex32(nid)); stop("Missing HLE import " + library_name + "::" + name); return; } if (slot >= import_bindings_.size()) import_bindings_.resize(static_cast(slot) + 1u, nullptr); bound = &function_it->second; import_bindings_[slot] = bound; } (*bound)(*this, ctx); if (!stopped_ && g_post_import_hook != nullptr) g_post_import_hook(*this, ctx); } void Runtime::invoke_import(std::string_view library, std::uint32_t nid, AllegrexContext &ctx) { // PSPRECOMP_HLE_HISTOGRAM distinguishes a genuine synchronous workload from // a kernel-wait livelock: real translated work barely calls into the HLE, // while a thread spinning on an operation the host never completes shows up // as a huge count on one or two wait imports. if (hle_histogram_enabled_) ++hle_histogram_[hle_key(library, nid)]; const auto library_it = hle_.find(library); const auto function_it = library_it != hle_.end() ? library_it->second.find(nid) : HleLibrary::const_iterator{}; if (library_it == hle_.end() || function_it == library_it->second.end()) { const std::string library_name(library); const auto name = nids_.resolve(library_name, nid).value_or(hex32(nid)); stop("Missing HLE import " + library_name + "::" + name); return; } function_it->second(*this, ctx); if (!stopped_ && g_post_import_hook != nullptr) g_post_import_hook(*this, ctx); } AllegrexContext &Runtime::cpu() noexcept { return cpu_; } const AllegrexContext &Runtime::cpu() const noexcept { return cpu_; } } // namespace psprecomp