This commit is contained in:
patchzyy
2026-08-23 17:10:50 +02:00
commit ec226e8348
1099 changed files with 400981 additions and 0 deletions
+794
View File
@@ -0,0 +1,794 @@
#pragma once
#include "memory.h"
#include "ppc_runtime.h"
#include "system_bridge.h"
#include "game_graphics_options.h"
#include "runtime_log.h"
#include <array>
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <optional>
#include <stdexcept>
#include <sstream>
#include <tuple>
#include <type_traits>
inline void InvokeIndirectCpu(uint32_t target, CpuContext* ctx);
// Some game-facing runtime options alter arguments at well-defined ABI
// boundaries. Keep this independent of the dispatch mechanism: generated
// static calls deliberately bypass InvokeDirectCpu for performance.
// (used for the path mask filtering in ScnRenderer::createPath: depth of
// field is always removed, bloom when the user disabled it)
inline void ApplyRuntimeCallOptions(uint32_t target, CpuContext* ctx) {
if (target == 0x8023BD38u) {
// ScnRenderer::createPath receives the post-processing path mask in r4.
ctx->gpr[4] = RuntimeGameGraphicsOptions::FilterScnRendererPathMask(ctx->gpr[4]);
}
}
// Persistent per-thread CPU context used across translated function calls.
CpuContext& GetPersistentCpuContext();
void InitializePersistentCpuContext();
enum class FunctionKind : uint8_t {
BaseTranslated = 0,
ModTranslated = 1,
Native = 2,
};
inline constexpr uint32_t kPpcAllNonvolatileFprMask = 0xffffc000u;
inline constexpr uint32_t kBaseTranslatedFunctionPriority = 0;
inline constexpr uint32_t kModTranslatedFunctionPriorityBase = 100;
inline constexpr uint32_t kNativeFunctionPriority = 10000;
#include "isa/ppc_isa_cr.h"
struct TranslatedFunctionInfo {
uint32_t address = 0;
const char* name = "";
uint64_t moduleId = 0;
uint32_t priority = 0;
uint32_t nonvolatileFprWriteMask = kPpcAllNonvolatileFprMask;
void* entryPoint = nullptr; // Raw typed function pointer for indirect-call resolution
void (*rawCpuInvoker)(CpuContext*) = nullptr;
bool mustRemainDynamicallyDispatchable = true;
FunctionKind kind = FunctionKind::BaseTranslated;
};
struct RawDispatchRecord {
uint32_t address = 0;
void (*entry)(CpuContext*) = nullptr;
uint32_t nonvolatileFprWriteMask = kPpcAllNonvolatileFprMask;
bool preserveNonvolatileGprs = false;
};
// Translator-owned indirect dispatch is split by the high address byte and
// then by 4 KiB guest pages. Each populated segment stores only the contiguous
// page range that contains verified function entry points. The final search is
// bounded to at most the 1024 aligned instruction addresses in one guest page.
struct StaticIndirectDispatchPage {
uint32_t firstEntry = 0;
uint16_t entryCount = 0;
// Explicit tail padding to an 8-byte stride. The emitter writes it as a
// literal third initializer, so it is part of the generated-data contract.
uint16_t reserved = 0;
};
struct StaticIndirectDispatchSegment {
const StaticIndirectDispatchPage* pages = nullptr;
uint16_t firstPage = 0;
uint16_t pageCount = 0;
};
struct StaticIndirectDispatchTable {
const char* profileName = nullptr;
const StaticIndirectDispatchSegment* segments = nullptr; // Exactly 256 high-byte segments.
const RawDispatchRecord* entries = nullptr;
size_t entryCount = 0;
};
inline const RawDispatchRecord* FindStaticIndirectDispatchEntry(
const StaticIndirectDispatchTable* table,
uint32_t address) noexcept {
if (!table || !table->segments || !table->entries || table->entryCount == 0) {
return nullptr;
}
const auto& segment = table->segments[address >> 24];
const uint32_t pageNumber = (address >> 12) & 0x0FFFu;
if (!segment.pages || pageNumber < segment.firstPage) {
return nullptr;
}
const uint32_t relativePage = pageNumber - segment.firstPage;
if (relativePage >= segment.pageCount) {
return nullptr;
}
const auto& page = segment.pages[relativePage];
const size_t first = page.firstEntry;
const size_t count = page.entryCount;
if (first > table->entryCount || count > table->entryCount - first) {
return nullptr;
}
size_t lower = first;
size_t upper = first + count;
while (lower < upper) {
const size_t middle = lower + (upper - lower) / 2;
const uint32_t candidate = table->entries[middle].address;
if (candidate < address) {
lower = middle + 1;
} else {
upper = middle;
}
}
return lower < first + count && table->entries[lower].address == address
? &table->entries[lower]
: nullptr;
}
void RegisterStaticIndirectDispatchTable(const StaticIndirectDispatchTable* table);
inline std::atomic<const StaticIndirectDispatchTable*> g_publishedStaticIndirectDispatchTable{nullptr};
class StaticIndirectDispatchTableRegistrar {
public:
explicit StaticIndirectDispatchTableRegistrar(const StaticIndirectDispatchTable* table) {
RegisterStaticIndirectDispatchTable(table);
}
};
inline unsigned PpcLowestSetBitIndex(uint32_t value) noexcept {
return static_cast<unsigned>(__builtin_ctz(value));
}
class PpcNonvolatileFprGuard {
public:
explicit PpcNonvolatileFprGuard(CpuContext* cpu, uint32_t mask = kPpcAllNonvolatileFprMask) noexcept
: cpu_(cpu), mask_(mask & kPpcAllNonvolatileFprMask) {
if (!cpu_ || mask_ == 0) {
return;
}
// Real masks are a single contiguous register run - f14..f31 for the
// conservative default and f23..f31 or similar for a measured writer -
// so resolve the run once and copy it as one straight-line block instead
// of testing 18 mask bits. Any other mask keeps the per-bit loop, which
// saves and restores exactly the same registers.
const uint32_t lowest = mask_ & (0u - mask_);
const uint32_t aboveRun = mask_ + lowest;
if ((aboveRun & (aboveRun - 1u)) == 0u) {
const unsigned first = PpcLowestSetBitIndex(mask_);
const unsigned end = aboveRun == 0u ? 32u : PpcLowestSetBitIndex(aboveRun);
firstSlot_ = static_cast<uint8_t>(first - 14u);
slotCount_ = static_cast<uint8_t>(end - first);
const size_t count = slotCount_;
for (size_t i = 0; i < count; ++i) {
saved_[firstSlot_ + i] = cpu_->fpr[first + i];
}
return;
}
for (size_t i = 0; i < saved_.size(); ++i) {
if ((mask_ & (1u << (14 + i))) == 0) {
continue;
}
saved_[i] = cpu_->fpr[14 + i];
}
}
~PpcNonvolatileFprGuard() noexcept {
if (!cpu_ || mask_ == 0) {
return;
}
if (slotCount_ != 0) {
const unsigned first = 14u + firstSlot_;
const size_t count = slotCount_;
for (size_t i = 0; i < count; ++i) {
cpu_->fpr[first + i] = saved_[firstSlot_ + i];
}
return;
}
for (size_t i = 0; i < saved_.size(); ++i) {
if ((mask_ & (1u << (14 + i))) == 0) {
continue;
}
cpu_->fpr[14 + i] = saved_[i];
}
}
PpcNonvolatileFprGuard(const PpcNonvolatileFprGuard&) = delete;
PpcNonvolatileFprGuard& operator=(const PpcNonvolatileFprGuard&) = delete;
private:
CpuContext* cpu_ = nullptr;
uint32_t mask_ = 0;
// Contiguous-run description of mask_. slotCount_ == 0 means the mask is not
// a single run, so the destructor mirrors the constructor's per-bit loop.
uint8_t firstSlot_ = 0;
uint8_t slotCount_ = 0;
// Every element selected by mask_ is written before it is read. Leaving the
// remaining slots uninitialized avoids zero-filling all 18 values around a
// call that may preserve only one or two architectural FPRs.
std::array<PPC_FPR, 18> saved_;
};
class PpcNonvolatileGprGuard {
public:
explicit PpcNonvolatileGprGuard(CpuContext* cpu, bool enabled = true) noexcept
: cpu_(enabled ? cpu : nullptr) {
if (!cpu_) {
return;
}
for (size_t i = 0; i < saved_.size(); ++i) {
saved_[i] = cpu_->gpr[14 + i];
}
}
~PpcNonvolatileGprGuard() noexcept {
if (!cpu_) {
return;
}
for (size_t i = 0; i < saved_.size(); ++i) {
cpu_->gpr[14 + i] = saved_[i];
}
}
PpcNonvolatileGprGuard(const PpcNonvolatileGprGuard&) = delete;
PpcNonvolatileGprGuard& operator=(const PpcNonvolatileGprGuard&) = delete;
private:
CpuContext* cpu_ = nullptr;
// Left uninitialized: enabled writes all 18 slots before reading them, disabled reads none,
// so skipping the zero-fill saves 72 bytes of stack work on the common disabled hot path.
std::array<uint32_t, 18> saved_;
};
// Guest address of OSLoadContext, which models rfi-style context restoration
// and intentionally replaces the full guest register file instead of returning
// like a normal ABI call. It can never be guarded.
inline constexpr uint32_t kOSLoadContextAddress = 0x801A1F58u;
inline bool ShouldPreserveNonvolatileGprsForRawCpuCall(const TranslatedFunctionInfo* info) noexcept {
return info->kind == FunctionKind::Native && info->address != kOSLoadContextAddress;
}
inline uint32_t NonvolatileFprGuardMaskFor(const TranslatedFunctionInfo* info) noexcept {
if (!info || info->nonvolatileFprWriteMask == 0) {
return 0;
}
return info->nonvolatileFprWriteMask & kPpcAllNonvolatileFprMask;
}
template <uint32_t Target>
struct KnownTranslatedCpuCall {
static constexpr bool kAvailable = false;
static constexpr uint32_t kNonvolatileFprWriteMask = kPpcAllNonvolatileFprMask;
static constexpr bool kMustRemainDynamicallyDispatchable = true;
static constexpr void (*Entry)(CpuContext*) = nullptr;
};
// Translator-emitted trait specializations, one macro shared by every generated shard.
// `addr` is the 8-digit hex entry point, `winner` the resolved symbol (mods/Retro Rewind can
// publish their own name). A specialization only exists for a single statically bound winner,
// so overridable/dynamically-dispatchable are hardcoded here rather than passed in.
#define MKW_TRANSLATED_TRAIT(addr, winner, nonvolatile_fpr_write_mask) \
extern "C" void winner(CpuContext* ctx); \
template <> \
struct KnownTranslatedCpuCall<0x##addr##u> { \
static constexpr bool kAvailable = true; \
static constexpr uint32_t kNonvolatileFprWriteMask = nonvolatile_fpr_write_mask; \
static constexpr bool kMustRemainDynamicallyDispatchable = false; \
static constexpr void (*Entry)(CpuContext*) = &winner; \
}
template <uint32_t Target>
struct KnownNativeCpuCall {
static constexpr bool kAvailable = false;
static constexpr uint32_t kNonvolatileFprWriteMask = kPpcAllNonvolatileFprMask;
static constexpr void (*Entry)(CpuContext*) = nullptr;
};
template <uint32_t Target>
struct KnownTypedNativeCpuCall {
static constexpr bool kAvailable = false;
};
#include "native_cpu_calls.inc"
// Direct-mapped thread-local memo in front of the sorted registry lookup every bctrl performs;
// a miss just re-runs the sorted lookup. 512 entries (12 KiB) covers the per-frame indirect
// working set while staying L1/L2 resident, unlike 4096 which would thrash L2. Must stay a
// power of two, the index masks with (size - 1).
inline constexpr size_t kIndirectDispatchCacheEntries = 512;
// Namespace-scope `inline thread_local`, not function-local `static thread_local`, to avoid a
// thread-static init epoch check on every bctrl (same as g_currentCpuContext in ppc_runtime.h).
struct IndirectResolvedDispatchMemoEntry {
bool valid;
uint32_t address;
const TranslatedFunctionInfo* info;
};
inline thread_local IndirectResolvedDispatchMemoEntry
g_indirectResolvedDispatchMemo[kIndirectDispatchCacheEntries]{};
struct IndirectRawDispatchMemoEntry {
bool valid;
uint32_t address;
const RawDispatchRecord* record;
};
inline thread_local IndirectRawDispatchMemoEntry
g_indirectRawDispatchMemo[kIndirectDispatchCacheEntries]{};
class TranslatedFunctionRegistry {
public:
static void Register(TranslatedFunctionInfo info);
static void Finalize();
static inline bool IsLookupPublished() noexcept {
return lookupPublished_.load(std::memory_order_acquire);
}
static std::optional<TranslatedFunctionInfo> FindNearestByAddress(uint32_t address);
static inline const TranslatedFunctionInfo* FindByAddressPtr(uint32_t address) {
if (!lookupPublished_.load(std::memory_order_acquire)) {
return FindByAddressPtrSlow(address);
}
auto& cached = g_indirectResolvedDispatchMemo[
((address >> 2) * 2654435761u) & (kIndirectDispatchCacheEntries - 1)];
if (cached.valid && cached.address == address) {
return cached.info;
}
const auto* info = FindByAddressPtrSlow(address);
cached = {true, address, info};
return info;
}
// Hot wrapper: only the direct-mapped memo probe stays inline. Inlining the
// generated-table binary search alongside it pushed the whole function past
// the inliner's budget and out-of-lined the probe itself, which is the part
// every bctrl executes.
static MKW_PPC_FORCE_INLINE const RawDispatchRecord* FindRawByAddressPtr(uint32_t address) {
if (!lookupPublished_.load(std::memory_order_acquire)) {
return nullptr;
}
auto& cached = g_indirectRawDispatchMemo[
((address >> 2) * 2654435761u) & (kIndirectDispatchCacheEntries - 1)];
if (cached.valid && cached.address == address) {
return cached.record;
}
return FindRawByAddressPtrMiss(address, cached);
}
// Look up a registered function by host (native) address.
// This enables stack trace symbolication for translated functions.
static std::optional<TranslatedFunctionInfo> FindByHostAddress(uintptr_t hostAddr);
private:
// Memo miss: resolve against the header-inline generated table first; only dynamically
// registered records or a not-yet-published table fall through to the slow path.
static MKW_PPC_NO_INLINE MKW_PPC_COLD const RawDispatchRecord* FindRawByAddressPtrMiss(
uint32_t address, IndirectRawDispatchMemoEntry& cached) {
const RawDispatchRecord* record = nullptr;
if (const auto* table =
g_publishedStaticIndirectDispatchTable.load(std::memory_order_acquire)) {
record = FindStaticIndirectDispatchEntry(table, address);
}
if (record == nullptr) {
record = FindRawByAddressPtrSlow(address);
}
cached = {true, address, record};
return record;
}
static const TranslatedFunctionInfo* FindByAddressPtrSlow(uint32_t address);
static const RawDispatchRecord* FindRawByAddressPtrSlow(uint32_t address);
// Finalize publishes immutable registry, generated-table, and dynamic raw
// dispatch storage. Header-inlined cache hits are enabled only after that
// release publication, so a pre-finalization miss can never poison them.
inline static std::atomic_bool lookupPublished_{false};
};
// Cold, profile-owned registration data is emitted in compact table shards.
// Keeping it out of generated function bodies avoids one static constructor
// per translated function.
struct BulkTranslatedFunctionRecord {
uint32_t address;
const char* name;
void (*entry)(CpuContext*);
FunctionKind kind;
bool preservesNonvolatileFprs;
uint32_t nonvolatileFprWriteMask;
uint32_t priority;
uint64_t moduleId;
bool mustRemainDynamicallyDispatchable;
};
void RegisterBulkTranslatedFunctions(const BulkTranslatedFunctionRecord* records, size_t count);
class BulkTranslatedFunctionRegistrar {
public:
BulkTranslatedFunctionRegistrar(const BulkTranslatedFunctionRecord* records, size_t count) {
RegisterBulkTranslatedFunctions(records, count);
}
};
MKW_PPC_FORCE_INLINE bool TryDispatchResolvedCpuTarget(const TranslatedFunctionInfo* info, CpuContext* cpu) {
if (!info || !info->rawCpuInvoker) {
return false;
}
RecompMod::ScopedTranslatedExecutionAddress translatedExecution(info->address);
PpcNonvolatileFprGuard fprGuard(cpu, NonvolatileFprGuardMaskFor(info));
PpcNonvolatileGprGuard gprGuard(cpu, ShouldPreserveNonvolatileGprsForRawCpuCall(info));
if (TryGetCpuContext() != cpu) {
CpuContextScope scope(cpu);
info->rawCpuInvoker(cpu);
return true;
}
info->rawCpuInvoker(cpu);
return true;
}
inline bool TryDispatchRawCpuTarget(const RawDispatchRecord* record, CpuContext* cpu) {
if (!record || !record->entry) {
return false;
}
RecompMod::ScopedTranslatedExecutionAddress translatedExecution(record->address);
PpcNonvolatileGprGuard gprGuard(cpu, record->preserveNonvolatileGprs);
PpcNonvolatileFprGuard fprGuard(cpu, record->nonvolatileFprWriteMask);
if (TryGetCpuContext() != cpu) {
CpuContextScope scope(cpu);
record->entry(cpu);
} else {
record->entry(cpu);
}
return true;
}
template <uint32_t Target>
inline void DispatchKnownTranslatedCpuTargetStatic(CpuContext* cpu) {
const auto invokeKnownTranslated = [&]() {
KnownTranslatedCpuCall<Target>::Entry(cpu);
};
// A statically resolved, non-overridable translated call already running
// in this CpuContext needs no registry, context, or diagnostic target
// transition. This is the normal generated-to-generated hot path.
if (TryGetCpuContext() == cpu) {
if constexpr (KnownTranslatedCpuCall<Target>::kNonvolatileFprWriteMask == 0) {
invokeKnownTranslated();
return;
} else {
PpcNonvolatileFprGuard fprGuard(
cpu, KnownTranslatedCpuCall<Target>::kNonvolatileFprWriteMask);
invokeKnownTranslated();
return;
}
}
CpuContextScope contextScope(cpu);
RecompMod::ScopedTranslatedExecutionAddress translatedExecution(Target);
if constexpr (KnownTranslatedCpuCall<Target>::kNonvolatileFprWriteMask == 0) {
invokeKnownTranslated();
return;
} else {
PpcNonvolatileFprGuard fprGuard(cpu, KnownTranslatedCpuCall<Target>::kNonvolatileFprWriteMask);
invokeKnownTranslated();
}
}
[[noreturn]] inline void ReportMissingCpuTarget(uint32_t target, CpuContext* cpu) {
// Centralized crash path: stderr diagnostics, heuristics, crash artifacts
// in the run's log folder, popup, exit.
RuntimeCrash::FatalMissingGuestTarget(target, cpu);
}
template <uint32_t Target>
inline const TranslatedFunctionInfo* ResolveDirectCpuTargetInfo() {
return TranslatedFunctionRegistry::FindByAddressPtr(Target);
}
template <uint32_t Target>
inline const TranslatedFunctionInfo* ResolveDirectCpuTargetInfoCached() {
// Only latch a result once the registry is published (immutable for the process lifetime);
// a pre-finalization miss or provisional winner must never be cached. 0 = not yet resolved,
// 1 = negative-cache sentinel, avoiding a second ready flag on the hot path.
static std::atomic<uintptr_t> cachedValue{0};
constexpr uintptr_t kNegativeCache = 1;
const uintptr_t cached = cachedValue.load(std::memory_order_acquire);
if (cached != 0) {
return cached == kNegativeCache
? nullptr
: reinterpret_cast<const TranslatedFunctionInfo*>(cached);
}
if (!TranslatedFunctionRegistry::IsLookupPublished()) {
return ResolveDirectCpuTargetInfo<Target>();
}
const auto* info = ResolveDirectCpuTargetInfo<Target>();
cachedValue.store(info != nullptr
? reinterpret_cast<uintptr_t>(info)
: kNegativeCache,
std::memory_order_release);
return info;
}
// Caches the "is the published winner still base translation?" verdict for state-free fast
// paths guarding tens of thousands of call sites. Same latching rule as above: 0 = unresolved,
// 1 = negative, 2 = positive, and a verdict computed pre-publication is never stored.
template <uint32_t Target>
inline bool IsBaseTranslatedCpuTargetActive() {
static std::atomic<uint8_t> cachedVerdict{0};
constexpr uint8_t kNegativeVerdict = 1;
constexpr uint8_t kPositiveVerdict = 2;
const uint8_t cached = cachedVerdict.load(std::memory_order_acquire);
if (cached != 0) {
return cached == kPositiveVerdict;
}
const bool publishable = TranslatedFunctionRegistry::IsLookupPublished();
const auto* info = ResolveDirectCpuTargetInfo<Target>();
const bool active = info != nullptr && info->kind == FunctionKind::BaseTranslated;
if (publishable) {
cachedVerdict.store(active ? kPositiveVerdict : kNegativeVerdict,
std::memory_order_release);
}
return active;
}
template <uint32_t Target>
inline void InvokeDirectCpu(CpuContext* ctx) {
static_assert(Target != 0, "InvokeDirectCpu cannot target address 0");
CpuContext* cpu = ctx ? ctx : &GetPersistentCpuContext();
ApplyRuntimeCallOptions(Target, cpu);
if constexpr (KnownNativeCpuCall<Target>::kAvailable) {
const auto invokeKnownNative = [&]() {
PpcNonvolatileGprGuard gprGuard(cpu);
if (TryGetCpuContext() != cpu) {
CpuContextScope scope(cpu);
KnownNativeCpuCall<Target>::Entry(cpu);
return;
}
KnownNativeCpuCall<Target>::Entry(cpu);
};
if constexpr (KnownNativeCpuCall<Target>::kNonvolatileFprWriteMask == 0) {
invokeKnownNative();
} else {
PpcNonvolatileFprGuard fprGuard(cpu, KnownNativeCpuCall<Target>::kNonvolatileFprWriteMask);
invokeKnownNative();
}
return;
}
if constexpr (KnownTypedNativeCpuCall<Target>::kAvailable) {
PpcNonvolatileGprGuard gprGuard(cpu);
if (TryGetCpuContext() != cpu) {
CpuContextScope scope(cpu);
KnownTypedNativeCpuCall<Target>::Invoke(cpu);
} else {
KnownTypedNativeCpuCall<Target>::Invoke(cpu);
}
return;
}
if constexpr (KnownTranslatedCpuCall<Target>::kAvailable) {
DispatchKnownTranslatedCpuTargetStatic<Target>(cpu);
return;
}
const auto* registeredInfo = ResolveDirectCpuTargetInfoCached<Target>();
if (TryDispatchResolvedCpuTarget(registeredInfo, cpu)) {
return;
}
ReportMissingCpuTarget(Target, cpu);
}
// Indirect jump (bctr without link) - used for switch tables.
inline void InvokeIndirectJump(uint32_t target, CpuContext* ctx) {
// Use the caller's context, not the persistent global context!
// This is essential for tail calls via bctr where the 'this' pointer (r3)
// must be passed correctly to the target function.
CpuContext* cpu = ctx ? ctx : &GetPersistentCpuContext();
if (TryDispatchRawCpuTarget(TranslatedFunctionRegistry::FindRawByAddressPtr(target), cpu)) {
return;
}
// Cold fallback for dynamically registered or signature-only targets.
const auto* info = TranslatedFunctionRegistry::FindByAddressPtr(target);
if (info) {
if (TryDispatchResolvedCpuTarget(info, cpu)) {
return;
}
}
// If not registered, this is likely a switch table jump to an intra-function label.
// The translator should have recognized this pattern and generated proper switch code.
RT_LOG(RT_TAG_RUNTIME) << "InvokeIndirectJump: target 0x"
<< std::hex << target << std::dec
<< " is not a registered function.\n"
<< "This is likely a switch statement jump table that the translator\n"
<< "failed to recognize. The function containing this bctr needs\n"
<< "proper switch pattern recognition." << std::endl;
RT_LOG(RT_TAG_RUNTIME) << "Indirect jump context: pc=0x" << std::hex << cpu->pc
<< " lr=0x" << cpu->lr << " ctr=0x" << cpu->ctr
<< " r1=0x" << cpu->gpr[1] << " r3=0x" << cpu->gpr[3]
<< " r29=0x" << cpu->gpr[29] << " r30=0x" << cpu->gpr[30]
<< " r31=0x" << cpu->gpr[31] << std::dec << std::endl;
DumpHostStackTraceForRuntimeHelper();
std::fflush(stderr);
std::ostringstream message;
message << "The game stopped because an indirect jump targeted guest address 0x"
<< std::hex << target
<< ", but that address is not a registered translated function.\n\n"
<< "The translator may have missed a switch/jump-table target.";
// Same artifact set as RuntimeCrash::FatalMissingGuestTarget, which is this
// path's sibling for indirect *calls*: MarkFatalErrorReported below stops the
// atexit reporter, so the crash log has to be written here.
RuntimeCrash::WriteCrashArtifacts("missing_jump_target", message.str(), &target);
SetRuntimeExitCode(EXIT_FAILURE);
ShowRuntimeFatalPopup("Missing indirect jump target", message.str());
MarkFatalErrorReported();
std::exit(EXIT_FAILURE);
}
inline void InvokeIndirectCpu(uint32_t target, CpuContext* ctx) {
CpuContext* cpu = ctx ? ctx : &GetPersistentCpuContext();
if (target == 0) {
ReportMissingCpuTarget(target, cpu);
}
ApplyRuntimeCallOptions(target, cpu);
if (TryDispatchRawCpuTarget(TranslatedFunctionRegistry::FindRawByAddressPtr(target), cpu)) {
return;
}
if (TryDispatchResolvedCpuTarget(TranslatedFunctionRegistry::FindByAddressPtr(target), cpu)) {
return;
}
ReportMissingCpuTarget(target, cpu);
}
template <typename Signature>
class AbiTrampoline;
template <typename Ret, typename... Args>
class AbiTrampoline<Ret(Args...)> {
public:
using TargetFn = Ret (*)(Args...);
AbiTrampoline(uint32_t address,
const char* name,
TargetFn target,
FunctionKind kind = FunctionKind::BaseTranslated,
bool preservesNonvolatileFprs = false,
uint32_t nonvolatileFprWriteMask = kPpcAllNonvolatileFprMask,
uint32_t priority = 0,
uint64_t moduleId = 0,
void (*rawCpuInvoker)(CpuContext*) = nullptr,
bool mustRemainDynamicallyDispatchable = true)
{
TranslatedFunctionInfo info;
info.address = address;
info.name = name ? name : "";
info.moduleId = moduleId;
info.priority = priority;
const bool effectivePreservesNonvolatileFprs =
preservesNonvolatileFprs || kind == FunctionKind::Native;
info.nonvolatileFprWriteMask = effectivePreservesNonvolatileFprs ? 0 : (nonvolatileFprWriteMask & kPpcAllNonvolatileFprMask);
info.entryPoint = reinterpret_cast<void*>(target);
info.rawCpuInvoker = rawCpuInvoker;
info.mustRemainDynamicallyDispatchable = mustRemainDynamicallyDispatchable;
info.kind = kind;
TranslatedFunctionRegistry::Register(std::move(info));
}
static std::tuple<Args...> BuildArgs(CpuContext* cpu) {
size_t gprIndex = 0;
size_t fprIndex = 0;
return std::tuple<Args...>{LoadArgument<Args>(cpu, gprIndex, fprIndex)...};
}
template <typename T>
static std::remove_reference_t<T> LoadArgument(CpuContext* cpu, size_t& gprIndex, size_t& fprIndex) {
using CleanT = std::remove_reference_t<T>;
if constexpr (std::is_floating_point_v<CleanT>) {
if (fprIndex >= 13) {
throw std::out_of_range("FPR argument overflow");
}
return static_cast<CleanT>(cpu->fpr[1 + fprIndex++].d);
} else {
if (gprIndex >= 8) {
size_t stackArgIndex = gprIndex - 8;
uint32_t sp = cpu->gpr[1];
uint32_t argAddr = sp + 8 + static_cast<uint32_t>(stackArgIndex * 4);
gprIndex++;
try {
return static_cast<CleanT>(Memory::Read32(argAddr));
} catch (const Memory::AccessViolation&) {
RT_LOG(RT_TAG_RUNTIME) << "Stack parameter read failed at 0x" << std::hex << argAddr
<< " (SP=0x" << sp << ", argIndex=" << std::dec << (gprIndex - 1) << ")" << std::endl;
throw;
}
}
return static_cast<CleanT>(cpu->gpr[3 + gprIndex++]);
}
}
static void InvokeCpu(TargetFn target, CpuContext* cpu) {
auto args = BuildArgs(cpu);
if constexpr (std::is_void_v<Ret>) {
std::apply(target, args);
} else {
auto result = std::apply(target, args);
if constexpr (std::is_floating_point_v<Ret>) {
cpu->fpr[1].d = static_cast<double>(result);
} else {
cpu->gpr[3] = static_cast<uint32_t>(result);
}
}
}
};
template <auto Target>
struct AbiRawCpuThunk {
static void Invoke(CpuContext* cpu) {
if constexpr (std::is_invocable_v<decltype(Target), CpuContext*>) {
(void)Target(cpu);
} else {
AbiTrampoline<std::remove_pointer_t<decltype(Target)>>::InvokeCpu(Target, cpu);
}
}
};
template <typename Ret>
class AbiTrampoline<Ret(CpuContext*)> {
public:
using TargetFn = Ret (*)(CpuContext*);
AbiTrampoline(uint32_t address,
const char* name,
TargetFn target,
FunctionKind kind = FunctionKind::BaseTranslated,
bool preservesNonvolatileFprs = false,
uint32_t nonvolatileFprWriteMask = kPpcAllNonvolatileFprMask,
uint32_t priority = 0,
uint64_t moduleId = 0,
void (*rawCpuInvoker)(CpuContext*) = nullptr,
bool mustRemainDynamicallyDispatchable = true)
{
TranslatedFunctionInfo info;
info.address = address;
info.name = name ? name : "";
info.moduleId = moduleId;
info.priority = priority;
// Deliberately *not* mirroring the typed specialization's
// `|| kind == FunctionKind::Native`. A CpuContext* native receives the
// whole guest register file and can legitimately write f14-f31 through
// it, so it stays conservative until the individual address has been
// measured clean and its registration opts out below.
info.nonvolatileFprWriteMask = preservesNonvolatileFprs ? 0 : (nonvolatileFprWriteMask & kPpcAllNonvolatileFprMask);
info.entryPoint = reinterpret_cast<void*>(target);
info.rawCpuInvoker = rawCpuInvoker ? rawCpuInvoker : reinterpret_cast<void (*)(CpuContext*)>(target);
info.mustRemainDynamicallyDispatchable = mustRemainDynamicallyDispatchable;
info.kind = kind;
TranslatedFunctionRegistry::Register(std::move(info));
}
};
#define MKW_DETAIL_CAT(a, b) a##b
#define MKW_DETAIL_MAKE_UNIQUE(a, b) MKW_DETAIL_CAT(a, b)
// Three hand-written registration macros. Generated code never registers per function; every
// translated function reaches the registry via the bulk BulkTranslatedFunctionRecord tables
// in the translator's *_registration TUs.
#define REGISTER_TRANSLATED_FUNCTION(address, fn) \
static AbiTrampoline<decltype(fn)> MKW_DETAIL_MAKE_UNIQUE(_abi_trampoline_, __COUNTER__)(address, #fn, fn, FunctionKind::BaseTranslated, false, kPpcAllNonvolatileFprMask, 0, 0, nullptr, KnownTranslatedCpuCall<address>::kMustRemainDynamicallyDispatchable)
#define REGISTER_NATIVE_FUNCTION(address, fn) \
static AbiTrampoline<decltype(fn)> MKW_DETAIL_MAKE_UNIQUE(_abi_native_trampoline_, __COUNTER__)(address, #fn, fn, FunctionKind::Native, false, kPpcAllNonvolatileFprMask, 0, 0, &AbiRawCpuThunk<&fn>::Invoke)
#define REGISTER_NATIVE_FUNCTION_AS(address, fn, pretty_name) \
static AbiTrampoline<decltype(fn)> MKW_DETAIL_MAKE_UNIQUE(_abi_native_trampoline_named_, __COUNTER__)(address, pretty_name, fn, FunctionKind::Native, false, kPpcAllNonvolatileFprMask, 0, 0, &AbiRawCpuThunk<&fn>::Invoke)
+48
View File
@@ -0,0 +1,48 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <mutex>
#include <vector>
#include <SDL3/SDL_audio.h>
class AudioBackend {
public:
static AudioBackend& Instance();
bool Init(uint32_t sampleRate, uint32_t channels);
void Shutdown();
// Wii AI DMA frames are big-endian and ordered right, left. SDL expects
// native-endian interleaved left, right samples.
bool PushWiiAiSamplesBE16(const uint8_t* data, size_t bytes);
bool PushSamplesLE16(const int16_t* samples, size_t sampleCount);
// Applied to the final host output, covering both AX and direct AI DMA.
void SetMasterVolume(float volume);
void SetMuted(bool muted);
private:
AudioBackend() = default;
~AudioBackend() = default;
AudioBackend(const AudioBackend&) = delete;
AudioBackend& operator=(const AudioBackend&) = delete;
bool EnsureInitializedLocked(uint32_t sampleRate, uint32_t channels);
bool QueueHasCapacityLocked(int incomingBytes);
uint32_t QueueLimitBytesLocked() const;
float EffectiveGainLocked() const;
void ApplyGainLocked();
mutable std::mutex m_mutex;
SDL_AudioStream* m_stream = nullptr;
SDL_AudioSpec m_spec{};
uint32_t m_sampleRate = 0;
uint32_t m_channels = 0;
bool m_initialized = false;
float m_masterVolume = 1.0f;
bool m_muted = false;
bool m_reportedDroppedBlock = false;
std::vector<int16_t> m_convertBuffer;
};
+142
View File
@@ -0,0 +1,142 @@
#pragma once
#include "settings_overlay.h"
#include "runtime_config.h"
#include <aurora/aurora.h>
#include <aurora/event.h>
#include <dolphin/gx/GXAurora.h>
#include <cstdlib>
#include <atomic>
#include <chrono>
#if defined(_WIN32)
#include <windows.h>
#endif
extern "C" bool g_dynamicAspectRatioEnabled;
void ConfigureMkwDynamicAspect(bool widescreen, uint32_t surfaceWidth, uint32_t surfaceHeight);
void UpdateMkwDynamicAspectSurface(uint32_t surfaceWidth, uint32_t surfaceHeight);
// Arms the "keep EGG::Frustum's projection scale" flag on every screen that
// renders to a fixed-size offscreen target. Cheap and idempotent; called from
// the GX viewport path so it beats bakes that never cross a frame boundary.
void AssertMkwOffscreenScreenBypass();
inline std::atomic_bool g_mkwDynamicAspectSurfacePending{false};
namespace WindowPlacementPersistence {
inline bool sizeDirty = false;
inline bool positionDirty = false;
inline uint32_t width = 0;
inline uint32_t height = 0;
inline int32_t x = 0;
inline int32_t y = 0;
inline std::chrono::steady_clock::time_point changedAt{};
inline void Flush(bool force = false) {
if (!sizeDirty && !positionDirty) {
return;
}
constexpr auto kSaveDelay = std::chrono::milliseconds(300);
if (!force && std::chrono::steady_clock::now() - changedAt < kSaveDelay) {
return;
}
if (sizeDirty && width != 0 && height != 0) {
RuntimeConfigFile::SetWindowSize(width, height);
}
if (positionDirty) {
RuntimeConfigFile::SetWindowPosition(x, y);
}
sizeDirty = false;
positionDirty = false;
}
} // namespace WindowPlacementPersistence
// The close event can be consumed while execution is inside a guest fiber
// (for example, OSSleepThread). Running normal C++/CRT shutdown from that
// fiber re-enters runtime teardown and can fault while the fiber machinery is
// still active. A window close is an intentional successful exit, so end the
// process directly and do not run the crash/atexit paths.
[[noreturn]] inline void ExitForAuroraWindowClose() noexcept {
WindowPlacementPersistence::Flush(true);
#if defined(_WIN32)
::ExitProcess(0);
#else
std::_Exit(EXIT_SUCCESS);
#endif
}
// Update cached Aurora window/framebuffer dimensions based on pending events.
inline void ProcessAuroraEvents(const AuroraEvent* events) {
if (!events) {
return;
}
bool surfaceChanged = false;
for (const AuroraEvent* event = events; event->type != AURORA_NONE; ++event) {
switch (event->type) {
case AURORA_WINDOW_MOVED:
if (aurora_get_display_mode() == AURORA_DISPLAY_MODE_WINDOWED) {
WindowPlacementPersistence::x = event->windowPos.x;
WindowPlacementPersistence::y = event->windowPos.y;
WindowPlacementPersistence::positionDirty = true;
WindowPlacementPersistence::changedAt = std::chrono::steady_clock::now();
}
break;
case AURORA_WINDOW_RESIZED:
if (aurora_get_display_mode() == AURORA_DISPLAY_MODE_WINDOWED &&
event->windowSize.width != 0 && event->windowSize.height != 0) {
WindowPlacementPersistence::width = event->windowSize.width;
WindowPlacementPersistence::height = event->windowSize.height;
WindowPlacementPersistence::sizeDirty = true;
WindowPlacementPersistence::changedAt = std::chrono::steady_clock::now();
}
surfaceChanged = true;
break;
case AURORA_DISPLAY_SCALE_CHANGED:
surfaceChanged = true;
break;
case AURORA_EXIT:
ExitForAuroraWindowClose();
default:
break;
}
}
WindowPlacementPersistence::Flush();
if (surfaceChanged) {
// Applying the viewport policy drains GX. Event dispatch can run
// between asynchronous end_frame and the next begin_frame, while the
// frame worker is intentionally waiting for begin permission. Joining
// it here creates a circular wait. Record the newest native size and
// apply it immediately after the next frame has been prepared.
g_mkwDynamicAspectSurfacePending.store(true, std::memory_order_release);
}
settings_overlay::HandleEvents(events);
}
inline void ApplyPendingMkwDynamicAspectSurface() {
// The OS can adjust a window without a resize event reaching the queue
// (observed with hidden windows clamped to the work area), so re-read the
// surface at every frame boundary instead of only on queued events.
// UpdateMkwDynamicAspectSurface is idempotent and cheap for a stable size.
(void)g_mkwDynamicAspectSurfacePending.exchange(false, std::memory_order_acq_rel);
uint32_t surfaceWidth = 0;
uint32_t surfaceHeight = 0;
AuroraGetSurfaceSize(&surfaceWidth, &surfaceHeight);
UpdateMkwDynamicAspectSurface(surfaceWidth, surfaceHeight);
}
inline bool BeginAuroraFrame() {
if (!aurora_begin_frame()) {
return false;
}
ApplyPendingMkwDynamicAspectSurface();
return true;
}
// Poll Aurora events and update cached window/framebuffer dimensions.
inline void UpdateAuroraAndProcessEvents() {
ProcessAuroraEvents(aurora_update());
}
+151
View File
@@ -0,0 +1,151 @@
#pragma once
#include "runtime_config.h"
#include <algorithm>
#include <array>
#include <cctype>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <optional>
#include <random>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
namespace RuntimeConsoleIdentity {
struct Identity {
std::string serial;
std::array<uint8_t, 6> mac;
};
inline bool IsValidSerial(const std::string& serial) {
return serial.size() == 9 &&
serial != "000000000" &&
std::all_of(serial.begin(), serial.end(),
[](unsigned char value) { return std::isdigit(value) != 0; });
}
inline Identity FromSerial(std::string serial) {
// Keep Nintendo's Wii OUI. The suffix is derived from the persisted serial
// so every API exposes one coherent, stable virtual-console identity.
uint32_t hash = 2166136261u;
for (const unsigned char value : serial) {
hash ^= value;
hash *= 16777619u;
}
uint32_t suffix = hash & 0x00FFFFFFu;
if (suffix == 0 || suffix == 0x00FFFFFFu) {
suffix ^= 0x005A17C3u;
}
return {
std::move(serial),
{
0x00,
0x09,
0xBF,
static_cast<uint8_t>(suffix >> 16),
static_cast<uint8_t>(suffix >> 8),
static_cast<uint8_t>(suffix),
},
};
}
inline std::optional<std::string> ReadSerial(const std::filesystem::path& path) {
std::ifstream input(path);
std::string line;
if (!input || !std::getline(input, line)) {
return std::nullopt;
}
constexpr std::string_view prefix = "serial=";
if (line.rfind(prefix, 0) != 0) {
return std::nullopt;
}
std::string serial = line.substr(prefix.size());
if (!IsValidSerial(serial)) {
return std::nullopt;
}
return serial;
}
inline bool WriteSerial(const std::filesystem::path& path, const std::string& serial) {
std::error_code ec;
std::filesystem::create_directories(path.parent_path(), ec);
if (ec) {
return false;
}
const std::filesystem::path temporary = path.string() + ".tmp";
{
std::ofstream output(temporary, std::ios::trunc);
if (!output) {
return false;
}
output << "serial=" << serial << '\n';
output.close();
if (!output) {
return false;
}
}
std::filesystem::rename(temporary, path, ec);
if (!ec) {
return true;
}
std::filesystem::remove(temporary, ec);
return false;
}
inline std::string GenerateSerial() {
std::random_device entropy;
std::seed_seq seed{
entropy(),
entropy(),
entropy(),
entropy(),
};
std::mt19937 generator(seed);
std::uniform_int_distribution<uint32_t> distribution(100000000u, 999999999u);
return std::to_string(distribution(generator));
}
inline Identity LoadOrCreate(const std::filesystem::path& path) {
if (const auto serial = ReadSerial(path)) {
return FromSerial(*serial);
}
const std::string generated = GenerateSerial();
if (WriteSerial(path, generated)) {
return FromSerial(generated);
}
// Remain operational in a read-only environment. This fallback matches
// Dolphin's deterministic serial while keeping the same valid identity shape.
return FromSerial("123456789");
}
inline const Identity& Current() {
static const Identity identity =
LoadOrCreate(RuntimeConfigFile::ApplicationDataDirectory() / "ConsoleIdentity.txt");
return identity;
}
inline std::string FormatMac(const std::array<uint8_t, 6>& mac) {
std::ostringstream output;
output << std::uppercase << std::hex << std::setfill('0');
for (size_t index = 0; index < mac.size(); ++index) {
if (index != 0) {
output << ':';
}
output << std::setw(2) << static_cast<unsigned>(mac[index]);
}
return output.str();
}
} // namespace RuntimeConsoleIdentity
+126
View File
@@ -0,0 +1,126 @@
#pragma once
#include <atomic>
#include <cstdint>
#include <functional>
#include <mutex>
#include <thread>
#include <unordered_map>
#if defined(_WIN32)
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#endif
#include "ppc_runtime.h"
// Forward declarations
struct CpuContext;
// GuestFiberManager: each guest OSThread maps to a Windows Fiber. A scheduler fiber picks
// which guest fiber runs; a real timer thread queues VI retraces at the VI cadence. Guest
// threads only switch at explicit yield points (OSSleepThread, OSYieldThread, ...), matching
// Wii cooperative semantics exactly.
namespace Fiber {
// Thread states matching guest OS
enum class ThreadState : uint32_t {
READY = 1,
RUNNING = 2,
WAITING = 4,
MORIBUND = 8,
};
// Information about a guest fiber
struct GuestFiber {
void* fiber = nullptr; // Windows fiber handle
uint32_t entryPoint = 0; // Thread entry function
uint32_t entryArg = 0; // Argument to entry function
CpuContext cpuContext{}; // Saved CPU context for this fiber
ThreadState state = ThreadState::READY;
bool isSchedulerFiber = false; // True for the main scheduler fiber
bool terminated = false; // Thread has exited
};
class GuestFiberManager {
public:
// Initialize the fiber system - must be called from main thread
static void Initialize();
// Shutdown the fiber system
static void Shutdown();
// Check if fiber system is initialized
static bool IsInitialized();
// Create a fiber for a guest thread (called from OSCreateThread HLE).
// OSCreateThread's stackSize/priority are not passed: the host fiber models
// neither, only the guest SP seeded from stackBase. Returns true on success.
static bool CreateGuestFiber(uint32_t guestThreadAddr, uint32_t entryPoint,
uint32_t entryArg, uint32_t stackBase);
// Resume a guest thread (called from OSResumeThread HLE)
// This marks the fiber as runnable
static void ResumeGuestThread(uint32_t guestThreadAddr);
// Suspend a guest thread (called from OSSuspendThread HLE)
static void SuspendGuestThread(uint32_t guestThreadAddr);
// Called when a guest thread exits or is canceled
static void ExitGuestThread(uint32_t guestThreadAddr, ThreadState finalState);
// Switch to a specific guest thread's fiber (called from OSLoadContext)
// This saves the current fiber's state and switches to the target
static void SwitchToThread(uint32_t guestThreadAddr, CpuContext* cpu);
// Get the currently running guest thread address (0 if in scheduler)
static uint32_t GetCurrentGuestThread();
// Get fiber info for a guest thread (may be null)
static GuestFiber* GetFiber(uint32_t guestThreadAddr);
// Check for VI retrace and process it (called from scheduler idle)
static void ProcessTimerEvents(CpuContext* cpu);
// Register the current host fiber as a guest thread fiber
// This is used for the default/main thread that exists before OSCreateThread
static bool RegisterMainThreadAsFiber(uint32_t guestThreadAddr, CpuContext* cpu);
// Check if a fiber exists for a guest thread
static bool HasFiber(uint32_t guestThreadAddr);
static bool IsTerminated(uint32_t guestThreadAddr);
private:
// The fiber entry point wrapper
#if defined(_WIN32)
static void CALLBACK FiberProc(void* param);
#else
static void FiberProc(void* param);
#endif
// Internal state
static std::mutex s_mutex;
static std::unordered_map<uint32_t, GuestFiber> s_fibers;
static std::vector<void*> s_fibersPendingDelete;
static void* s_schedulerFiber;
static uint32_t s_currentGuestThread;
static bool s_initialized;
// Stored CPU context pointer for fiber switches
static thread_local CpuContext* s_cpuContext;
static void PurgePendingFibers();
};
// Global pending retrace count. GuestFiberManager::ProcessTimerEvents drains
// this on the guest thread so VI callbacks still run with guest state/locking
// expectations.
extern std::atomic<uint32_t> g_viRetracePendingCount;
} // namespace Fiber
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include <atomic>
#include <cstdint>
namespace RuntimeGameGraphicsOptions {
inline std::atomic<uint32_t>& DisabledPostProcessingPathsState() noexcept {
static std::atomic<uint32_t> disabledMask{0};
return disabledMask;
}
inline uint32_t DisabledPostProcessingPaths() noexcept {
return DisabledPostProcessingPathsState().load(std::memory_order_relaxed);
}
inline void SetDisabledPostProcessingPaths(uint32_t disabledMask) noexcept {
DisabledPostProcessingPathsState().store(disabledMask, std::memory_order_relaxed);
}
inline uint32_t FilterScnRendererPathMask(uint32_t pathMask) noexcept {
return pathMask & ~(DisabledPostProcessingPaths() | 0x20u);
}
} // namespace RuntimeGameGraphicsOptions
+80
View File
@@ -0,0 +1,80 @@
#pragma once
// Flat 4 GiB guest address space: a guest access is just `*(T*)(kFlatGuestBase + addr)` plus a
// byte swap, no page-table load/branch. Two views alias the same physical memory: the GUEST
// view at kFlatGuestBase has page protections as the interception mechanism (unmapped/MMIO/
// executable/deferred-read pages are uncommitted or protected), while the HOST view is a plain
// alias native runtime code (image loading, DVD reads, HLE, GX) writes through unchecked.
#include <cstddef>
#include <cstdint>
#include <vector>
namespace GuestFlat {
// Fixed base so the emitted access is `[reg + imm64-in-register]` with no load
// of a global. 16 TiB: clear of the Windows ASan shadow (32 TiB) and of the
// usual image/heap placement.
inline constexpr uint64_t kGuestSpaceSize = 0x1'0000'0000ull;
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'1000'0000'0000ull;
#define MKW_FLAT_GUEST_BASE (reinterpret_cast<uint8_t*>(GuestFlat::kFixedFlatGuestBase))
enum class Backing {
Owned,
Mem1,
Mem2,
};
struct RegionRequest {
uint32_t base = 0;
uint64_t size = 0;
Backing backing = Backing::Owned;
};
struct FaultCounters {
uint32_t mmio = 0; // MMIO access that reached the handler
uint32_t efb = 0; // deferred (EFB) read materialized from a trap
uint32_t xguard = 0; // executable-page write trap
uint32_t unmapped = 0; // guest touches that landed outside every mapped region
uint32_t unmappedRegions = 0; // distinct 64 KiB blocks committed on demand
};
// True once the reservation exists and translated code may use the flat path.
bool IsActive();
// Reserves the 4 GiB space (once per process) and maps every requested region
// into both views. Throws std::runtime_error with a precise diagnosis when the
// reservation, the section objects or a view cannot be created - a silent
// fallback would let translated code read from an unmapped constant base.
void Initialize(const std::vector<RegionRequest>& regions);
// Host-view pointer for a mapped guest address, or nullptr when the address is
// outside every mapped region. This is what the page table and
// Memory::GetPointer hand out.
uint8_t* HostPointer(uint32_t guestAddress);
// Deferred (EFB) reads: the covered guest pages are made PAGE_NOACCESS in the
// guest view so a flat read traps and materializes the copy.
void ProtectDeferredRange(uint32_t address, size_t length);
void UnprotectDeferredRange(uint32_t address, size_t length);
// Executable-write guard: pages fully covered by a registered executable range
// become PAGE_READONLY in the guest view. Registration order does not matter -
// ranges registered before the mapping exists are re-applied by Initialize.
void RegisterExecutableRange(uint32_t start, uint32_t end);
FaultCounters Counters();
// End-of-run report for the counters above. An unmapped touch is a wild guest
// pointer whose block this module silently committed so execution could go on,
// which makes it invisible unless it is repeated at shutdown; a nonzero count
// is therefore reported as a warning. Idempotent, so every exit path (normal
// return, caught exception, abort handler) may call it.
void LogFaultSummary() noexcept;
// Returns true when the access violation was a guest-space fault this module
// resolved; the caller must then resume execution. `exceptionPointers` is a
// Windows EXCEPTION_POINTERS*.
bool HandleAccessViolation(void* exceptionPointers) noexcept;
} // namespace GuestFlat
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include "abi_bridge.h"
#include "ppc_runtime.h"
// Deferred HLE dispatch (VRetrace/alarm callbacks) runs mid-function on an arbitrary translated
// caller; using its live CpuContext would let callback exit state clobber resident locals (once
// wild-read a resident r29 via postVRetrace). This models hardware save/restore: callbacks run on
// a private copy seeded from the interrupted r1/r2/r13/FP mode, made ambient via CpuContextScope.
class GuestInterruptCallbackContext {
public:
GuestInterruptCallbackContext()
: registers_(InterruptedRegisters()), scope_(&registers_) {}
GuestInterruptCallbackContext(const GuestInterruptCallbackContext&) = delete;
GuestInterruptCallbackContext& operator=(const GuestInterruptCallbackContext&) = delete;
CpuContext* get() noexcept { return &registers_; }
private:
static CpuContext InterruptedRegisters() {
// The ambient context is the interrupted guest thread's file. Without
// one (host frame loop between fibers) the persistent context is the
// only meaningful seed for r1/r2/r13.
const CpuContext* interrupted = TryGetCpuContext();
return interrupted != nullptr ? *interrupted : GetPersistentCpuContext();
}
CpuContext registers_;
CpuContextScope scope_;
};
+135
View File
@@ -0,0 +1,135 @@
#pragma once
// Per-granule write-generation counters let GX cache consumers skip re-digesting unchanged
// ranges. Assumes GPU visibility only happens via DCStoreRange/DCFlushRange/DCInvalidateRange,
// so hooking those plus DMA writers is a complete notification. Header-only: hot, must not
// link-depend on the GX HLE.
#include "memory.h"
#include <array>
#include <atomic>
#include <cstddef>
#include <cstdint>
// Canonical MEM1/MEM2 physical address for any of the guest's cached, uncached
// or physical aliases. Lives here rather than in the GX HLE so the tracking
// table, its notifiers and the GX caches all agree on one address space.
inline uint32_t CanonicalizeGxMainRamAddress(uint32_t addr) noexcept {
if (addr < 0x01800000u) {
return addr;
}
if (addr >= Memory::kMem2PhysicalBase && addr < Memory::kMem2PhysicalEnd) {
return addr;
}
if (addr >= 0x80000000u && addr < 0x81800000u) {
return addr - 0x80000000u;
}
if (addr >= Memory::kMem2CachedBase && addr < Memory::kMem2CachedEnd) {
return addr - 0x80000000u;
}
if (addr >= 0xC0000000u && addr < 0xC1800000u) {
return addr - 0xC0000000u;
}
if (addr >= Memory::kMem2UncachedBase && addr < Memory::kMem2UncachedEnd) {
return addr - 0xC0000000u;
}
return addr;
}
namespace GxGuestWrite {
// Granularity matches the GX resource caches' occupancy maps: 64 KiB, so a
// cacheable display list or texture spans only a handful of counters. A false
// bump only costs one re-digest, which is exactly the untracked behaviour.
inline constexpr uint32_t kGranuleShift = 16; // 64 KiB per granule
inline constexpr uint64_t kTrackedSpan = static_cast<uint64_t>(Memory::kMem2PhysicalEnd);
inline constexpr size_t kGranuleCount = static_cast<size_t>(kTrackedSpan >> kGranuleShift);
// Folded value used for "this range is not covered by the granule map", which
// forces the consumer to recompute its digest on every call.
inline constexpr uint64_t kUntracked = ~0ull;
// Shared (not thread_local) because notifications come from the OS HLE and from
// aurora's readback callbacks while the caches themselves are per guest thread;
// relaxed ordering is enough, every access is a plain load or a lock-xadd on
// x86 and a missed-by-a-hair ordering only delays a re-digest by one call.
inline std::array<std::atomic<uint32_t>, kGranuleCount> g_generations{};
// Monotone fold of every granule counter covering [addr, addr + nbytes).
// Counters only ever increase, so the sum changes whenever any covered granule
// is bumped and can never alias back to a previously observed value.
inline uint64_t GenerationForRange(uint32_t addr, uint32_t nbytes) noexcept {
if (nbytes == 0) {
return kUntracked;
}
const uint64_t start = static_cast<uint64_t>(CanonicalizeGxMainRamAddress(addr));
const uint64_t end = start + static_cast<uint64_t>(nbytes);
if (end <= start || end > kTrackedSpan) {
return kUntracked;
}
const size_t firstGranule = static_cast<size_t>(start >> kGranuleShift);
const size_t lastGranule = static_cast<size_t>((end - 1) >> kGranuleShift);
uint64_t folded = 0;
for (size_t granule = firstGranule; granule <= lastGranule; ++granule) {
folded += g_generations[granule].load(std::memory_order_relaxed);
}
// Never collide with the "untracked" sentinel.
return folded == kUntracked ? folded - 1u : folded;
}
// Bumps every granule covering [addr, addr + size). Deliberately branch-light:
// this runs on every DC range op, including the ones that only ever touch
// memory no GX cache has ever looked at.
inline void NotifyWrite(uint32_t addr, uint32_t size) noexcept {
if (size == 0) {
return;
}
const uint64_t start = static_cast<uint64_t>(CanonicalizeGxMainRamAddress(addr));
const uint64_t end = start + static_cast<uint64_t>(size);
if (end <= start || start >= kTrackedSpan) {
return;
}
const uint64_t clampedEnd = end < kTrackedSpan ? end : kTrackedSpan;
const size_t firstGranule = static_cast<size_t>(start >> kGranuleShift);
const size_t lastGranule = static_cast<size_t>((clampedEnd - 1) >> kGranuleShift);
for (size_t granule = firstGranule; granule <= lastGranule; ++granule) {
g_generations[granule].fetch_add(1u, std::memory_order_relaxed);
}
}
// The skip contract shared by every consumer: a stored digest may be trusted
// only when the range is tracked at all and no notification has landed on it
// since the digest was taken.
inline bool CanSkipDigest(uint64_t storedGeneration, uint64_t currentGeneration) noexcept {
return currentGeneration != kUntracked && storedGeneration == currentGeneration;
}
// Host-to-guest inversion for one contiguous guest RAM alias. Aurora holds host
// pointers only, so the generation and notification hooks it calls have to map
// them back before they can touch the table.
inline bool HostRangeToGuest(const void* hostBase, uint64_t hostSize, uint32_t guestBase,
const void* hostPtr, size_t size, uint32_t& outAddr) noexcept {
if (hostBase == nullptr || hostPtr == nullptr || size == 0) {
return false;
}
const uintptr_t base = reinterpret_cast<uintptr_t>(hostBase);
const uintptr_t ptr = reinterpret_cast<uintptr_t>(hostPtr);
if (ptr < base) {
return false;
}
const uint64_t offset = static_cast<uint64_t>(ptr - base);
if (offset >= hostSize || static_cast<uint64_t>(size) > hostSize - offset) {
return false;
}
outAddr = guestBase + static_cast<uint32_t>(offset);
return true;
}
// Installs the generation/notification hooks into aurora. Called once after
// aurora_initialize; until then (and if guest RAM cannot be resolved) aurora
// digests its source bytes on every validation, which is the old behaviour.
// Defined in gx_guest_write_hooks.cpp.
void InstallAuroraHooks();
} // namespace GxGuestWrite
@@ -0,0 +1,82 @@
#pragma once
#include "isa/big_endian.h"
#include <array>
#include <cstddef>
#include <cstdint>
namespace PadStatusContract {
inline constexpr std::size_t kGuestStatusSize = 0x0C;
using GuestStatus = std::array<uint8_t, kGuestStatusSize>;
struct Fields {
uint16_t buttons = 0;
int8_t stickX = 0;
int8_t stickY = 0;
int8_t substickX = 0;
int8_t substickY = 0;
uint8_t triggerLeft = 0;
uint8_t triggerRight = 0;
uint8_t analogA = 0;
uint8_t analogB = 0;
int8_t error = 0;
};
inline GuestStatus Encode(const Fields& fields)
{
GuestStatus status{};
BigEndian::Write16(status.data(), fields.buttons);
status[0x02] = static_cast<uint8_t>(fields.stickX);
status[0x03] = static_cast<uint8_t>(fields.stickY);
status[0x04] = static_cast<uint8_t>(fields.substickX);
status[0x05] = static_cast<uint8_t>(fields.substickY);
status[0x06] = fields.triggerLeft;
status[0x07] = fields.triggerRight;
status[0x08] = fields.analogA;
status[0x09] = fields.analogB;
status[0x0A] = static_cast<uint8_t>(fields.error);
return status;
}
} // namespace PadStatusContract
namespace WpadContract {
inline constexpr std::size_t kChannelCount = 4;
inline constexpr int32_t kStatusDisabled = 0;
inline constexpr int32_t kStatusReady = 3;
inline constexpr int32_t kErrorNoController = -1;
inline constexpr int32_t kErrorNotReady = -2;
inline constexpr int32_t kErrorBadChannel = -6;
inline constexpr int32_t kExtensionCore = 0;
class State {
public:
void Initialize() { m_initialized = true; }
bool IsInitialized() const { return m_initialized; }
int32_t GetLibraryStatus() const { return m_initialized ? kStatusReady : kStatusDisabled; }
int32_t GetDataFormat(uint32_t chan) const
{
if (chan >= kChannelCount) {
return kErrorBadChannel;
}
return m_initialized ? 0 : kErrorNotReady;
}
int32_t SetDataFormat(uint32_t chan, int32_t format) const
{
(void)format;
if (chan >= kChannelCount) {
return kErrorBadChannel;
}
return m_initialized ? kErrorNoController : kErrorNotReady;
}
private:
bool m_initialized = false;
};
} // namespace WpadContract
+338
View File
@@ -0,0 +1,338 @@
#pragma once
#include "isa/big_endian.h"
#include <algorithm>
#include <cctype>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <limits>
#include <map>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
namespace DvdFstContract {
struct RegisteredFile {
std::string hostPath;
std::string dvdPath;
uint32_t size = 0;
uint32_t discOffsetWords = 0;
};
struct IndexedEntry {
std::string hostPath;
std::string dvdPath;
uint32_t size = 0;
uint32_t discOffsetWords = 0;
uint32_t parentIndex = 0;
uint32_t subtreeEnd = 0;
bool isDirectory = false;
};
struct Image {
std::vector<IndexedEntry> entries;
std::map<std::string, int32_t> pathToEntry;
std::vector<uint8_t> bytes;
};
struct GuestPlacement {
uint32_t address = 0;
uint32_t reservedArenaHi = 0;
};
inline std::string CanonicalizePath(const std::string& input) {
std::string path = input;
std::replace(path.begin(), path.end(), '\\', '/');
std::vector<std::string> components;
size_t cursor = 0;
while (cursor < path.size()) {
while (cursor < path.size() && path[cursor] == '/') {
++cursor;
}
const size_t start = cursor;
while (cursor < path.size() && path[cursor] != '/') {
++cursor;
}
if (start == cursor) {
continue;
}
std::string component = path.substr(start, cursor - start);
if (component == ".") {
continue;
}
if (component == "..") {
if (!components.empty()) {
components.pop_back();
}
continue;
}
components.push_back(std::move(component));
}
std::string canonical = "/";
for (size_t i = 0; i < components.size(); ++i) {
if (i != 0) {
canonical.push_back('/');
}
canonical += components[i];
}
return canonical;
}
inline std::string NormalizeLookupPath(const std::string& input) {
std::string path = CanonicalizePath(input);
std::transform(path.begin(), path.end(), path.begin(), [](unsigned char ch) {
return static_cast<char>(std::tolower(ch));
});
return path;
}
namespace Detail {
struct TreeNode {
std::string name;
std::map<std::string, std::unique_ptr<TreeNode>> children;
std::optional<RegisteredFile> file;
};
inline std::vector<std::string> Components(const std::string& canonicalPath) {
std::vector<std::string> result;
size_t cursor = canonicalPath == "/" ? canonicalPath.size() : 1;
while (cursor < canonicalPath.size()) {
const size_t slash = canonicalPath.find('/', cursor);
const size_t end = slash == std::string::npos ? canonicalPath.size() : slash;
result.push_back(canonicalPath.substr(cursor, end - cursor));
cursor = end + 1;
}
return result;
}
inline std::string Lowercase(std::string value) {
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
return static_cast<char>(std::tolower(ch));
});
return value;
}
inline uint32_t EmitTree(const TreeNode& directory,
uint32_t directoryIndex,
const std::string& directoryPath,
Image& image,
std::vector<std::string>& names) {
for (const auto& [lookupName, childPointer] : directory.children) {
(void)lookupName;
const TreeNode& child = *childPointer;
const std::string childPath = directoryPath == "/"
? "/" + child.name
: directoryPath + "/" + child.name;
const uint32_t index = static_cast<uint32_t>(image.entries.size());
if (!child.children.empty()) {
if (child.file.has_value()) {
throw std::runtime_error("DVD FST path is both a file and a directory: " + childPath);
}
image.entries.push_back({{}, childPath, 0, 0, directoryIndex, 0, true});
names.push_back(child.name);
image.pathToEntry.emplace(NormalizeLookupPath(childPath), static_cast<int32_t>(index));
image.entries[index].subtreeEnd = EmitTree(child, index, childPath, image, names);
continue;
}
if (!child.file.has_value()) {
throw std::runtime_error("DVD FST contains an empty implicit node: " + childPath);
}
const RegisteredFile& file = *child.file;
image.entries.push_back({file.hostPath, childPath, file.size, file.discOffsetWords,
directoryIndex, index + 1, false});
names.push_back(child.name);
image.pathToEntry.emplace(NormalizeLookupPath(childPath), static_cast<int32_t>(index));
}
return static_cast<uint32_t>(image.entries.size());
}
} // namespace Detail
inline Image BuildImage(const std::vector<RegisteredFile>& registrations) {
// Overlay scanning deliberately registers later mappings last. Collapse those
// mappings before assigning FST indices so one guest path has one stable entry.
std::map<std::string, RegisteredFile> filesByPath;
for (RegisteredFile file : registrations) {
file.dvdPath = CanonicalizePath(file.dvdPath);
if (file.dvdPath == "/") {
throw std::runtime_error("DVD FST cannot register the root as a file");
}
filesByPath[NormalizeLookupPath(file.dvdPath)] = std::move(file);
}
Detail::TreeNode root;
for (const auto& [lookupPath, file] : filesByPath) {
(void)lookupPath;
Detail::TreeNode* node = &root;
const std::vector<std::string> components = Detail::Components(file.dvdPath);
for (const std::string& component : components) {
const std::string key = Detail::Lowercase(component);
auto& child = node->children[key];
if (!child) {
child = std::make_unique<Detail::TreeNode>();
child->name = component;
}
node = child.get();
}
node->name = components.back();
node->file = file;
}
Image image;
image.entries.push_back({{}, "/", 0, 0, 0, 0, true});
image.pathToEntry.emplace("/", 0);
std::vector<std::string> names(1);
image.entries[0].subtreeEnd = Detail::EmitTree(root, 0, "/", image, names);
if (image.entries.size() > std::numeric_limits<uint32_t>::max() / 12u) {
throw std::runtime_error("DVD FST contains too many entries");
}
const size_t entriesSize = image.entries.size() * 12u;
std::vector<uint8_t> stringTable(1, 0);
std::vector<uint32_t> nameOffsets(image.entries.size(), 0);
for (size_t i = 1; i < names.size(); ++i) {
if (stringTable.size() > 0x00FFFFFFu) {
throw std::runtime_error("DVD FST name table exceeds the Wii 24-bit offset limit");
}
nameOffsets[i] = static_cast<uint32_t>(stringTable.size());
stringTable.insert(stringTable.end(), names[i].begin(), names[i].end());
stringTable.push_back(0);
}
image.bytes.assign(entriesSize + stringTable.size(), 0);
for (size_t i = 0; i < image.entries.size(); ++i) {
const IndexedEntry& entry = image.entries[i];
const uint32_t typeAndName = (entry.isDirectory ? 0x01000000u : 0u) | nameOffsets[i];
const uint32_t word1 = entry.isDirectory ? entry.parentIndex : entry.discOffsetWords;
const uint32_t word2 = entry.isDirectory ? entry.subtreeEnd : entry.size;
BigEndian::Write32(image.bytes.data(), i * 12u + 0u, typeAndName);
BigEndian::Write32(image.bytes.data(), i * 12u + 4u, word1);
BigEndian::Write32(image.bytes.data(), i * 12u + 8u, word2);
}
std::copy(stringTable.begin(), stringTable.end(), image.bytes.begin() + entriesSize);
return image;
}
inline std::optional<GuestPlacement> ReserveBelowArena(uint32_t arenaLo,
uint32_t arenaHi,
size_t byteCount) {
constexpr uint32_t kAlignment = 32;
if (byteCount == 0 || byteCount > std::numeric_limits<uint32_t>::max() || arenaHi <= arenaLo) {
return std::nullopt;
}
const uint32_t size = static_cast<uint32_t>(byteCount);
if (size > arenaHi - arenaLo) {
return std::nullopt;
}
const uint32_t unaligned = arenaHi - size;
const uint32_t address = unaligned & ~(kAlignment - 1u);
if (address < arenaLo || static_cast<uint64_t>(address) + size > arenaHi) {
return std::nullopt;
}
return GuestPlacement{address, address};
}
} // namespace DvdFstContract
namespace DvdReadContract {
inline constexpr int32_t kInterruptTransferComplete = 1;
inline constexpr int32_t kInterruptDriveError = 2;
struct LowReadCompletion {
int32_t returnValue;
int32_t callbackResult;
};
inline constexpr LowReadCompletion CompletionFor(bool succeeded) noexcept {
return succeeded ? LowReadCompletion{1, kInterruptTransferComplete}
: LowReadCompletion{0, kInterruptDriveError};
}
enum class HostReadFailure : uint8_t {
None,
MissingFile,
BadOffset,
ShortRead,
};
inline constexpr const char* Describe(HostReadFailure failure) noexcept {
switch (failure) {
case HostReadFailure::None:
return "no error";
case HostReadFailure::MissingFile:
return "host file is missing or cannot be opened";
case HostReadFailure::BadOffset:
return "read offset is outside the host file";
case HostReadFailure::ShortRead:
return "host file did not contain the complete requested range";
}
return "unknown host read error";
}
// Read into private storage first and publish it only after the complete host
// range has been obtained. Callers can therefore leave a guest DMA destination
// untouched for every failure, including a host file truncated after indexing.
inline bool ReadExact(const std::filesystem::path& hostPath,
uint64_t offset,
uint32_t length,
std::vector<uint8_t>& destination,
HostReadFailure& failure) {
failure = HostReadFailure::None;
std::ifstream file(hostPath, std::ios::binary);
if (!file.is_open()) {
failure = HostReadFailure::MissingFile;
return false;
}
file.seekg(0, std::ios::end);
const std::streamoff fileSize = file.tellg();
if (fileSize < 0 ||
offset > static_cast<uint64_t>(std::numeric_limits<std::streamoff>::max()) ||
offset >= static_cast<uint64_t>(fileSize)) {
failure = HostReadFailure::BadOffset;
return false;
}
const uint64_t remaining = static_cast<uint64_t>(fileSize) - offset;
if (static_cast<uint64_t>(length) > remaining) {
failure = HostReadFailure::ShortRead;
return false;
}
std::vector<uint8_t> staged(length);
file.seekg(static_cast<std::streamoff>(offset), std::ios::beg);
if (!file) {
failure = HostReadFailure::BadOffset;
return false;
}
if (length != 0) {
file.read(reinterpret_cast<char*>(staged.data()),
static_cast<std::streamsize>(length));
if (file.gcount() != static_cast<std::streamsize>(length)) {
failure = HostReadFailure::ShortRead;
return false;
}
}
destination = std::move(staged);
return true;
}
} // namespace DvdReadContract
@@ -0,0 +1,199 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <exception>
#include <memory>
#include <optional>
#include <utility>
namespace NetworkDeferredContract {
enum class PreparationState : uint8_t {
NotApplicable,
Ready,
Error,
};
// A recognized operation must not be represented by an empty optional: doing
// so makes malformed input and resource failures indistinguishable from an
// unrelated ioctl and lets the caller fall through to a blocking fallback.
template <typename Work> class Preparation {
public:
static Preparation NotApplicable() {
return Preparation(PreparationState::NotApplicable, 0, std::nullopt);
}
static Preparation Ready(Work work, int32_t failureResult) {
return Preparation(PreparationState::Ready, failureResult,
std::optional<Work>(std::move(work)));
}
static Preparation Error(int32_t result) {
return Preparation(PreparationState::Error, result, std::nullopt);
}
PreparationState State() const noexcept { return state_; }
int32_t FailureResult() const noexcept { return failureResult_; }
Work &&TakeWork() && { return std::move(*work_); }
private:
Preparation(PreparationState state, int32_t failureResult,
std::optional<Work> work)
: state_(state), failureResult_(failureResult), work_(std::move(work)) {}
PreparationState state_ = PreparationState::NotApplicable;
int32_t failureResult_ = 0;
std::optional<Work> work_;
};
enum class StartDisposition : uint8_t {
NotApplicable,
Started,
ImmediateResult,
};
struct StartOutcome {
StartDisposition disposition = StartDisposition::NotApplicable;
int32_t result = 0;
uint64_t token = 0;
static constexpr StartOutcome NotApplicable() noexcept { return {}; }
static constexpr StartOutcome Started(uint64_t token) noexcept {
return {StartDisposition::Started, 0, token};
}
static constexpr StartOutcome Immediate(int32_t result) noexcept {
return {StartDisposition::ImmediateResult, result, 0};
}
};
// Launcher returns a token when the operation is fully installed. Token zero
// is valid for an asynchronous route because only synchronous callers consume
// it. A launcher failure or exception becomes the operation-specific immediate
// error; it can never be reinterpreted as "not applicable".
template <typename Work, typename Launcher>
StartOutcome StartPrepared(Preparation<Work> &&preparation,
Launcher &&launcher) {
const PreparationState state = preparation.State();
const int32_t failureResult = preparation.FailureResult();
if (state == PreparationState::NotApplicable) {
return StartOutcome::NotApplicable();
}
if (state == PreparationState::Error) {
return StartOutcome::Immediate(failureResult);
}
try {
const std::optional<uint64_t> token =
std::forward<Launcher>(launcher)(std::move(preparation).TakeWork());
return token ? StartOutcome::Started(*token)
: StartOutcome::Immediate(failureResult);
} catch (...) {
return StartOutcome::Immediate(failureResult);
}
}
// Keep an untouched copy of the host-only work value until resolution and
// completion publication both succeed. Any worker-side exception is converted
// into one failure-publication attempt instead of escaping the thread entry and
// terminating the process.
template <typename Work, typename Resolver, typename Publish,
typename PublishFailure>
void RunWorker(Work work, Resolver &&resolver, Publish &&publish,
PublishFailure &&publishFailure) noexcept {
try {
auto completion = std::forward<Resolver>(resolver)(work);
std::forward<Publish>(publish)(std::move(completion));
} catch (...) {
const std::exception_ptr error = std::current_exception();
try {
std::forward<PublishFailure>(publishFailure)(std::move(work), error);
} catch (...) {
// There is no safe blocking fallback from a detached worker. The
// production failure publisher logs if its completion queue cannot
// accept the already-normalized failure.
}
}
}
template <typename Worker, typename OnDetachFailure>
Worker *DetachOrRelease(std::unique_ptr<Worker> worker,
OnDetachFailure &&onDetachFailure) noexcept {
if (!worker) {
return nullptr;
}
try {
worker->detach();
return nullptr;
} catch (...) {
const std::exception_ptr error = std::current_exception();
Worker *const runningWorker = worker.release();
try {
std::forward<OnDetachFailure>(onDetachFailure)(error);
} catch (...) {
// Diagnostics must not turn containment of a running worker into a
// second failure path.
}
return runningWorker;
}
}
// IOS encodes an IPv4 sockaddr as a two-byte length/family header followed by
// sockaddr::sa_data. Advertising any larger ai_addrlen would expose bytes that
// were never copied into the guest result.
inline constexpr size_t kWiiSockAddrHeaderBytes = 2;
inline constexpr size_t kWiiSockAddrPayloadBytes = 14;
inline constexpr size_t kWiiIpv4SockAddrBytes =
kWiiSockAddrHeaderBytes + kWiiSockAddrPayloadBytes;
inline constexpr bool CanCopyIpv4SockAddr(int nativeFamily, size_t nativeLength,
int nativeIpv4Family) noexcept {
return nativeFamily == nativeIpv4Family &&
nativeLength >= kWiiIpv4SockAddrBytes;
}
inline constexpr bool
AdvertisedSockAddrFits(uint32_t advertisedLength) noexcept {
return advertisedLength <= kWiiIpv4SockAddrBytes;
}
} // namespace NetworkDeferredContract
namespace NetworkConnectContract {
// IOS presents a blocking socket to the guest while the retained host socket
// stays nonblocking. A blocking guest connect therefore waits on the guest
// OSThread, never inside WSAPoll/poll on the emulation scheduler thread.
inline constexpr int64_t kGuestBlockingTimeoutMilliseconds = 10000;
enum class ProbeDisposition : uint8_t {
StaleSocket,
PollError,
SocketReady,
TimedOut,
Pending,
};
// Keep the ordering explicit: fd reuse invalidates the operation before any
// host syscall, readiness wins at the deadline, and only a zero-result probe
// may remain pending or time out.
inline constexpr ProbeDisposition ClassifyProbe(
bool socketIdentityIsCurrent, int pollResult, bool deadlineExpired) noexcept {
if (!socketIdentityIsCurrent) {
return ProbeDisposition::StaleSocket;
}
if (pollResult < 0) {
return ProbeDisposition::PollError;
}
if (pollResult > 0) {
return ProbeDisposition::SocketReady;
}
return deadlineExpired ? ProbeDisposition::TimedOut
: ProbeDisposition::Pending;
}
} // namespace NetworkConnectContract
+161
View File
@@ -0,0 +1,161 @@
#pragma once
#include <array>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <vector>
#ifdef _WIN32
#include <winsock2.h>
#else
#include <poll.h>
#include <sys/socket.h>
#endif
namespace NetworkPollContract {
constexpr size_t kMaxDescriptors = 24;
#ifdef _WIN32
using NativeSocket = SOCKET;
using NativePollFd = WSAPOLLFD;
constexpr NativeSocket kInvalidSocket = INVALID_SOCKET;
#else
using NativeSocket = int;
using NativePollFd = pollfd;
constexpr NativeSocket kInvalidSocket = -1;
#endif
struct CopiedDescriptor {
uint32_t wiiFd = 0;
NativeSocket nativeFd = kInvalidSocket;
uint64_t socketGeneration = 0;
short events = 0;
short revents = 0;
};
// A zero-timeout SO_POLL is a pure readiness probe. Running it directly on
// the emulation thread is safe because ProbeNow always passes timeout zero to
// the host API; putting the guest IOS caller to sleep until the next scheduler
// pump only adds a needless context switch to every GameSpy update tick.
inline bool RequiresSchedulerWait(int64_t timeoutMilliseconds) {
return timeoutMilliseconds != 0;
}
inline short WiiEventsToNative(uint32_t events) {
int native = 0;
if (events & 0x0001u) native |= POLLRDNORM;
if (events & 0x0002u) native |= POLLRDBAND;
if (events & 0x0004u) native |= POLLPRI;
if (events & 0x0008u) native |= POLLWRNORM;
if (events & 0x0010u) native |= POLLWRBAND;
// ERR/HUP/NVAL are return-only. Winsock's WSAPoll also rejects the
// priority and write-band inputs which Dolphin masks on Windows.
native &= ~(POLLERR | POLLHUP | POLLNVAL);
#ifdef _WIN32
native &= ~(POLLPRI | POLLWRBAND);
#endif
return static_cast<short>(native);
}
inline uint32_t NativeEventsToWii(short events) {
uint32_t wii = 0;
if (events & POLLRDNORM) wii |= 0x0001u;
if (events & POLLRDBAND) wii |= 0x0002u;
if (events & POLLPRI) wii |= 0x0004u;
if (events & POLLWRNORM) wii |= 0x0008u;
if (events & POLLWRBAND) wii |= 0x0010u;
if (events & POLLERR) wii |= 0x0020u;
if (events & POLLHUP) wii |= 0x0040u;
if (events & POLLNVAL) wii |= 0x0080u;
return wii;
}
class Timeout {
public:
using Clock = std::chrono::steady_clock;
using TimePoint = Clock::time_point;
static Timeout FromMilliseconds(int64_t milliseconds, TimePoint now = Clock::now()) {
Timeout timeout;
if (milliseconds < 0) {
timeout.m_infinite = true;
timeout.m_deadline = TimePoint::max();
return timeout;
}
using Milliseconds = std::chrono::milliseconds;
const int64_t maximum = std::chrono::duration_cast<Milliseconds>(TimePoint::max() - now).count();
timeout.m_deadline = milliseconds >= maximum
? TimePoint::max()
: now + Milliseconds(milliseconds);
return timeout;
}
bool IsExpired(TimePoint now = Clock::now()) const {
return !m_infinite && now >= m_deadline;
}
bool ShouldRemainPending(int nativeResult, TimePoint now = Clock::now()) const {
return nativeResult == 0 && !IsExpired(now);
}
bool IsInfinite() const { return m_infinite; }
TimePoint Deadline() const { return m_deadline; }
private:
bool m_infinite = false;
TimePoint m_deadline{};
};
// Probes only descriptors whose copied socket identity is still live. A dead identity
// (SOClose/SOCleanup/slot reuse) reports POLLNVAL and counts toward readiness like IOS/Dolphin;
// skipping it silently would return 0 forever and strand an infinite-timeout SO_POLL parked
// during socket teardown mid-WFC-connect.
template <typename IsStillValid>
int ProbeNow(std::vector<CopiedDescriptor>& descriptors, IsStillValid&& isStillValid) {
if (descriptors.size() > kMaxDescriptors) {
return -1;
}
std::array<NativePollFd, kMaxDescriptors> active{};
std::array<size_t, kMaxDescriptors> originalIndices{};
size_t activeCount = 0;
int invalidCount = 0;
for (size_t i = 0; i < descriptors.size(); ++i) {
CopiedDescriptor& descriptor = descriptors[i];
descriptor.revents = 0;
if (!isStillValid(descriptor)) {
descriptor.revents = POLLNVAL;
++invalidCount;
continue;
}
active[activeCount].fd = descriptor.nativeFd;
active[activeCount].events = descriptor.events;
active[activeCount].revents = 0;
originalIndices[activeCount] = i;
++activeCount;
}
if (activeCount == 0) {
return invalidCount;
}
#ifdef _WIN32
const int result = WSAPoll(active.data(), static_cast<ULONG>(activeCount), 0);
#else
const int result = poll(active.data(), activeCount, 0);
#endif
if (result >= 0) {
for (size_t i = 0; i < activeCount; ++i) {
descriptors[originalIndices[i]].revents = active[i].revents;
}
return result + invalidCount;
}
return invalidCount > 0 ? invalidCount : result;
}
} // namespace NetworkPollContract
+692
View File
@@ -0,0 +1,692 @@
// Riivolution patch-XML parsing and patch selection.
//
// Ported from Dolphin Emulator's DiscIO/RiivolutionParser.h/cpp and the
// external-path resolution rules of DiscIO/RiivolutionPatcher.cpp
// (https://github.com/dolphin-emu/dolphin).
// Copyright 2021 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
//
// Deviations from Dolphin, all deliberate:
// - <memory> patches are parsed but never applied here: guest code patching
// belongs to the translator's Code.pul/lowmem pipeline, not the runtime.
// - Riivolution "macros" are not supported
#pragma once
#include <algorithm>
#include <cstdint>
#include <map>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include <pugixml.hpp>
namespace RiivolutionContract {
// ============================================================================
// Minimal XML document model
// ============================================================================
struct XmlNode {
std::string name;
std::vector<std::pair<std::string, std::string>> attributes;
std::vector<XmlNode> children;
const std::string* FindAttribute(std::string_view attributeName) const {
for (const auto& attribute : attributes) {
if (attribute.first == attributeName) {
return &attribute.second;
}
}
return nullptr;
}
std::string Attribute(std::string_view attributeName, std::string_view fallback = {}) const {
const std::string* value = FindAttribute(attributeName);
return value ? *value : std::string(fallback);
}
bool AttributeBool(std::string_view attributeName, bool fallback) const {
const std::string* value = FindAttribute(attributeName);
if (!value) {
return fallback;
}
if (*value == "true" || *value == "1" || *value == "yes") {
return true;
}
if (*value == "false" || *value == "0" || *value == "no") {
return false;
}
return fallback;
}
// Accepts decimal and 0x-prefixed hex, matching pugixml's number parsing
// (Riivolution XMLs write memory offsets as 0x........).
uint32_t AttributeUint(std::string_view attributeName, uint32_t fallback) const {
const std::string* value = FindAttribute(attributeName);
if (!value || value->empty()) {
return fallback;
}
const std::string& text = *value;
size_t index = 0;
uint32_t base = 10;
if (text.size() > 2 && text[0] == '0' && (text[1] == 'x' || text[1] == 'X')) {
base = 16;
index = 2;
}
uint64_t result = 0;
for (; index < text.size(); ++index) {
const char c = text[index];
uint32_t digit;
if (c >= '0' && c <= '9') {
digit = static_cast<uint32_t>(c - '0');
} else if (base == 16 && c >= 'a' && c <= 'f') {
digit = static_cast<uint32_t>(c - 'a' + 10);
} else if (base == 16 && c >= 'A' && c <= 'F') {
digit = static_cast<uint32_t>(c - 'A' + 10);
} else {
return fallback;
}
result = result * base + digit;
if (result > 0xffffffffull) {
return fallback;
}
}
return index > (base == 16 ? 2u : 0u) ? static_cast<uint32_t>(result) : fallback;
}
int AttributeInt(std::string_view attributeName, int fallback) const {
const std::string* value = FindAttribute(attributeName);
if (!value || value->empty()) {
return fallback;
}
const bool negative = (*value)[0] == '-';
const uint32_t magnitude =
AttributeUintFromText(negative ? value->substr(1) : *value, 0x80000000u);
if (magnitude == 0x80000000u && !negative) {
return fallback;
}
return negative ? -static_cast<int>(magnitude) : static_cast<int>(magnitude);
}
private:
static uint32_t AttributeUintFromText(const std::string& text, uint32_t fallback) {
XmlNode probe;
probe.attributes.push_back({"v", text});
return probe.AttributeUint("v", fallback);
}
};
namespace XmlDetail {
inline bool StartsWith(std::string_view text, std::string_view prefix) {
return text.size() >= prefix.size() && text.compare(0, prefix.size(), prefix) == 0;
}
} // namespace XmlDetail
// Converts only element and attribute data from pugixml. Riivolution carries
// its data in attributes, so text, declarations, comments, and CDATA do not
// need to become part of the contract's data model.
inline XmlNode CopyXmlNode(const pugi::xml_node& source) {
XmlNode destination;
destination.name = source.name();
for (const pugi::xml_attribute& attribute : source.attributes()) {
destination.attributes.emplace_back(attribute.name(), attribute.value());
}
for (const pugi::xml_node& child : source.children()) {
if (child.type() == pugi::node_element) {
destination.children.push_back(CopyXmlNode(child));
}
}
return destination;
}
// Parses a document with pugixml and returns its root element, or nullopt when
// malformed. load_buffer accepts the UTF-8 BOM used by some Riivolution packs.
inline std::optional<XmlNode> ParseXml(std::string_view text) {
pugi::xml_document document;
const pugi::xml_parse_result result = document.load_buffer(
text.data(), text.size(), pugi::parse_default, pugi::encoding_utf8);
if (!result) {
return std::nullopt;
}
const pugi::xml_node root = document.document_element();
if (!root) {
return std::nullopt;
}
return CopyXmlNode(root);
}
// ============================================================================
// Riivolution data model (mirrors Dolphin's DiscIO::Riivolution)
// ============================================================================
struct GameFilter {
std::optional<std::string> game;
std::optional<std::string> developer;
std::optional<int> disc;
std::optional<int> version;
std::optional<std::vector<std::string>> regions;
};
struct PatchReference {
std::string id;
std::map<std::string, std::string> params;
};
struct Choice {
std::string name;
std::vector<PatchReference> patchReferences;
};
struct Option {
std::string name;
std::string id;
std::vector<Choice> choices;
// 1-based index into choices; 0 means disabled.
uint32_t selectedChoice = 0;
};
struct Section {
std::string name;
std::vector<Option> options;
};
struct File {
std::string disc;
std::string external;
bool resize = true;
bool create = false;
uint32_t offset = 0;
uint32_t fileoffset = 0;
uint32_t length = 0;
};
struct Folder {
std::string disc;
std::string external;
bool resize = true;
bool create = false;
bool recursive = true;
uint32_t length = 0;
};
struct Savegame {
std::string external;
bool clone = true;
};
// Parsed for completeness; the runtime never applies these (guest code and
// lowmem patching is the translator pipeline's job).
struct MemoryPatch {
uint32_t offset = 0;
std::string value;
std::string valuefile;
std::string original;
bool ocarina = false;
bool search = false;
uint32_t align = 1;
};
struct Patch {
std::string id;
std::string root;
std::vector<File> filePatches;
std::vector<Folder> folderPatches;
std::vector<Savegame> savegamePatches;
std::vector<MemoryPatch> memoryPatches;
};
struct Disc {
int version = 0;
GameFilter gameFilter;
std::vector<Section> sections;
std::vector<Patch> patches;
bool IsValidForGame(const std::string& gameId, std::optional<uint16_t> revision,
std::optional<uint8_t> discNumber) const;
std::vector<Patch> GeneratePatches(const std::string& gameId) const;
};
// riivolution/config/<GameID4>.xml - remembered option choices.
struct ConfigOption {
std::string id;
uint32_t defaultChoice = 0;
};
struct Config {
int version = 0;
std::vector<ConfigOption> options;
};
// An option choice pinned by the distribution manifest (recomp.yml).
struct OptionSelection {
std::string section; // empty = match any section
std::string option; // matches Option::id first, then Option::name
uint32_t choice = 0;
};
// ============================================================================
// Parsing
// ============================================================================
namespace Detail {
inline std::map<std::string, std::string> ReadParams(const XmlNode& node,
std::map<std::string, std::string> params = {}) {
for (const XmlNode& paramNode : node.children) {
if (paramNode.name != "param") {
continue;
}
params[paramNode.Attribute("name")] = paramNode.Attribute("value");
}
return params;
}
} // namespace Detail
inline std::optional<Disc> ParseString(std::string_view xml) {
const std::optional<XmlNode> root = ParseXml(xml);
if (!root || root->name != "wiidisc") {
return std::nullopt;
}
Disc disc;
disc.version = root->AttributeInt("version", -1);
if (disc.version != 1) {
return std::nullopt;
}
const std::string defaultRoot = root->Attribute("root");
for (const XmlNode& node : root->children) {
if (node.name == "id") {
for (const auto& attribute : node.attributes) {
if (attribute.first == "game") {
disc.gameFilter.game = attribute.second;
} else if (attribute.first == "developer") {
disc.gameFilter.developer = attribute.second;
} else if (attribute.first == "disc") {
disc.gameFilter.disc = node.AttributeInt("disc", -1);
} else if (attribute.first == "version") {
disc.gameFilter.version = node.AttributeInt("version", -1);
}
}
std::vector<std::string> regions;
for (const XmlNode& regionNode : node.children) {
if (regionNode.name == "region") {
regions.push_back(regionNode.Attribute("type"));
}
}
if (!regions.empty()) {
disc.gameFilter.regions = std::move(regions);
}
continue;
}
if (node.name == "options") {
for (const XmlNode& sectionNode : node.children) {
if (sectionNode.name != "section") {
continue;
}
Section section;
section.name = sectionNode.Attribute("name");
for (const XmlNode& optionNode : sectionNode.children) {
if (optionNode.name != "option") {
continue;
}
Option option;
option.id = optionNode.Attribute("id");
option.name = optionNode.Attribute("name");
option.selectedChoice = optionNode.AttributeUint("default", 0);
auto optionParams = Detail::ReadParams(optionNode);
for (const XmlNode& choiceNode : optionNode.children) {
if (choiceNode.name != "choice") {
continue;
}
Choice choice;
choice.name = choiceNode.Attribute("name");
auto choiceParams = Detail::ReadParams(choiceNode, optionParams);
for (const XmlNode& patchRefNode : choiceNode.children) {
if (patchRefNode.name != "patch") {
continue;
}
PatchReference patchReference;
patchReference.id = patchRefNode.Attribute("id");
patchReference.params = Detail::ReadParams(patchRefNode, choiceParams);
choice.patchReferences.push_back(std::move(patchReference));
}
option.choices.push_back(std::move(choice));
}
section.options.push_back(std::move(option));
}
disc.sections.push_back(std::move(section));
}
continue;
}
if (node.name == "patch") {
Patch patch;
patch.id = node.Attribute("id");
patch.root = node.Attribute("root");
if (patch.root.empty()) {
patch.root = defaultRoot;
}
for (const XmlNode& patchNode : node.children) {
if (patchNode.name == "file") {
File file;
file.disc = patchNode.Attribute("disc");
file.external = patchNode.Attribute("external");
file.resize = patchNode.AttributeBool("resize", true);
file.create = patchNode.AttributeBool("create", false);
file.offset = patchNode.AttributeUint("offset", 0);
file.fileoffset = patchNode.AttributeUint("fileoffset", 0);
file.length = patchNode.AttributeUint("length", 0);
patch.filePatches.push_back(std::move(file));
} else if (patchNode.name == "folder") {
Folder folder;
folder.disc = patchNode.Attribute("disc");
folder.external = patchNode.Attribute("external");
folder.resize = patchNode.AttributeBool("resize", true);
folder.create = patchNode.AttributeBool("create", false);
folder.recursive = patchNode.AttributeBool("recursive", true);
folder.length = patchNode.AttributeUint("length", 0);
patch.folderPatches.push_back(std::move(folder));
} else if (patchNode.name == "savegame") {
Savegame savegame;
savegame.external = patchNode.Attribute("external");
savegame.clone = patchNode.AttributeBool("clone", true);
patch.savegamePatches.push_back(std::move(savegame));
} else if (patchNode.name == "memory") {
MemoryPatch memory;
memory.offset = patchNode.AttributeUint("offset", 0);
memory.value = patchNode.Attribute("value");
memory.valuefile = patchNode.Attribute("valuefile");
memory.original = patchNode.Attribute("original");
memory.ocarina = patchNode.AttributeBool("ocarina", false);
memory.search = patchNode.AttributeBool("search", false);
memory.align = patchNode.AttributeUint("align", 1);
patch.memoryPatches.push_back(std::move(memory));
}
}
disc.patches.push_back(std::move(patch));
}
}
return disc;
}
inline std::optional<Config> ParseConfigString(std::string_view xml) {
const std::optional<XmlNode> root = ParseXml(xml);
if (!root || root->name != "riivolution") {
return std::nullopt;
}
Config config;
config.version = root->AttributeInt("version", -1);
if (config.version != 2) {
return std::nullopt;
}
for (const XmlNode& optionNode : root->children) {
if (optionNode.name != "option") {
continue;
}
ConfigOption option;
option.id = optionNode.Attribute("id");
option.defaultChoice = optionNode.AttributeUint("default", 0);
config.options.push_back(std::move(option));
}
return config;
}
// ============================================================================
// Game matching and patch generation
// ============================================================================
inline bool Disc::IsValidForGame(const std::string& gameId, std::optional<uint16_t> revision,
std::optional<uint8_t> discNumber) const {
if (gameId.size() != 6) {
return false;
}
const std::string_view gameIdFull(gameId);
const std::string_view gameRegion = gameIdFull.substr(3, 1);
const std::string_view gameDeveloper = gameIdFull.substr(4, 2);
const int discNumberInt = discNumber ? static_cast<int>(*discNumber) : -1;
const int revisionInt = revision ? static_cast<int>(*revision) : -1;
if (gameFilter.game && !XmlDetail::StartsWith(gameIdFull, *gameFilter.game)) {
return false;
}
if (gameFilter.developer && gameDeveloper != *gameFilter.developer) {
return false;
}
if (gameFilter.disc && discNumberInt != *gameFilter.disc) {
return false;
}
if (gameFilter.version && revisionInt != *gameFilter.version) {
return false;
}
if (gameFilter.regions) {
const auto& regions = *gameFilter.regions;
if (!regions.empty() &&
std::find(regions.begin(), regions.end(), std::string(gameRegion)) == regions.end()) {
return false;
}
}
return true;
}
inline std::vector<Patch> Disc::GeneratePatches(const std::string& gameId) const {
const std::string_view gameIdFull(gameId);
const std::string_view gameIdNoRegion = gameIdFull.substr(0, 3);
const std::string_view gameRegion = gameIdFull.substr(3, 1);
const std::string_view gameDeveloper = gameIdFull.size() >= 6 ? gameIdFull.substr(4, 2) : std::string_view();
const auto replaceVariables =
[](std::string_view sv, const std::vector<std::pair<std::string, std::string_view>>& replacements) {
std::string result;
result.reserve(sv.size());
while (!sv.empty()) {
bool replaced = false;
for (const auto& replacement : replacements) {
if (XmlDetail::StartsWith(sv, replacement.first)) {
result.append(replacement.second.data(), replacement.second.size());
sv = sv.substr(replacement.first.size());
replaced = true;
break;
}
}
if (replaced) {
continue;
}
result.push_back(sv[0]);
sv = sv.substr(1);
}
return result;
};
// Take only selected patches, replace placeholders in all strings, and
// return them.
std::vector<Patch> activePatches;
for (const Section& section : sections) {
for (const Option& option : section.options) {
const uint32_t selected = option.selectedChoice;
if (selected == 0 || selected > option.choices.size()) {
continue;
}
const Choice& choice = option.choices[selected - 1];
for (const PatchReference& patchReference : choice.patchReferences) {
const auto patch = std::find_if(patches.begin(), patches.end(), [&](const Patch& candidate) {
return candidate.id == patchReference.id;
});
if (patch == patches.end()) {
continue;
}
std::vector<std::pair<std::string, std::string_view>> replacements;
replacements.emplace_back("{$__gameid}", gameIdNoRegion);
replacements.emplace_back("{$__region}", gameRegion);
replacements.emplace_back("{$__maker}", gameDeveloper);
for (const auto& param : patchReference.params) {
replacements.emplace_back("{$" + param.first + "}", param.second);
}
Patch newPatch = *patch;
newPatch.root = replaceVariables(newPatch.root, replacements);
for (File& file : newPatch.filePatches) {
file.disc = replaceVariables(file.disc, replacements);
file.external = replaceVariables(file.external, replacements);
}
for (Folder& folder : newPatch.folderPatches) {
folder.disc = replaceVariables(folder.disc, replacements);
folder.external = replaceVariables(folder.external, replacements);
}
for (Savegame& savegame : newPatch.savegamePatches) {
savegame.external = replaceVariables(savegame.external, replacements);
}
for (MemoryPatch& memory : newPatch.memoryPatches) {
memory.valuefile = replaceVariables(memory.valuefile, replacements);
}
activePatches.push_back(std::move(newPatch));
}
}
}
return activePatches;
}
// ============================================================================
// Option selection
// ============================================================================
// Dolphin's config identifier: an option is addressed by its id when it has
// one, otherwise by the concatenation of section name and option name.
inline void ApplyConfigDefaults(Disc& disc, const Config& config) {
for (const ConfigOption& configOption : config.options) {
for (Section& section : disc.sections) {
for (Option& option : section.options) {
const bool matches = option.id.empty()
? (section.name + option.name) == configOption.id
: option.id == configOption.id;
if (matches) {
option.selectedChoice = configOption.defaultChoice;
}
}
}
}
}
// Applies distribution-pinned selections. Runs after ApplyConfigDefaults so a
// pin always wins over the user's remembered choice.
inline void ApplySelections(Disc& disc, const std::vector<OptionSelection>& selections) {
for (const OptionSelection& selection : selections) {
for (Section& section : disc.sections) {
if (!selection.section.empty() && section.name != selection.section) {
continue;
}
for (Option& option : section.options) {
const bool matches = (!option.id.empty() && option.id == selection.option) ||
option.name == selection.option;
if (matches && selection.choice <= option.choices.size()) {
option.selectedChoice = selection.choice;
}
}
}
}
}
// External path resolution (Dolphin FileDataLoaderHostFS semantics). A leading '/' is absolute
// (relative to the SD card root); otherwise relative to the patch root (the XML's folder, or
// its 'root' attribute override). All paths use '/' separators (callers convert host paths via
// generic_string() first); returns nullopt for ".." traversal or a backslash, which Riivolution
// treats as a filename character that Windows paths can't replicate.
inline std::optional<std::string> MakeAbsoluteFromRelative(std::string_view sdRoot,
std::string_view patchRoot,
std::string_view externalRelativePath) {
if (externalRelativePath.find('\\') != std::string_view::npos) {
return std::nullopt;
}
const bool absolute = !externalRelativePath.empty() && externalRelativePath[0] == '/';
std::string result(absolute ? sdRoot : patchRoot);
while (!result.empty() && result.back() == '/') {
result.pop_back();
}
std::string_view work = externalRelativePath;
while (!work.empty() && work.front() == '/') {
work.remove_prefix(1);
}
while (!work.empty() && work.back() == '/') {
work.remove_suffix(1);
}
size_t depth = 0;
while (!work.empty()) {
const size_t separator = work.find('/');
const std::string_view element = work.substr(0, separator);
if (element == ".") {
// Harmless, changes nothing.
} else if (element == "..") {
// Going up a level; never above the root.
if (depth == 0) {
return std::nullopt;
}
--depth;
const size_t lastSlash = result.rfind('/');
if (lastSlash == std::string::npos) {
return std::nullopt;
}
result.resize(lastSlash);
} else if (!element.empty()) {
++depth;
result.push_back('/');
result.append(element.data(), element.size());
}
if (separator == std::string_view::npos) {
break;
}
work.remove_prefix(separator + 1);
}
return result;
}
// Computes a patch's effective root directory from the XML file's directory
// and the patch's 'root' attribute.
inline std::string ResolvePatchRoot(std::string_view sdRoot, std::string_view xmlDirectory,
std::string_view rootAttribute) {
std::string patchRoot(xmlDirectory);
if (!rootAttribute.empty()) {
if (auto resolved = MakeAbsoluteFromRelative(sdRoot, xmlDirectory, rootAttribute)) {
patchRoot = std::move(*resolved);
}
}
return patchRoot;
}
// First <savegame> across the active patches, in order (Dolphin
// ExtractSavegameRedirect).
inline const Savegame* FindSavegamePatch(const std::vector<Patch>& activePatches) {
for (const Patch& patch : activePatches) {
if (!patch.savegamePatches.empty()) {
return &patch.savegamePatches[0];
}
}
return nullptr;
}
} // namespace RiivolutionContract
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include "abi_bridge.h"
// hle/gx/gx_fatal_stubs.cpp includes nothing but this header and reaches
// std::fprintf / std::snprintf / std::abort through it.
#include <cstdio>
#include <cstdlib>
// VI Utils
void VI_HLE_ForceRetrace(CpuContext* ctx);
void VI_HLE_PollRetrace(CpuContext* ctx);
void VI_HLE_ProcessRetracesDeferred(int maxToProcess);
void VI_HLE_WaitForNextRetracePoll();
// Single owner of the Aurora frame presentation sequence (seal, optional pace
// to the VI retrace boundary, pre-warm the next frame). paceToRetrace is true
// for the GXCopyDisp producer path and false for retrace-context presents.
void VI_HLE_PresentFrame(bool presentedXfb, bool paceToRetrace);
bool VI_HLE_IsAdvancingRetrace();
void VI_HLE_SetXfbReady(uint32_t xfbAddr); // Called by GXCopyDisp to signal EFB→XFB copy
void Audio_HLE_Tick(CpuContext* ctx, uint32_t deltaMicros);
void Audio_HLE_Poll(CpuContext* ctx);
// Deferred twin of Audio_HLE_Poll for the long host waits that already service
// retraces and alarms (the VI retrace pacing loop, the Aurora frame-worker wait
// callback). Runs the AI DMA tick on an isolated register file the way
// OS_HLE_ProcessAlarmsDeferred does, so it is safe to call from the middle of an
// arbitrary translated function.
void Audio_HLE_PollDeferred();
bool OS_HLE_InterruptsEnabled() noexcept;
extern "C" void OS_HLE_ProcessAlarmsDeferred(int maxToProcess);
extern "C" void OS_HLE_BeginDeferredGuestCallbacks();
extern "C" void OS_HLE_EndDeferredGuestCallbacks();
// Defines and registers a faithful native reimplementation that REPLACES the translated function
// at a PPC address (not a stub; genuine not-yet-implemented entries live in hle/gx/gx_fatal_stubs.cpp
// and abort). The translator regex-parses these macro names to skip that address at build time, so
// renaming requires updating Translator.Cli/Program.cs, RuntimeNativeGuestEffectAnalyzer.cs,
// TranslatedBuildShardEmitter.cs and RuntimeNativeFunctionAbiProvider.cs together.
#define PPC_NATIVE_OVERRIDE(addr_hex, name, ret_type, arg_list, call_list) \
extern "C" ret_type func_##addr_hex arg_list { return name call_list; } \
REGISTER_NATIVE_FUNCTION(0x##addr_hex, name)
#define PPC_NATIVE_OVERRIDE_VOID(addr_hex, name, arg_list, call_list) \
extern "C" void func_##addr_hex arg_list { name call_list; } \
REGISTER_NATIVE_FUNCTION(0x##addr_hex, name)
+91
View File
@@ -0,0 +1,91 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <type_traits>
namespace BigEndian {
inline uint16_t Read16(const uint8_t* src) {
return (static_cast<uint16_t>(src[0]) << 8) |
static_cast<uint16_t>(src[1]);
}
inline uint32_t Read32(const uint8_t* src) {
return (static_cast<uint32_t>(src[0]) << 24) |
(static_cast<uint32_t>(src[1]) << 16) |
(static_cast<uint32_t>(src[2]) << 8) |
static_cast<uint32_t>(src[3]);
}
inline float ReadFloat32(const uint8_t* src) {
const uint32_t bits = Read32(src);
float value = 0.0f;
static_assert(sizeof(bits) == sizeof(value));
std::memcpy(&value, &bits, sizeof(value));
return value;
}
inline void Write16(uint8_t* dst, uint16_t value) {
dst[0] = static_cast<uint8_t>(value >> 8);
dst[1] = static_cast<uint8_t>(value);
}
inline void Write32(uint8_t* dst, uint32_t value) {
dst[0] = static_cast<uint8_t>(value >> 24);
dst[1] = static_cast<uint8_t>(value >> 16);
dst[2] = static_cast<uint8_t>(value >> 8);
dst[3] = static_cast<uint8_t>(value);
}
inline void Write64(uint8_t* dst, uint64_t value) {
Write32(dst, static_cast<uint32_t>(value >> 32));
Write32(dst + sizeof(uint32_t), static_cast<uint32_t>(value));
}
inline void WriteFloat32(uint8_t* dst, float value) {
uint32_t bits = 0;
static_assert(sizeof(bits) == sizeof(value));
std::memcpy(&bits, &value, sizeof(bits));
Write32(dst, bits);
}
inline void Write16(uint8_t* dst, std::size_t offset, uint16_t value) {
Write16(dst + offset, value);
}
inline void Write32(uint8_t* dst, std::size_t offset, uint32_t value) {
Write32(dst + offset, value);
}
inline void Write64(uint8_t* dst, std::size_t offset, uint64_t value) {
Write64(dst + offset, value);
}
inline void WriteFloat32(uint8_t* dst, std::size_t offset, float value) {
WriteFloat32(dst + offset, value);
}
template <typename Offset>
inline void Append16(uint8_t* dst, Offset& offset, uint16_t value) {
static_assert(std::is_integral_v<Offset>);
Write16(dst + offset, value);
offset += static_cast<Offset>(sizeof(value));
}
template <typename Offset>
inline void Append32(uint8_t* dst, Offset& offset, uint32_t value) {
static_assert(std::is_integral_v<Offset>);
Write32(dst + offset, value);
offset += static_cast<Offset>(sizeof(value));
}
template <typename Offset>
inline void AppendFloat32(uint8_t* dst, Offset& offset, float value) {
static_assert(std::is_integral_v<Offset>);
WriteFloat32(dst + offset, value);
offset += static_cast<Offset>(sizeof(value));
}
} // namespace BigEndian
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include <atomic>
#include <cstdint>
#define MKW_RESTRICT __restrict
#include <immintrin.h>
inline constexpr bool MkwStateFreeAbiEnabled(uint32_t) noexcept
{
return true;
}
#define MKW_PPC_FORCE_INLINE __forceinline
#define MKW_PPC_NO_INLINE __declspec(noinline)
#define MKW_PPC_ALWAYS_INLINE_BODY __attribute__((always_inline))
#define MKW_PPC_COLD __attribute__((cold))
#define MKW_PPC_INTERNAL_CALL __regcall
using MkwStateFreeResult2 = uint64_t __attribute__((ext_vector_type(2)));
+188
View File
@@ -0,0 +1,188 @@
#pragma once
#include "ppc_isa_fpenv.h"
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <string_view>
void ShowRuntimeFatalPopup(std::string_view category, std::string_view details) noexcept;
union PPC_FPR {
uint64_t raw;
double d;
struct {
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
float ps1; // Low word (Least Significant)
float ps0; // High word (Most Significant)
#else
float ps0; // High word
float ps1; // Low word
#endif
} paired;
};
// PowerPC CPU Context
struct CpuContext {
// Standard GPRs
uint32_t gpr[32];
// Special Purpose Registers defined by standard PPC
uint32_t cr; // Condition Register
uint32_t lr; // Link Register
uint32_t ctr; // Count Register
uint32_t xer; // Integer Exception Register
uint32_t fpscr; // Floating-Point Status and Control Register
// Program State
uint32_t pc; // Program Counter (not used alot tho)
// Floating Point Registers (Modified for Paired Single support)
PPC_FPR fpr[32];
// Broadway Specific Extensions
uint32_t gqr[8]; // Graphics Quantization Registers
uint32_t hid0; // HID0
uint32_t hid1; // HID1
uint32_t hid2; // HID2
uint32_t srr0; // Save/Restore Register 0
uint32_t srr1; // Save/Restore Register 1
uint32_t msr; // Machine State Register
};
inline thread_local CpuContext* g_currentCpuContext = nullptr;
class CpuContextScope {
public:
explicit CpuContextScope(CpuContext* ctx)
: previous_(g_currentCpuContext)
{
g_currentCpuContext = ctx;
savedMxcsr_ = _mm_getcsr();
if (ctx != nullptr)
MkwApplyHostNiMode(ctx->fpscr);
}
~CpuContextScope()
{
g_currentCpuContext = previous_;
if (previous_ != nullptr)
MkwApplyHostNiMode(previous_->fpscr);
else
MkwRestoreHostMxcsr(savedMxcsr_);
}
CpuContextScope(const CpuContextScope&) = delete;
CpuContextScope& operator=(const CpuContextScope&) = delete;
private:
CpuContext* previous_ = nullptr;
uint32_t savedMxcsr_ = 0;
};
inline CpuContext* TryGetCpuContext() noexcept
{
return g_currentCpuContext;
}
inline CpuContext* CurrentCpuContext()
{
CpuContext* cpu = TryGetCpuContext();
if (!cpu) {
std::cerr << "[runtime] CRITICAL: CurrentCpuContext is NULL. "
<< "Did you forget to create a CpuContextScope?" << std::endl;
ShowRuntimeFatalPopup("Runtime context failure",
"The game stopped because a translated function tried to run without a CPU context.");
std::abort();
}
if (cpu->gpr[1] == 0) {
std::cerr << "[runtime] CRITICAL: Guest Stack Pointer (r1) is NULL (0x00000000). "
<< "The emulated program has crashed." << std::endl;
ShowRuntimeFatalPopup("Guest execution failure",
"The game stopped because the guest stack pointer became null while translated code was running.");
std::abort(); // Stop immediately so you can debug the cause.
}
return cpu;
}
// Condition Register Fields
#define CR_LT 0
#define CR_GT 1
#define CR_EQ 2
#define CR_SO 3
extern "C" void DumpHostStackTraceForRuntimeHelper();
void MarkFatalErrorReported();
[[noreturn]] inline void PPC_Undefined(uint32_t pc, uint32_t rawInstruction, const char* details)
{
std::fprintf(stderr,
"[runtime] UNDEFINED guest instruction: pc=0x%08X raw=0x%08X %s\n",
pc,
rawInstruction,
details ? details : "");
char message[256]{};
std::snprintf(message, sizeof(message),
"The game stopped because it reached an unsupported guest instruction at PC 0x%08X (instruction 0x%08X).\n\n%s",
pc, rawInstruction, details ? details : "No additional details were provided.");
ShowRuntimeFatalPopup("Unsupported guest instruction", message);
std::abort();
}
// Used by generated code when the translator encounters privileged/unmodeled PPC instructions
// (e.g. rfi). This is intentionally loud so missing HLE hooks are easy to find.
#define UNDEFINED(pc, rawInstruction, details) PPC_Undefined((pc), (rawInstruction), (details))
// Helper to set CR bits (Signed)
inline void SetCR(CpuContext* cpu, int field, int32_t a, int32_t b) {
uint32_t crField = 0;
if (a < b) crField |= 0x8; // LT
if (a > b) crField |= 0x4; // GT
if (a == b) crField |= 0x2; // EQ
crField |= (cpu->xer >> 31) & 1u; // SO
int shift = (7 - field) * 4;
uint32_t mask = 0xF << shift;
cpu->cr = (cpu->cr & ~mask) | (crField << shift);
}
// Helper to set CR bits (Unsigned)
inline void SetCR(CpuContext* cpu, int field, uint32_t a, uint32_t b) {
uint32_t crField = 0;
if (a < b) crField |= 0x8; // LT
if (a > b) crField |= 0x4; // GT
if (a == b) crField |= 0x2; // EQ
crField |= (cpu->xer >> 31) & 1u; // SO
int shift = (7 - field) * 4;
uint32_t mask = 0xF << shift;
cpu->cr = (cpu->cr & ~mask) | (crField << shift);
}
// Helper to set CR bits (Floating-point)
inline void SetCRFloat(CpuContext* cpu, int field, double a, double b) {
uint32_t crField = 0;
if (std::isnan(a) || std::isnan(b)) {
// Unordered: LT/GT/EQ clear, SO set.
crField = 0x1;
} else {
if (a < b) crField |= 0x8; // LT
if (a > b) crField |= 0x4; // GT
if (a == b) crField |= 0x2; // EQ
}
int shift = (7 - field) * 4;
uint32_t mask = 0xF << shift;
cpu->cr = (cpu->cr & ~mask) | (crField << shift);
}
// Helper to get CR bit
inline bool GetCRBit(CpuContext* cpu, int field, int bit) {
int shift = (7 - field) * 4 + (3 - bit);
return (cpu->cr >> shift) & 1;
}
+62
View File
@@ -0,0 +1,62 @@
#pragma once
#include <cmath>
#include <cstdint>
inline void SetCRResident(uint32_t& cr, uint32_t xer, int field, int32_t a, int32_t b) noexcept {
uint32_t value = (a < b ? 0x8u : 0u) | (a > b ? 0x4u : 0u) | (a == b ? 0x2u : 0u) | ((xer >> 31) & 1u);
const int shift = (7 - field) * 4;
cr = (cr & ~(0xFu << shift)) | (value << shift);
}
inline void SetCRResident(uint32_t& cr, uint32_t xer, int field, uint32_t a, uint32_t b) noexcept {
uint32_t value = (a < b ? 0x8u : 0u) | (a > b ? 0x4u : 0u) | (a == b ? 0x2u : 0u) | ((xer >> 31) & 1u);
const int shift = (7 - field) * 4;
cr = (cr & ~(0xFu << shift)) | (value << shift);
}
inline void SetCRFloatResident(uint32_t& cr, int field, double a, double b) noexcept {
uint32_t value = (std::isnan(a) || std::isnan(b)) ? 0x1u :
((a < b ? 0x8u : 0u) | (a > b ? 0x4u : 0u) | (a == b ? 0x2u : 0u));
const int shift = (7 - field) * 4;
cr = (cr & ~(0xFu << shift)) | (value << shift);
}
inline bool GetCRBitResident(uint32_t cr, int field, int bit) noexcept {
const int shift = (7 - field) * 4 + (3 - bit);
return ((cr >> shift) & 1u) != 0u;
}
inline uint32_t PpcCrSetBitResident(uint32_t cr, uint32_t bitIndex, uint32_t value) noexcept {
const uint32_t mask = 1u << (31u - (bitIndex & 31u));
return (value & 1u) != 0 ? (cr | mask) : (cr & ~mask);
}
inline uint32_t PpcCrLogicalResident(
uint32_t cr, uint32_t op, uint32_t bt, uint32_t ba, uint32_t bb) noexcept {
const auto readBit = [cr](uint32_t index) noexcept {
return (cr >> (31u - (index & 31u))) & 1u;
};
const uint32_t a = readBit(ba);
const uint32_t b = readBit(bb);
uint32_t result = 0;
switch (op & 7u) {
case 0: result = ~(a | b) & 1u; break;
case 1: result = a & (~b & 1u); break;
case 2: result = a ^ b; break;
case 3: result = ~(a & b) & 1u; break;
case 4: result = a & b; break;
case 5: result = ~(a ^ b) & 1u; break;
case 6: result = (~a & 1u) | b; break;
case 7: result = a | b; break;
}
return PpcCrSetBitResident(cr, bt, result);
}
inline uint32_t PpcMcrfResident(uint32_t cr, uint32_t dstField, uint32_t srcField) noexcept {
dstField &= 7u;
srcField &= 7u;
const uint32_t dstShift = (7u - dstField) * 4u;
const uint32_t srcShift = (7u - srcField) * 4u;
const uint32_t field = (cr >> srcShift) & 0xFu;
return (cr & ~(0xFu << dstShift)) | (field << dstShift);
}
+858
View File
@@ -0,0 +1,858 @@
// SPDX-License-Identifier: GPL-2.0-or-later
//
// Copyright 2018 Dolphin Emulator Project
//
// Portions of this file are derived from the Dolphin Emulator
// (https://github.com/dolphin-emu/dolphin):
//
// * Source/Core/Common/FloatUtils.cpp - the Gekko/Broadway fres and frsqrte
// estimate tables (kPpcFresEstimateInline / kPpcFrsqrteEstimateInline,
// upstream fres_expected / frsqrte_expected) together with the
// PpcApproximateReciprocalInline and PpcApproximateReciprocalSquareRootInline
// interpolation routines, which are ports of upstream ApproximateReciprocal
// and ApproximateReciprocalSquareRoot.
//
// The remainder of this header is original to this project. It is
// GPL-2.0-or-later as a consequence of the above; see THIRD-PARTY-NOTICES.md.
#pragma once
// Pure floating-point and paired-single PowerPC semantics: the scalar single
// and double families, every ps_* arithmetic form, the NI flush rules and the
// Gekko fres/frsqrte estimates. Nothing in this header touches guest memory -
// the psq_l/psq_st tier that does lives in ppc_isa_quantized.h.
#include "ppc_isa_config.h"
#include "ppc_isa_context.h"
#include <array>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <limits>
inline uint64_t PpcBitCastToU64Inline(double value)
{
uint64_t integral = 0;
std::memcpy(&integral, &value, sizeof(integral));
return integral;
}
inline uint32_t PPC_FprLowWordInline(double value)
{
return static_cast<uint32_t>(PpcBitCastToU64Inline(value));
}
inline double PpcBitCastToDoubleInline(uint64_t value)
{
double result = 0.0;
std::memcpy(&result, &value, sizeof(result));
return result;
}
inline uint32_t PpcBitCastToU32Inline(float value)
{
uint32_t integral = 0;
std::memcpy(&integral, &value, sizeof(integral));
return integral;
}
inline float PpcBitCastToFloatInline(uint32_t value)
{
float result = 0.0f;
std::memcpy(&result, &value, sizeof(result));
return result;
}
// NOTE: PpcGetPs0Inline / PpcGetPs1Inline / PpcPackPairedInline are defined
// below the paired-single helper block (PpcPsToM128Inline and
// friends) because their register-domain implementations are written in terms
// of those helpers. Nothing between here and there uses them.
inline double PpcGetPairedFprInline(const PPC_FPR& fpr)
{
return fpr.d;
}
inline void PpcSetPairedFprInline(PPC_FPR& fpr, double packed)
{
fpr.d = packed;
}
// Must stay inside the XMM register domain. Bitcasting through a 64-bit GPR added a movq
// domain crossing on every paired-single op (630 in the THP IDCT region alone); a double
// local already lives in an XMM register, so these casts compile to nothing.
inline __m128 PpcPsToM128Inline(double value)
{
return _mm_castpd_ps(_mm_set_sd(value));
}
inline double PpcM128ToPsInline(__m128 value)
{
return _mm_cvtsd_f64(_mm_castps_pd(value));
}
inline __m128 PpcBroadcastPs0Inline(double value)
{
const __m128 lanes = PpcPsToM128Inline(value);
return _mm_shuffle_ps(lanes, lanes, _MM_SHUFFLE(1, 1, 1, 1));
}
inline __m128 PpcBroadcastPs1Inline(double value)
{
const __m128 lanes = PpcPsToM128Inline(value);
return _mm_shuffle_ps(lanes, lanes, _MM_SHUFFLE(0, 0, 0, 0));
}
inline __m128 PpcNegateNonNanLanesInline(__m128 value)
{
const __m128 signMask = _mm_castsi128_ps(_mm_set1_epi32(static_cast<int>(0x80000000u)));
const __m128 negated = _mm_xor_ps(value, signMask);
const __m128 ordered = _mm_cmpord_ps(value, value);
return _mm_or_ps(_mm_and_ps(ordered, negated), _mm_andnot_ps(ordered, value));
}
// Paired-single lane accessors. The packed double's LOW 32 bits hold ps1 and HIGH 32 bits
// hold ps0, so in the SSE float-lane view lane 0 == ps1 and lane 1 == ps0. Must stay pure
// register-domain shuffles (no arithmetic/conversion, so NaN/denormal bits pass through);
// the former union-based forms store-then-reloaded through memory, a guaranteed
// store-to-load-forwarding stall on every scalar-lane op.
inline float PpcGetPs0Inline(double value)
{
// ps0 lives in lane 1; PpcBroadcastPs0Inline already splats it.
return _mm_cvtss_f32(PpcBroadcastPs0Inline(value));
}
inline float PpcGetPs1Inline(double value)
{
// ps1 is already lane 0 of the packed representation.
return _mm_cvtss_f32(PpcPsToM128Inline(value));
}
inline double PpcPackPairedInline(float ps0, float ps1)
{
// _mm_unpacklo_ps(x, y) -> { x[0], y[0], x[1], y[1] }, so lane 0 becomes
// ps1 and lane 1 becomes ps0, matching the union layout bit for bit.
return PpcM128ToPsInline(_mm_unpacklo_ps(_mm_set_ss(ps1), _mm_set_ss(ps0)));
}
// FPSCR[NI] is modeled by MXCSR FTZ/DAZ, so arithmetic output flushing compiles to nothing.
// Two cases still need a software check against the mirrored bit (not STMXCSR, too hot):
// double->single conversion (CVTSD2SS isn't covered by FTZ) and raw lane pass-through moves.
inline bool MkwHostNiActiveInline() noexcept
{
return g_mkwHostNiActive;
}
inline float PpcForceSingleValueInline(double value)
{
// FPSCR[NI] flushes an exact pre-round single-subnormal even when rounding would promote it
// to the smallest normal. g_mkwNiFlushThreshold (2^-126 active, 0.0 inactive) turns the
// flush into a branchless mask: a set compare lane keeps just the sign bit, a clear lane
// passes the value to CVTSD2SS. DAZ (set exactly when NI is) makes the compare itself treat
// a subnormal as zero, matching the mask's answer.
const __m128d v = _mm_set_sd(value);
const __m128d signMask = _mm_set_sd(-0.0);
const __m128d magnitude = _mm_andnot_pd(signMask, v);
const __m128d flush = _mm_cmplt_sd(magnitude, _mm_set_sd(g_mkwNiFlushThreshold));
const __m128d kept = _mm_andnot_pd(_mm_andnot_pd(signMask, flush), v);
return static_cast<float>(_mm_cvtsd_f64(kept));
}
inline float PpcFlushSingleForNiInline(float value)
{
if (!MkwHostNiActiveInline())
return value;
const uint32_t bits = PpcBitCastToU32Inline(value);
if ((bits & 0x7FFFFFFFu) < 0x00800000u) [[unlikely]]
return PpcBitCastToFloatInline(bits & 0x80000000u);
return value;
}
inline double PpcFlushPairedForNiInline(double value)
{
// Callers pass results of SSE arithmetic; MXCSR.FTZ already flushed them.
return value;
}
inline double PpcForce25BitInline(double value)
{
constexpr uint64_t kDoubleExpMask = 0x7FF0000000000000ULL;
constexpr uint64_t kDoubleFracMask = 0x000FFFFFFFFFFFFFULL;
constexpr int kDoubleFracWidth = 52;
uint64_t integral = PpcBitCastToU64Inline(value);
const uint64_t exponent = integral & kDoubleExpMask;
const uint64_t fraction = integral & kDoubleFracMask;
if (exponent == 0 && fraction != 0)
{
int64_t keepMask = 0xFFFFFFFFF8000000LL;
uint64_t round = 0x8000000ULL;
uint32_t leadingZeros = 0;
uint64_t normalizedFraction = fraction;
while ((normalizedFraction & (1ULL << 63)) == 0)
{
normalizedFraction <<= 1;
++leadingZeros;
}
const uint32_t shift = leadingZeros - (63 - kDoubleFracWidth);
keepMask >>= shift;
round >>= shift;
integral = (integral & static_cast<uint64_t>(keepMask)) + (integral & round);
}
else
{
integral = (integral & 0xFFFFFFFFF8000000ULL) + (integral & 0x8000000ULL);
}
return PpcBitCastToDoubleInline(integral);
}
struct PpcEstimateEntryInline
{
int32_t base;
int32_t decrement;
};
inline constexpr std::array<PpcEstimateEntryInline, 32> kPpcFresEstimateInline = {{
{0x7ff800, 0x3e1}, {0x783800, 0x3a7}, {0x70ea00, 0x371}, {0x6a0800, 0x340},
{0x638800, 0x313}, {0x5d6200, 0x2ea}, {0x579000, 0x2c4}, {0x520800, 0x2a0},
{0x4cc800, 0x27f}, {0x47ca00, 0x261}, {0x430800, 0x245}, {0x3e8000, 0x22a},
{0x3a2c00, 0x212}, {0x360800, 0x1fb}, {0x321400, 0x1e5}, {0x2e4a00, 0x1d1},
{0x2aa800, 0x1be}, {0x272c00, 0x1ac}, {0x23d600, 0x19b}, {0x209e00, 0x18b},
{0x1d8800, 0x17c}, {0x1a9000, 0x16e}, {0x17ae00, 0x15b}, {0x14f800, 0x15b},
{0x124400, 0x143}, {0x0fbe00, 0x143}, {0x0d3800, 0x12d}, {0x0ade00, 0x12d},
{0x088400, 0x11a}, {0x065000, 0x11a}, {0x041c00, 0x108}, {0x020c00, 0x106},
}};
inline double PpcApproximateReciprocalInline(double value)
{
constexpr uint64_t kSign = 0x8000000000000000ULL;
constexpr uint64_t kExponent = 0x7FF0000000000000ULL;
constexpr uint64_t kFraction = 0x000FFFFFFFFFFFFFULL;
constexpr uint64_t kQuietBit = 0x0008000000000000ULL;
const uint64_t input = PpcBitCastToU64Inline(value);
const uint64_t mantissa = input & kFraction;
const uint64_t sign = input & kSign;
uint64_t exponent = input & kExponent;
if (mantissa == 0 && exponent == 0)
return PpcBitCastToDoubleInline(sign | kExponent);
if (exponent == kExponent)
{
if (mantissa == 0)
return PpcBitCastToDoubleInline(sign);
return PpcBitCastToDoubleInline(input | kQuietBit);
}
if (exponent < (uint64_t{895} << 52))
return std::copysign(static_cast<double>(std::numeric_limits<float>::max()), value);
if (exponent >= (uint64_t{1149} << 52))
return std::copysign(0.0, value);
exponent = (uint64_t{0x7FD} << 52) - exponent;
const int index = static_cast<int>(mantissa >> 37);
const auto& entry = kPpcFresEstimateInline[static_cast<size_t>(index / 1024)];
const int64_t estimate = static_cast<int64_t>(entry.base) -
(static_cast<int64_t>(entry.decrement) * (index % 1024) + 1) / 2;
return PpcBitCastToDoubleInline(
sign | exponent | (static_cast<uint64_t>(estimate) << 29));
}
// Broadway/Gekko frsqrte lookup table. These constants and the interpolation
// below match the algorithm used by the checked-in Dolphin reference rather
// than substituting an exact host reciprocal square root. This lives in the
// header because frsqrte is emitted at 281 translated sites and an out-of-line
// call is a full register barrier at each of them.
inline constexpr std::array<PpcEstimateEntryInline, 32> kPpcFrsqrteEstimateInline = {{
{0x1a7e800, -0x568}, {0x17cb800, -0x4f3}, {0x1552800, -0x48d}, {0x130c000, -0x435},
{0x10f2000, -0x3e7}, {0x0eff000, -0x3a2}, {0x0d2e000, -0x365}, {0x0b7c000, -0x32e},
{0x09e5000, -0x2fc}, {0x0867000, -0x2d0}, {0x06ff000, -0x2a8}, {0x05ab800, -0x283},
{0x046a000, -0x261}, {0x0339800, -0x243}, {0x0218800, -0x226}, {0x0105800, -0x20b},
{0x3ffa000, -0x7a4}, {0x3c29000, -0x700}, {0x38aa000, -0x670}, {0x3572000, -0x5f2},
{0x3279000, -0x584}, {0x2fb7000, -0x524}, {0x2d26000, -0x4cc}, {0x2ac0000, -0x47e},
{0x2881000, -0x43a}, {0x2665000, -0x3fa}, {0x2468000, -0x3c2}, {0x2287000, -0x38e},
{0x20c1000, -0x35e}, {0x1f12000, -0x332}, {0x1d79000, -0x30a}, {0x1bf4000, -0x2e6},
}};
inline double PpcApproximateReciprocalSquareRootInline(double value)
{
constexpr uint64_t kSign = 0x8000000000000000ULL;
constexpr uint64_t kExponent = 0x7FF0000000000000ULL;
constexpr uint64_t kFraction = 0x000FFFFFFFFFFFFFULL;
constexpr uint64_t kQuietBit = 0x0008000000000000ULL;
constexpr uint64_t kCanonicalQuietNaN = kExponent | kQuietBit;
const uint64_t input = PpcBitCastToU64Inline(value);
uint64_t mantissa = input & kFraction;
const uint64_t sign = input & kSign;
int64_t exponent = static_cast<int64_t>(input & kExponent);
if (mantissa == 0 && exponent == 0)
{
return PpcBitCastToDoubleInline(sign | kExponent);
}
if (static_cast<uint64_t>(exponent) == kExponent)
{
if (mantissa == 0)
{
return sign ? PpcBitCastToDoubleInline(kCanonicalQuietNaN) : 0.0;
}
return PpcBitCastToDoubleInline(input | kQuietBit);
}
if (sign != 0)
{
return PpcBitCastToDoubleInline(kCanonicalQuietNaN);
}
if (exponent == 0)
{
// Normalize a subnormal while allowing the signed exponent to extend
// below the IEEE-754 encoded range, exactly as the hardware estimate
// interpolation expects.
do
{
exponent -= int64_t{1} << 52;
mantissa <<= 1;
} while ((mantissa & (uint64_t{1} << 52)) == 0);
mantissa &= kFraction;
exponent += int64_t{1} << 52;
}
const int64_t exponentLsb = exponent & (int64_t{1} << 52);
exponent = (((int64_t{0x3FF} << 52) -
((exponent - (int64_t{0x3FE} << 52)) / 2)) &
static_cast<int64_t>(kExponent));
const int index = static_cast<int>(
(static_cast<uint64_t>(exponentLsb) | mantissa) >> 37);
const auto& entry = kPpcFrsqrteEstimateInline[static_cast<size_t>(index / 2048)];
const int64_t estimate =
static_cast<int64_t>(entry.base) +
static_cast<int64_t>(entry.decrement) * (index % 2048);
const uint64_t result = sign | static_cast<uint64_t>(exponent) |
(static_cast<uint64_t>(estimate) << 26);
return PpcBitCastToDoubleInline(result);
}
// fctiwz: convert to a 32-bit signed integer with round-toward-zero and place
// the result in the LOW 32 bits of the FPR. The upper 32 bits are
// architecturally undefined; zero is what the hardware leaves in practice and
// what stfd/lwz+4 sequences in guest code expect to read back.
inline int32_t PpcClampIntegerWordInline(double value)
{
if (std::isnan(value)) {
return static_cast<int32_t>(0x80000000u);
}
if (value >= 2147483647.0) {
return 2147483647;
}
if (value <= -2147483648.0) {
return static_cast<int32_t>(0x80000000u);
}
return static_cast<int32_t>(value);
}
inline double PpcPackIntegerWordInline(int32_t value)
{
return PpcBitCastToDoubleInline(
static_cast<uint64_t>(static_cast<uint32_t>(value)));
}
template <bool Subtract>
inline float PpcAccuratePsMaddLaneInline(float a, float c, float b)
{
const float signedB = Subtract ? -b : b;
// Paired values are stored as exact float32 lanes in this runtime. An
// explicit float FMA therefore matches Gekko's single rounding point;
// multiplying and adding separately can differ by an ULP on ordinary
// vector and matrix workloads.
return PpcFlushSingleForNiInline(std::fma(a, c, signedB));
}
template <bool Subtract>
inline float PpcAccuratePsMaddLaneNoNiInline(float a, float c, float b)
{
const float signedB = Subtract ? -b : b;
return std::fma(a, c, signedB);
}
template <bool Subtract>
inline double PpcAccurateSingleMaddIntermediateInline(double a, double c, double b)
{
// Gekko single-precision fused operations keep the full precision of A
// and B, round C to a 25-bit significand, and round only the final result
// to float32. A double FMA is almost sufficient, but an exact result just
// beyond a float32 halfway point can be rounded to a double tie first.
// Recover the direction of that discarded error for those tie cases.
const double roundedC = PpcForce25BitInline(c);
const double signedB = Subtract ? -b : b;
double result = std::fma(a, roundedC, signedB);
const uint64_t resultBits = PpcBitCastToU64Inline(result);
constexpr uint64_t kDiscardedMask = 0x000000001FFFFFFFULL;
constexpr uint64_t kEvenTie = 0x0000000010000000ULL;
if ((resultBits & kDiscardedMask) == kEvenTie)
{
const double aPrime = signedB - result;
const double bPrime = result + aPrime;
const double deltaA = std::fma(a, roundedC, aPrime);
const double deltaB = signedB - bPrime;
const double error = deltaA + deltaB;
if (error != 0.0)
{
result = PpcBitCastToDoubleInline(
(error > 0.0) == (result > 0.0) ? resultBits + 1 : resultBits - 1);
}
}
return result;
}
inline double PpcFmulsInline(double a, double c)
{
return static_cast<double>(
PpcForceSingleValueInline(a * PpcForce25BitInline(c)));
}
inline double PpcFmulsNoNiInline(double a, double c)
{
return static_cast<double>(static_cast<float>(a * PpcForce25BitInline(c)));
}
inline double PpcFmaddInline(double a, double c, double b)
{
return std::fma(a, c, b);
}
inline double PpcFmsubInline(double a, double c, double b)
{
return std::fma(a, c, -b);
}
inline double PpcFnmaddInline(double a, double c, double b)
{
const double result = std::fma(a, c, b);
return std::isnan(result) ? result : -result;
}
inline double PpcFnmsubInline(double a, double c, double b)
{
const double result = std::fma(a, c, -b);
return std::isnan(result) ? result : -result;
}
inline double PpcFmaddsInline(double a, double c, double b)
{
return static_cast<double>(
PpcForceSingleValueInline(PpcAccurateSingleMaddIntermediateInline<false>(a, c, b)));
}
inline double PpcFmsubsInline(double a, double c, double b)
{
return static_cast<double>(
PpcForceSingleValueInline(PpcAccurateSingleMaddIntermediateInline<true>(a, c, b)));
}
inline double PpcFnmaddsInline(double a, double c, double b)
{
const float result = PpcForceSingleValueInline(
PpcAccurateSingleMaddIntermediateInline<false>(a, c, b));
return static_cast<double>(std::isnan(result) ? result : -result);
}
inline double PpcFnmsubsInline(double a, double c, double b)
{
const float result = PpcForceSingleValueInline(
PpcAccurateSingleMaddIntermediateInline<true>(a, c, b));
return static_cast<double>(std::isnan(result) ? result : -result);
}
inline double PPC_PsMulInline(double lhs, double rhs)
{
return PpcFlushPairedForNiInline(
PpcM128ToPsInline(_mm_mul_ps(PpcPsToM128Inline(lhs), PpcPsToM128Inline(rhs))));
}
inline double PPC_PsMulNoNiInline(double lhs, double rhs)
{
return PpcM128ToPsInline(_mm_mul_ps(PpcPsToM128Inline(lhs), PpcPsToM128Inline(rhs)));
}
// The paired madd family lowers to one hardware FMA. Semantics match the scalar lanes exactly: a
// single fused rounding per lane (std::fma(float) == vfmaddps per lane), and
// the negate-unless-NaN behavior of the nmadd/nmsub forms is expressed with
// PpcNegateNonNanLanesInline. NI flushing is handled by MXCSR (see
// MkwApplyHostNiMode), so the NI and NoNi entry points are identical here.
inline double PPC_PsMsubInline(double multiplicand, double multiplier, double subtractor)
{
return PpcM128ToPsInline(_mm_fmsub_ps(
PpcPsToM128Inline(multiplicand), PpcPsToM128Inline(multiplier), PpcPsToM128Inline(subtractor)));
}
inline double PPC_PsMsubNoNiInline(double multiplicand, double multiplier, double subtractor)
{
return PPC_PsMsubInline(multiplicand, multiplier, subtractor);
}
inline double PPC_PsMaddInline(double multiplicand, double multiplier, double addend)
{
return PpcM128ToPsInline(_mm_fmadd_ps(
PpcPsToM128Inline(multiplicand), PpcPsToM128Inline(multiplier), PpcPsToM128Inline(addend)));
}
inline double PPC_PsMaddNoNiInline(double multiplicand, double multiplier, double addend)
{
return PPC_PsMaddInline(multiplicand, multiplier, addend);
}
inline double PPC_PsMadds0Inline(double multiplicand, double multiplier, double addend)
{
return PpcM128ToPsInline(_mm_fmadd_ps(
PpcPsToM128Inline(multiplicand), PpcBroadcastPs0Inline(multiplier), PpcPsToM128Inline(addend)));
}
inline double PPC_PsMadds1Inline(double multiplicand, double multiplier, double addend)
{
return PpcM128ToPsInline(_mm_fmadd_ps(
PpcPsToM128Inline(multiplicand), PpcBroadcastPs1Inline(multiplier), PpcPsToM128Inline(addend)));
}
inline double PPC_PsNmsubInline(double multiplicand, double multiplier, double subtractor)
{
return PpcM128ToPsInline(PpcNegateNonNanLanesInline(_mm_fmsub_ps(
PpcPsToM128Inline(multiplicand), PpcPsToM128Inline(multiplier), PpcPsToM128Inline(subtractor))));
}
inline double PPC_PsNmsubNoNiInline(double multiplicand, double multiplier, double subtractor)
{
return PPC_PsNmsubInline(multiplicand, multiplier, subtractor);
}
inline double PPC_PsNmaddInline(double multiplicand, double multiplier, double addend)
{
return PpcM128ToPsInline(PpcNegateNonNanLanesInline(_mm_fmadd_ps(
PpcPsToM128Inline(multiplicand), PpcPsToM128Inline(multiplier), PpcPsToM128Inline(addend))));
}
inline double PPC_PsMuls0Inline(double aValue, double cValue)
{
return PpcFlushPairedForNiInline(PpcM128ToPsInline(
_mm_mul_ps(PpcPsToM128Inline(aValue), PpcBroadcastPs0Inline(cValue))));
}
inline double PPC_PsMuls1Inline(double aValue, double cValue)
{
return PpcFlushPairedForNiInline(PpcM128ToPsInline(
_mm_mul_ps(PpcPsToM128Inline(aValue), PpcBroadcastPs1Inline(cValue))));
}
inline PPC_FPR PpcMakePairedResultInline(float ps0, float ps1);
inline double PPC_PsFromScalarInline(double value)
{
// Representation conversion, not an architectural operation: Gekko has no
// "scalar to paired" instruction, so there is no NI rounding point here.
// If the scalar is a single-denormal it stays one; MXCSR.DAZ flushes it as
// an input at the next real arithmetic op, exactly like the hardware.
const float single = static_cast<float>(value);
return PpcPackPairedInline(single, single);
}
inline double PPC_PsFromScalarNoNiInline(double value)
{
const float single = static_cast<float>(value);
return PpcPackPairedInline(single, single);
}
inline double PPC_PsToScalarInline(double value)
{
return static_cast<double>(PpcGetPs0Inline(value));
}
// ps_merge* are pure lane selections (result.ps0 from frA, result.ps1 from frB); with lane 0
// == ps1 and lane 1 == ps0, two shuffles build the result bit-exact instead of round-tripping
// through the pack helper.
inline double PPC_PsMerge00Inline(double aValue, double bValue)
{
// lane0 = b.ps0 (b lane 1), lane1 = a.ps0 (a lane 1)
const __m128 gathered = _mm_shuffle_ps(
PpcPsToM128Inline(bValue), PpcPsToM128Inline(aValue), _MM_SHUFFLE(1, 1, 1, 1));
return PpcM128ToPsInline(_mm_shuffle_ps(gathered, gathered, _MM_SHUFFLE(0, 0, 2, 0)));
}
inline double PPC_PsMerge01Inline(double aValue, double bValue)
{
// lane0 = b.ps1 (b lane 0), lane1 = a.ps0 (a lane 1)
const __m128 gathered = _mm_shuffle_ps(
PpcPsToM128Inline(bValue), PpcPsToM128Inline(aValue), _MM_SHUFFLE(1, 1, 0, 0));
return PpcM128ToPsInline(_mm_shuffle_ps(gathered, gathered, _MM_SHUFFLE(0, 0, 2, 0)));
}
inline double PPC_PsMerge10Inline(double aValue, double bValue)
{
// lane0 = b.ps0 (b lane 1), lane1 = a.ps1 (a lane 0)
const __m128 gathered = _mm_shuffle_ps(
PpcPsToM128Inline(bValue), PpcPsToM128Inline(aValue), _MM_SHUFFLE(0, 0, 1, 1));
return PpcM128ToPsInline(_mm_shuffle_ps(gathered, gathered, _MM_SHUFFLE(0, 0, 2, 0)));
}
inline double PPC_PsMerge11Inline(double aValue, double bValue)
{
// lane0 = b.ps1 (b lane 0), lane1 = a.ps1 (a lane 0): plain unpcklps.
return PpcM128ToPsInline(
_mm_unpacklo_ps(PpcPsToM128Inline(bValue), PpcPsToM128Inline(aValue)));
}
inline double PPC_PsAddInline(double aValue, double bValue)
{
return PpcFlushPairedForNiInline(
PpcM128ToPsInline(_mm_add_ps(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue))));
}
inline double PPC_PsAddNoNiInline(double aValue, double bValue)
{
return PpcM128ToPsInline(
_mm_add_ps(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue)));
}
inline double PPC_PsSelInline(double lhsValue, double controlValue, double rhsValue)
{
const float lhs0 = PpcGetPs0Inline(lhsValue);
const float lhs1 = PpcGetPs1Inline(lhsValue);
const float control0 = PpcGetPs0Inline(controlValue);
const float control1 = PpcGetPs1Inline(controlValue);
const float rhs0 = PpcGetPs0Inline(rhsValue);
const float rhs1 = PpcGetPs1Inline(rhsValue);
return PpcPackPairedInline(
control0 >= -0.0f ? lhs0 : rhs0,
control1 >= -0.0f ? lhs1 : rhs1);
}
inline double PPC_PsSubInline(double aValue, double bValue)
{
return PpcFlushPairedForNiInline(
PpcM128ToPsInline(_mm_sub_ps(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue))));
}
inline double PPC_PsSubNoNiInline(double aValue, double bValue)
{
return PpcM128ToPsInline(
_mm_sub_ps(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue)));
}
inline double PPC_PsDivInline(double aValue, double bValue)
{
return PpcFlushPairedForNiInline(
PpcM128ToPsInline(_mm_div_ps(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue))));
}
inline double PPC_PsNegInline(double value)
{
return PpcPackPairedInline(-PpcGetPs0Inline(value), -PpcGetPs1Inline(value));
}
inline double PPC_PsAbsInline(double value)
{
return PpcPackPairedInline(std::abs(PpcGetPs0Inline(value)), std::abs(PpcGetPs1Inline(value)));
}
inline double PPC_PsSum0Inline(double aValue, double bValue, double cValue)
{
return PpcPackPairedInline(
PpcForceSingleValueInline(static_cast<double>(PpcGetPs0Inline(aValue)) + PpcGetPs1Inline(bValue)),
PpcFlushSingleForNiInline(PpcGetPs1Inline(cValue)));
}
inline double PPC_PsSum1Inline(double aValue, double bValue, double cValue)
{
return PpcPackPairedInline(
PpcFlushSingleForNiInline(PpcGetPs0Inline(cValue)),
PpcForceSingleValueInline(static_cast<double>(PpcGetPs0Inline(aValue)) + PpcGetPs1Inline(bValue)));
}
inline uint32_t PpcConvertToSingleFTZInline(uint64_t value)
{
const uint32_t exp = static_cast<uint32_t>((value >> 52) & 0x7FFu);
if (exp > 896u || (value & 0x7FFFFFFFFFFFFFFFULL) == 0)
{
return static_cast<uint32_t>(((value >> 32) & 0xC0000000ULL) |
((value >> 29) & 0x3FFFFFFFULL));
}
return static_cast<uint32_t>((value >> 32) & 0x80000000ULL);
}
inline uint64_t PpcConvertToDoubleBitsInline(uint32_t value)
{
uint64_t x = value;
uint64_t exp = (x >> 23) & 0xFFu;
uint64_t frac = x & 0x007FFFFFu;
if (exp > 0 && exp < 255)
{
const uint64_t y = !(exp >> 7);
const uint64_t z = (y << 61) | (y << 60) | (y << 59);
return ((x & 0xC0000000ULL) << 32) | z | ((x & 0x3FFFFFFFULL) << 29);
}
if (exp == 0 && frac != 0)
{
exp = 1023 - 126;
do
{
frac <<= 1;
--exp;
} while ((frac & 0x00800000u) == 0);
return ((x & 0x80000000ULL) << 32) | (exp << 52) | ((frac & 0x007FFFFFULL) << 29);
}
const uint64_t y = exp >> 7;
const uint64_t z = (y << 61) | (y << 60) | (y << 59);
return ((x & 0xC0000000ULL) << 32) | z | ((x & 0x3FFFFFFFULL) << 29);
}
inline double PpcPackPairedBitsInline(uint32_t ps0, uint32_t ps1)
{
return PpcBitCastToDoubleInline((static_cast<uint64_t>(ps0) << 32) | ps1);
}
inline PPC_FPR PpcMakePairedResultInline(float ps0, float ps1)
{
PPC_FPR result{};
result.paired.ps0 = ps0;
result.paired.ps1 = ps1;
return result;
}
extern "C" void PPC_Mtfsf(uint32_t fieldMask, double source);
extern "C" void PPC_Mtfsfi(uint32_t field, uint32_t value);
extern "C" void PPC_Mtfsb0(uint32_t bit);
extern "C" void PPC_Mtfsb1(uint32_t bit);
extern "C" double PPC_Mffs();
extern "C" double PPC_PsAdd(double lhs, double rhs);
extern "C" double PPC_PsSub(double lhs, double rhs);
extern "C" double PPC_PsDiv(double lhs, double rhs);
extern "C" double PPC_PsNeg(double value);
extern "C" double PPC_PsMul(double lhs, double rhs);
extern "C" double PPC_PsMsub(double lhs, double mul, double sub);
extern "C" double PPC_PsMadd(double lhs, double mul, double add);
extern "C" double PPC_PsNmsub(double lhs, double mul, double sub);
extern "C" double PPC_PsMadds0(double lhs, double mul, double add);
extern "C" double PPC_PsMadds1(double lhs, double mul, double add);
extern "C" double PPC_PsNmadd(double lhs, double mul, double add);
extern "C" double PPC_PsSel(double lhs, double control, double rhs);
// ps_res is hot in the THP dequant path (148 static call sites in the IDCT
// region); the out-of-line definition cost a call + full spill barrier per
// use, so it is defined inline here. The estimate logic is byte-identical to
// the old ppc_helpers.cpp body (both used PpcApproximateReciprocalInline).
extern "C" inline double PPC_PsRes(double value)
{
const float ps0 = static_cast<float>(
PpcApproximateReciprocalInline(static_cast<double>(PpcGetPs0Inline(value))));
const float ps1 = static_cast<float>(
PpcApproximateReciprocalInline(static_cast<double>(PpcGetPs1Inline(value))));
return PpcPackPairedInline(ps0, ps1);
}
extern "C" double PPC_PsRsqrte(double value);
// Pack a scalar double into a paired-single FPR value (ps0=ps1=float(value)).
extern "C" double PPC_PsFromScalar(double value);
// Extract the ps0 lane as a scalar double (used to feed single-precision ops).
extern "C" double PPC_PsToScalar(double value);
extern "C" double PPC_PsMerge00(double a, double b);
extern "C" double PPC_PsMerge01(double a, double b);
extern "C" double PPC_PsMerge10(double a, double b);
extern "C" double PPC_PsMerge11(double a, double b);
extern "C" double PPC_Fsqrt(double value);
// Hot scalar float helpers, defined inline so the compiler can see through them instead of
// taking a cross-TU caller-saved register barrier at ~2,500 call sites in the hottest float
// code in the game (bodies moved verbatim from ppc_helpers.cpp/fpu_helpers.cpp). PPC_PsRes
// above uses the same pattern.
extern "C" inline double PPC_Fres(double value)
{
// The generated paired/scalar boundary presents fres as a packed input and
// expects its architecturally replicated single result in the same format.
const float result = static_cast<float>(
PpcApproximateReciprocalInline(static_cast<double>(PpcGetPs0Inline(value))));
return PpcPackPairedInline(result, result);
}
extern "C" inline double PPC_Frsqrte(double value)
{
// frsqrte (opcode 63) takes the full scalar-double input (generated code normalizes
// scalar/paired ownership beforehand) and returns exact Gekko estimate bits only; FPSCR/
// Rc/FPRF updates need instruction-level context this value-only helper doesn't have.
return PpcApproximateReciprocalSquareRootInline(value);
}
extern "C" inline double PPC_Fsel(double control, double negative, double positive)
{
// Dolphin models fsel/ps_sel as "fra >= -0.0 ? frC : frB".
// That comparison deliberately sends unordered/NaN controls to frB.
return (control >= -0.0) ? positive : negative;
}
// PowerPC fctiwz instruction: Float Convert to Integer Word with Round toward
// Zero. The result goes in the LOWER 32 bits of the FPR (bits 32-63); the upper
// 32 bits are undefined. When the value is stored via stfd and reloaded via lwz
// at offset +4, the integer is correctly retrieved.
extern "C" inline double PPC_Fctiwz(double value)
{
return PpcPackIntegerWordInline(PpcClampIntegerWordInline(value));
}
extern "C" double PPC_Fmadd(double multiplicand, double multiplier, double addend);
extern "C" double PPC_Fmsub(double multiplicand, double multiplier, double subtractor);
extern "C" double PPC_Fnmadd(double multiplicand, double multiplier, double addend);
extern "C" double PPC_Fnmsub(double multiplicand, double multiplier, double subtractor);
// Single-precision scalar helpers (fadds/fsubs/fmuls/fdivs and fused variants).
extern "C" double PPC_Fadds(double a, double b);
extern "C" double PPC_Fsubs(double a, double b);
extern "C" double PPC_Fmuls(double a, double b);
extern "C" double PPC_Fdivs(double a, double b);
// The fused single-precision family is a thin wrapper over the Ppc*Inline
// bodies above; keeping the wrapper out of line meant the wrapper itself was
// the register barrier. See the block at PPC_Fres for why there is no LTO to
// fall back on.
extern "C" inline double PPC_Fmadds(double multiplicand, double multiplier, double addend)
{
return PpcFmaddsInline(multiplicand, multiplier, addend);
}
extern "C" inline double PPC_Fmsubs(double multiplicand, double multiplier, double subtractor)
{
return PpcFmsubsInline(multiplicand, multiplier, subtractor);
}
extern "C" inline double PPC_Fnmadds(double multiplicand, double multiplier, double addend)
{
return PpcFnmaddsInline(multiplicand, multiplier, addend);
}
extern "C" inline double PPC_Fnmsubs(double multiplicand, double multiplier, double subtractor)
{
return PpcFnmsubsInline(multiplicand, multiplier, subtractor);
}
extern "C" double PPC_Fctiw(double value); // Convert float to int using FPSCR rounding mode
extern "C" void PPC_Stfiwx(uint32_t addr, double fprValue); // Store Float as Integer Word (indexed)
extern "C" double PPC_PsSum0(double a, double b, double c);
extern "C" double PPC_PsSum1(double a, double b, double c);
extern "C" double PPC_PsMuls0(double a, double c);
extern "C" double PPC_PsMuls1(double a, double c);
extern "C" double PPC_PsAbs(double value);
// Floating-point comparison helper (fcmpu/fcmpo)
extern "C" void PPC_Fcmp(uint32_t crField, double a, double b);
extern "C" void PPC_PsCmpo0(uint32_t crField, double a, double b);
extern "C" void PPC_PsCmpu0(uint32_t crField, double a, double b);
extern "C" void PPC_PsCmpo1(uint32_t crField, double a, double b);
extern "C" void PPC_PsCmpu1(uint32_t crField, double a, double b);
extern "C" double PPC_PsNabs(double value);
extern "C" double PPC_PsMr(double value);
+53
View File
@@ -0,0 +1,53 @@
#pragma once
// FPSCR[NI] (non-IEEE flush-to-zero) modeled on the host FP environment, plus
// the thread-local mirror of that state the hot paths read instead of MXCSR.
#include "ppc_isa_config.h"
#include <cstdint>
// Software-flushing Gekko's single-precision denormals per op roughly doubled the THP IDCT
// kernel's cycle count, so instead the runtime mirrors guest FPSCR[NI] into host MXCSR FTZ+DAZ
// wherever FPSCR can change (PPC_Mtfs*, fiber context switches, CpuContextScope), making per-op
// flushes free. Accepted deviations (same trade Dolphin makes): FTZ also flushes double
// denormals unlike real NI, and a pre-round-flush edge near FLT_MIN rounds via cvtsd2ss instead.
inline constexpr uint32_t kMkwMxcsrFlushToZeroBits = (1u << 15) | (1u << 6); // FTZ | DAZ
inline thread_local bool g_mkwHostNiActive = false;
// Same state in the form PpcForceSingleValueInline consumes: the pre-round subnormal threshold
// while NI is active, 0.0 (identity, `|value| < 0.0` is always false) otherwise, so that path
// needs no branch. Every writer of g_mkwHostNiActive must write this beside it in agreement.
inline constexpr double kMkwNiFlushThreshold = 0x1p-126; // 0x3810000000000000
inline thread_local double g_mkwNiFlushThreshold = 0.0;
inline void MkwApplyHostNiMode(uint32_t fpscr) noexcept
{
const uint32_t csr = _mm_getcsr();
const bool wantNi = (fpscr & 0x4u) != 0;
const uint32_t want = wantNi
? (csr | kMkwMxcsrFlushToZeroBits)
: (csr & ~kMkwMxcsrFlushToZeroBits);
if (want != csr)
_mm_setcsr(want);
// `want` has both bits set or both clear, so this is exactly
// `(_mm_getcsr() & kMkwMxcsrFlushToZeroBits) != 0` after the write - the
// mirror cannot disagree with the register even if the incoming CSR held
// only one of the two bits.
g_mkwHostNiActive = wantNi;
g_mkwNiFlushThreshold = wantNi ? kMkwNiFlushThreshold : 0.0;
}
/// <summary>
/// Restores a previously captured MXCSR value and re-derives the mirror from
/// it. Every raw restore has to go through here; a bare _mm_setcsr would leave
/// the mirror describing the FP environment that was just replaced.
/// </summary>
inline void MkwRestoreHostMxcsr(uint32_t csr) noexcept
{
_mm_setcsr(csr);
const bool niActive = (csr & kMkwMxcsrFlushToZeroBits) != 0;
g_mkwHostNiActive = niActive;
g_mkwNiFlushThreshold = niActive ? kMkwNiFlushThreshold : 0.0;
}
+127
View File
@@ -0,0 +1,127 @@
#pragma once
// Pure integer PowerPC semantics, together with the declarations of the
// integer, condition/SPR and system helpers the host runtime implements out of
// line. Nothing here depends on another isa/ header beyond the configuration.
#include "ppc_isa_config.h"
#include <cstdint>
#include <cstdlib>
#include <limits>
inline uint32_t PpcRotl32Inline(uint32_t value, uint32_t shift)
{
return __builtin_rotateleft32(value, shift);
}
extern "C" uint32_t OSSystemCall();
extern "C" int32_t memset_zero_32(int32_t address);
extern "C" void OS_HLE_ProcessAlarms(int maxToProcess);
extern "C" uint32_t PPC_Mcrxr(uint32_t crField);
extern "C" uint32_t PPC_ReadSpr(uint32_t spr);
extern "C" void PPC_WriteSpr(uint32_t spr, uint32_t value);
extern "C" uint32_t PPC_CrSetBit(uint32_t bitIndex, uint32_t value);
extern "C" uint32_t PPC_CrLogical(uint32_t op, uint32_t bt, uint32_t ba, uint32_t bb);
extern "C" uint32_t PPC_Mcrf(uint32_t dstField, uint32_t srcField);
// Time base helpers (PowerPC 'mftb' / 'mftbu') - declared so generated code can call them.
extern "C" uint32_t PPC_Mftb();
extern "C" uint32_t PPC_Mftbu();
// Carry helpers used by translated arithmetic that depends on XER[CA].
extern "C" uint32_t PPC_UpdateCarryAdd(uint32_t lhs, uint32_t rhs, uint32_t carryIn);
extern "C" uint32_t PPC_UpdateCarrySub(uint32_t lhs, uint32_t rhs);
extern "C" uint32_t PPC_UpdateCarryShiftRight(uint32_t value, uint32_t shift);
extern "C" uint32_t PPC_GetCarry();
extern "C" uint32_t PPC_Addo(uint32_t lhs, uint32_t rhs);
extern "C" uint32_t PPC_Addco(uint32_t lhs, uint32_t rhs);
extern "C" uint32_t PPC_Addeo(uint32_t lhs, uint32_t rhs);
extern "C" uint32_t PPC_Addmeo(uint32_t value);
extern "C" uint32_t PPC_Addzeo(uint32_t value);
extern "C" uint32_t PPC_Subfo(uint32_t subtrahend, uint32_t minuend);
extern "C" uint32_t PPC_Subfco(uint32_t subtrahend, uint32_t minuend);
extern "C" uint32_t PPC_Subfeo(uint32_t subtrahend, uint32_t minuend);
extern "C" uint32_t PPC_Subfmeo(uint32_t value);
extern "C" uint32_t PPC_Subfzeo(uint32_t value);
extern "C" uint32_t PPC_Nego(uint32_t value);
extern "C" uint32_t PPC_Mullwo(uint32_t lhs, uint32_t rhs);
extern "C" uint32_t PPC_Divwo(uint32_t lhs, uint32_t rhs);
extern "C" uint32_t PPC_Divwuo(uint32_t lhs, uint32_t rhs);
extern "C" void PPC_Lswi(uint32_t rD, uint32_t addr, uint32_t byteCount);
extern "C" void PPC_Lswx(uint32_t rD, uint32_t addr);
extern "C" void PPC_Stswi(uint32_t rS, uint32_t addr, uint32_t byteCount);
extern "C" void PPC_Stswx(uint32_t rS, uint32_t addr);
extern "C" uint32_t PPC_Lwarx(uint32_t addr);
extern "C" uint32_t PPC_Stwcx(uint32_t addr, uint32_t value);
extern "C" uint32_t PPC_Mcrfs(uint32_t dstField, uint32_t srcField);
extern "C" uint32_t PPC_Eciwx(uint32_t addr);
extern "C" void PPC_Ecowx(uint32_t addr, uint32_t value);
extern "C" void PPC_TrapWord(uint32_t trapOptions, uint32_t lhs, uint32_t rhs);
extern "C" uint32_t PPC_Cntlzw(uint32_t value);
MKW_PPC_FORCE_INLINE uint32_t PPC_CntlzwInline(uint32_t value)
{
return value == 0 ? 32u : static_cast<uint32_t>(__builtin_clz(value));
}
// Byte-reverse helpers (PowerPC 'lwbrx' / 'stwbrx' / 'lhbrx' / 'sthbrx')
extern "C" uint32_t PPC_LoadWordByteReverse(uint32_t addr);
extern "C" void PPC_StoreWordByteReverse(uint32_t addr, uint32_t value);
extern "C" uint32_t PPC_LoadHalfwordByteReverse(uint32_t addr);
extern "C" void PPC_StoreHalfwordByteReverse(uint32_t addr, uint32_t value);
template <typename T>
inline int32_t CompareUnsigned(T a, T b) {
uint32_t ua = static_cast<uint32_t>(a);
uint32_t ub = static_cast<uint32_t>(b);
if (ua < ub) return -1;
if (ua > ub) return 1;
return 0;
}
template <int Bits>
inline int32_t SignExtend(uint32_t val) {
struct { int32_t x : Bits; } s;
s.x = val;
return s.x;
}
inline int32_t ArithmeticShiftRight(int32_t val, int amount) {
return val >> amount;
}
inline uint32_t PPC_Slw(uint32_t value, uint32_t amount)
{
return (amount & 0x20u) != 0 ? 0u : value << (amount & 0x1Fu);
}
inline uint32_t PPC_Srw(uint32_t value, uint32_t amount)
{
return (amount & 0x20u) != 0 ? 0u : value >> (amount & 0x1Fu);
}
inline uint32_t PPC_Sraw(uint32_t value, uint32_t amount)
{
if ((amount & 0x20u) != 0)
{
return (value & 0x80000000u) != 0 ? 0xFFFFFFFFu : 0u;
}
return static_cast<uint32_t>(static_cast<int32_t>(value) >> (amount & 0x1Fu));
}
inline uint32_t PPC_Divwu(uint32_t dividend, uint32_t divisor)
{
// Gekko does not raise a program exception for non-OE division by zero.
// Match the hardware result used by Dolphin's interpreter/JIT.
return divisor == 0 ? 0u : dividend / divisor;
}
inline int32_t PPC_Divw(int32_t dividend, int32_t divisor)
{
if (divisor == 0 || (dividend == std::numeric_limits<int32_t>::min() && divisor == -1))
return dividend < 0 ? -1 : 0;
return dividend / divisor;
}
File diff suppressed because it is too large Load Diff
+116
View File
@@ -0,0 +1,116 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>
class Memory {
public:
using DeferredReadCallback = bool (*)(void* user);
static constexpr size_t kMem1Size = 24u * 1024u * 1024u;
static constexpr size_t kMem2Size = 128u * 1024u * 1024u;
static constexpr uint32_t kMem1PhysicalBase = 0x00000000u;
static constexpr uint32_t kMem1CachedBase = 0x80000000u;
static constexpr uint32_t kMem1UncachedBase = 0xC0000000u;
static constexpr uint32_t kMem2PhysicalBase = 0x10000000u;
static constexpr uint32_t kMem2CachedBase = 0x90000000u;
static constexpr uint32_t kMem2UncachedBase = 0xD0000000u;
static constexpr uint32_t kMem2PhysicalEnd =
kMem2PhysicalBase + static_cast<uint32_t>(kMem2Size);
static constexpr uint32_t kMem2CachedEnd =
kMem2CachedBase + static_cast<uint32_t>(kMem2Size);
static constexpr uint32_t kMem2UncachedEnd =
kMem2UncachedBase + static_cast<uint32_t>(kMem2Size);
struct RegionConfig {
std::string name;
uint32_t baseAddress = 0;
size_t sizeBytes = 0;
};
struct Config {
std::vector<RegionConfig> regions;
static Config WiiDefaults();
};
class AccessViolation : public std::runtime_error {
public:
AccessViolation(uint32_t address, size_t length, std::string_view reason);
uint32_t address() const noexcept { return address_; }
size_t length() const noexcept { return length_; }
std::string_view reason() const noexcept { return reason_; }
private:
uint32_t address_ = 0;
size_t length_ = 0;
std::string reason_;
};
static void Init(const Config& config);
// Single flat MEM1 region. Not used by the shipped runtime, but the
// translator integration-test harnesses emit calls to it.
static void Init(size_t mem1Size);
static void Reset();
// Executable ranges are normally registered during startup. Rebuild the
// writable fast-path classification after each registration so a page
// previously classified as ordinary data cannot retain a stale direct
// write bias.
static void RefreshWritableFastPathsForExecutableRanges();
static uint8_t Read8(uint32_t addr);
static uint16_t Read16(uint32_t addr);
static uint32_t Read32(uint32_t addr);
static uint64_t Read64(uint32_t addr);
static float ReadFloat32(uint32_t addr);
static double ReadFloat64(uint32_t addr);
static void Write8(uint32_t addr, uint8_t val);
static void Write16(uint32_t addr, uint16_t val);
static void Write32(uint32_t addr, uint32_t val);
static void Write64(uint32_t addr, uint64_t val);
static void WriteFloat32(uint32_t addr, double val);
static void WriteFloat64(uint32_t addr, double val);
// Exception-safe scalar access for HLE code. These keep Read32/Write32's
// full mapping behavior and only convert an unmapped address into a failure
// result; they are deliberately not MemoryInline::Try*GuestScalar, which is
// the translated-code fast path over the page table.
static bool TryRead32(uint32_t addr, uint32_t& value) noexcept {
try {
value = Read32(addr);
return true;
} catch (const AccessViolation&) {
return false;
}
}
static bool TryWrite32(uint32_t addr, uint32_t value) noexcept {
try {
Write32(addr, value);
return true;
} catch (const AccessViolation&) {
return false;
}
}
static uint8_t* GetPointer(uint32_t addr);
static uint8_t* GetPointer(uint32_t addr, size_t length);
static bool Contains(uint32_t addr, size_t length = 1);
static uint64_t RegisterDeferredRead(uint32_t addr, size_t length,
DeferredReadCallback callback, void* user);
static void ClearDeferredReads();
// sizeBytes reports the storage actually allocated, which is not always the
// configured size (aliased MEM1/MEM2 windows are clamped to what is behind
// them). Used by the crash dump.
static std::vector<RegionConfig> DescribeRegions();
};
// The translated-code access layer is kept separately from the public memory API.
#include "memory_access.h"
+696
View File
@@ -0,0 +1,696 @@
#pragma once
#include "guest_flat_memory.h"
#include "memory.h"
#include "recomp_mod_loader.h"
#include <cstddef>
#include <cstdint>
#include <cstring>
// Hooks for GX HLE FIFO handling
extern "C" {
void GX_HLE_FIFO_WriteFloat(float val);
void GX_HLE_FIFO_Write32(uint32_t val);
void GX_HLE_FIFO_Write16(uint16_t val);
void GX_HLE_FIFO_Write8(uint8_t val);
void GX_HLE_FIFO_WriteBurst(const uint8_t* data, uint32_t sizeBytes);
}
namespace MemoryInline {
#define MKW_MEMORY_FORCE_INLINE __forceinline
#define MKW_MEMORY_NO_INLINE __declspec(noinline)
#define MKW_MEMORY_COLD __attribute__((cold))
inline constexpr uint32_t kPageShift = 20;
inline constexpr uint32_t kPageSize = 1u << kPageShift;
inline constexpr uint32_t kPageMask = kPageSize - 1u;
inline constexpr uint32_t kPageCount = 1u << (32 - kPageShift);
inline constexpr uint32_t kMaxFastScalarSize = 8;
inline constexpr uint32_t kWritableSubPageShift = RecompMod::kExecutableWriteGuardPageShift;
inline constexpr uint32_t kWritableSubPageSize = 1u << kWritableSubPageShift;
inline constexpr uint32_t kWritableSubPagesPerPage = kPageSize / kWritableSubPageSize;
struct PageEntry {
uint8_t* base = nullptr;
uint32_t limit = 0;
};
extern PageEntry g_pageTable[kPageCount];
// Encoded (host page base - guest page base) + 1 for full pages whose next
// page is contiguous. This permits any native access up to 8 bytes without a
// per-access mask/limit check. Zero retains the general PageEntry fallback.
extern uintptr_t g_fullPageBias[kPageCount];
// Runtime-active readable biases. Deferred-read pages clear these entries once
// instead of paying a mode branch on every translated read.
extern uintptr_t g_fullReadablePageBias[kPageCount];
// Same encoding, but only for pages proven not to contain executable bytes.
// Executable-range registration invalidates entries before guest execution.
extern uintptr_t g_fullWritablePageBias[kPageCount];
// Allocated only for mapped 1 MiB pages that contain both executable and data
// 4 KiB pages. Entries use the same encoded host bias as the coarse table.
// Exact executable bits remain authoritative and are checked at lookup time,
// including both sides of a cross-4-KiB access.
struct SparseWritablePageTable {
uintptr_t encodedBias[kWritableSubPagesPerPage]{};
};
extern const SparseWritablePageTable* g_sparseWritablePageTables[kPageCount];
// Nonzero while any registered deferred read overlaps the page. Small
// mappings (e.g. the 16 KiB locked cache) have no coarse bias entry, so a
// zero readable bias alone cannot distinguish "deferred content pending"
// from "small but plain memory"; range resolution needs the explicit flag.
extern uint8_t g_deferredReadCoveredPages[kPageCount];
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD uint8_t Read8Slow(uint32_t addr);
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD uint16_t Read16Slow(uint32_t addr);
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD uint32_t Read32Slow(uint32_t addr);
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD uint64_t Read64Slow(uint32_t addr);
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD float ReadFloat32Slow(uint32_t addr);
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD double ReadFloat64Slow(uint32_t addr);
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void Write8Slow(uint32_t addr, uint8_t val);
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void Write16Slow(uint32_t addr, uint16_t val);
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void Write32Slow(uint32_t addr, uint32_t val);
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void Write64Slow(uint32_t addr, uint64_t val);
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void WriteFloat32Slow(uint32_t addr, double val);
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void WriteFloat64Slow(uint32_t addr, double val);
template <typename T>
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD T ReadResolvedFallback(uint32_t addr);
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD float ReadResolvedFallbackFloat32(uint32_t addr);
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD double ReadResolvedFallbackFloat64(uint32_t addr);
template <typename T>
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void WriteResolvedFallback(uint32_t addr, T value);
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void WriteResolvedFallbackFloat32(uint32_t addr, double val);
MKW_MEMORY_NO_INLINE MKW_MEMORY_COLD void WriteResolvedFallbackFloat64(uint32_t addr, double val);
bool ResolveDeferredReads(uint32_t addr, size_t length);
constexpr bool IsMmioAddress(uint32_t addr) {
return addr >= 0xCC000000u && addr < 0xCE000000u;
}
constexpr bool IsGpuFifoAddress(uint32_t addr) {
return addr >= 0xCC008000u && addr < 0xCC008100u;
}
// Page protections can't cover this: an MMIO write must reach GX HLE with its value or be
// reported, and a fault record can't carry the value, so this mask/compare sits in front of
// every flat store instead.
MKW_MEMORY_FORCE_INLINE bool FlatWriteNeedsPolicy(uint32_t address) {
return (address & 0xFE000000u) == 0xCC000000u; // 0xCC000000..0xCDFFFFFF
}
// Gekko stfs conversion is a bit-level narrowing operation. In particular it
// does not behave like a host double-to-float cast for values which were left
// in double precision, and it has hardware-tested handling for tiny values.
MKW_MEMORY_FORCE_INLINE uint32_t ConvertPpcDoubleToSingleBits(double value) {
uint64_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
const uint32_t exponent = static_cast<uint32_t>((bits >> 52) & 0x7FFu);
// The subnormal-single arm applies to exactly the exponents 874..896; every
// other exponent takes the plain sign/exponent/fraction narrowing below.
// Inside that window the exponent field is nonzero, so the magnitude cannot
// be zero and needs no separate test - the zero case (exponent 0) reaches
// the narrowing exactly as it did when the two arms shared that test.
if (exponent - 874u <= 22u) [[unlikely]]
{
uint32_t narrowed = static_cast<uint32_t>(
0x80000000ULL | ((bits & 0x000FFFFFFFFFFFFFULL) >> 21));
narrowed >>= (905u - exponent);
narrowed |= static_cast<uint32_t>((bits >> 32) & 0x80000000ULL);
return narrowed;
}
// Results below the documented conversion range are architecturally
// undefined; this is the behavior measured on Gekko/Broadway hardware.
return static_cast<uint32_t>(
((bits >> 32) & 0xC0000000ULL) | ((bits >> 29) & 0x3FFFFFFFULL));
}
MKW_MEMORY_FORCE_INLINE float PpcSingleBitsToFloat(uint32_t bits) {
float value = 0.0f;
std::memcpy(&value, &bits, sizeof(value));
return value;
}
MKW_MEMORY_FORCE_INLINE bool TryGetPointerFast(uint32_t address, size_t length, uint8_t*& pointer) {
const uint32_t page = address >> kPageShift;
if (length <= 8) {
const uintptr_t encodedBias = g_fullPageBias[page];
if (encodedBias != 0) {
pointer = reinterpret_cast<uint8_t*>((encodedBias - 1u) + address);
return true;
}
}
const uint32_t offset = address & kPageMask;
const auto& entry = g_pageTable[page];
if (!entry.base || offset + length > entry.limit) {
pointer = nullptr;
return false;
}
pointer = entry.base + offset;
return true;
}
inline uint8_t* GetPointerFast(uint32_t address, size_t length) {
uint8_t* pointer = nullptr;
return TryGetPointerFast(address, length, pointer) ? pointer : nullptr;
}
MKW_MEMORY_FORCE_INLINE bool TryGetWritablePointerFast(
uint32_t address, size_t length, uint8_t*& pointer) {
// The sparse-table hit below distinguishes itself from the small-mapping
// hit through `pointer`, so the out-parameter must start null regardless
// of what the caller passed in. (A caller handing in an uninitialized
// pointer used to turn every sparse hit into a write through stack
// garbage - random host memory corruption.)
pointer = nullptr;
if (length == 0 || length > 8 || address > UINT32_MAX - (length - 1))
return false;
const uint32_t coarsePage = address >> kPageShift;
uintptr_t encodedBias = g_fullWritablePageBias[coarsePage];
const uint32_t endAddress = address + static_cast<uint32_t>(length - 1);
const uint32_t firstExactPage = address >> kWritableSubPageShift;
const uint32_t lastExactPage = endAddress >> kWritableSubPageShift;
if (encodedBias != 0 && (endAddress >> kPageShift) != coarsePage &&
RecompMod::g_executableWriteGuardPages[lastExactPage].load(
std::memory_order_relaxed) != 0) {
return false;
}
if (encodedBias == 0) {
const auto* subTable = g_sparseWritablePageTables[coarsePage];
if (subTable != nullptr) {
encodedBias = subTable->encodedBias[
(address & kPageMask) >> kWritableSubPageShift];
if (encodedBias == 0)
return false;
} else {
// Small mappings such as Broadway's 16 KiB locked cache cannot
// populate the full-1-MiB bias table. Keep them native by proving
// the exact access against the ordinary page entry, then applying
// the same executable-write policy as a sparse-table hit.
const uint32_t offset = address & kPageMask;
const auto& entry = g_pageTable[coarsePage];
if (!entry.base || offset + length > entry.limit)
return false;
pointer = entry.base + offset;
}
// The exact 4 KiB guard is the final authority. Checking it on every
// checked/sparse hit also makes later executable-range registration
// safe when a prebuilt table still contains the old mapped bias.
if (RecompMod::g_executableWriteGuardPages[firstExactPage].load(
std::memory_order_relaxed) != 0 ||
(lastExactPage != firstExactPage &&
RecompMod::g_executableWriteGuardPages[lastExactPage].load(
std::memory_order_relaxed) != 0)) {
return false;
}
if (pointer != nullptr)
return true;
}
if (encodedBias == 0)
return false;
pointer = reinterpret_cast<uint8_t*>((encodedBias - 1u) + address);
return true;
}
// Flat form: guest_flat_memory.h's page protections already answer mapped/non-deferred/non-executable, so resolving is pure address arithmetic.
// Two checks stay inline: a wrapped guest address can't survive 64-bit `host + rangeOffset`, and an MMIO write's value isn't recoverable from a
// fault record, so a write touching that window must resolve null and fall back to Memory::Write*. Checking both range endpoints is a complete
// proof since length <= kPageSize (1 MiB) can't straddle the 32 MiB MMIO window.
MKW_MEMORY_FORCE_INLINE uint8_t* ResolveRangeHost(uint32_t base, int32_t minOffset, uint32_t length,
bool needsRead, bool needsWrite) {
(void)needsRead;
const uint32_t guestStart = base + static_cast<uint32_t>(minOffset);
if (length == 0 || length > kPageSize || guestStart > UINT32_MAX - (length - 1)) return nullptr;
if (needsWrite &&
(FlatWriteNeedsPolicy(guestStart) || FlatWriteNeedsPolicy(guestStart + (length - 1))))
[[unlikely]] return nullptr;
return MKW_FLAT_GUEST_BASE + guestStart;
}
// Guest-address byte order. Distinct from isa/big_endian.h, which is the
// host-pointer codec; do not "unify" them.
inline uint16_t ByteSwap16(uint16_t value) {
return __builtin_bswap16(value);
}
inline uint32_t ByteSwap32(uint32_t value) {
return __builtin_bswap32(value);
}
inline uint64_t ByteSwap64(uint64_t value) {
return __builtin_bswap64(value);
}
template <typename T>
inline T MaybeByteSwap(T value) {
if constexpr (sizeof(T) == 1) {
return value;
} else if constexpr (sizeof(T) == 2) {
return static_cast<T>(ByteSwap16(static_cast<uint16_t>(value)));
} else if constexpr (sizeof(T) == 4) {
return static_cast<T>(ByteSwap32(static_cast<uint32_t>(value)));
} else if constexpr (sizeof(T) == 8) {
return static_cast<T>(ByteSwap64(static_cast<uint64_t>(value)));
} else {
return value;
}
}
template <typename T>
MKW_MEMORY_FORCE_INLINE bool ReadResolvedScalar(uint8_t* host, uint32_t rangeOffset, T& outValue) {
if (!host) return false;
if constexpr (sizeof(T) == 1) {
outValue = host[rangeOffset];
} else {
T value = 0;
std::memcpy(&value, host + rangeOffset, sizeof(T));
outValue = MaybeByteSwap(value);
}
return true;
}
struct ResolvedLoadPair {
uint32_t first = 0;
uint32_t second = 0;
bool valid = false;
};
MKW_MEMORY_FORCE_INLINE ResolvedLoadPair ReadResolvedPair16(
uint8_t* host, uint32_t rangeOffset) {
uint32_t packed = 0;
if (!ReadResolvedScalar(host, rangeOffset, packed)) return {};
return {packed >> 16, packed & 0xFFFFu, true};
}
MKW_MEMORY_FORCE_INLINE ResolvedLoadPair ReadResolvedPair32(uint8_t* host, uint32_t rangeOffset) {
uint64_t packed = 0;
if (!ReadResolvedScalar(host, rangeOffset, packed)) return {};
return {static_cast<uint32_t>(packed >> 32), static_cast<uint32_t>(packed), true};
}
template <typename Packed>
MKW_MEMORY_FORCE_INLINE bool WriteResolvedPairFast(
uint8_t* host, uint32_t rangeOffset, Packed packed) {
if (!host) return false;
const Packed swapped = MaybeByteSwap(packed);
std::memcpy(host + rangeOffset, &swapped, sizeof(swapped));
return true;
}
MKW_MEMORY_FORCE_INLINE bool WriteResolvedPair16(
uint8_t* host, uint32_t rangeOffset, uint32_t packed) {
return WriteResolvedPairFast(host, rangeOffset, packed);
}
MKW_MEMORY_FORCE_INLINE bool WriteResolvedPair32(
uint8_t* host, uint32_t rangeOffset, uint64_t packed) {
return WriteResolvedPairFast(host, rangeOffset, packed);
}
template <typename T>
MKW_MEMORY_FORCE_INLINE bool WriteResolvedScalar(uint8_t* host, uint32_t rangeOffset, T value) {
if (!host) return false;
if constexpr (sizeof(T) == 1) {
host[rangeOffset] = static_cast<uint8_t>(value);
} else {
const T swapped = MaybeByteSwap(value);
std::memcpy(host + rangeOffset, &swapped, sizeof(T));
}
return true;
}
template <typename T>
inline bool TryReadMappedScalar(uint32_t address, T& outValue) {
uint8_t* ptr = nullptr;
if (TryGetPointerFast(address, sizeof(T), ptr)) {
if constexpr (sizeof(T) == 1) {
outValue = *ptr;
} else {
T value = 0;
std::memcpy(&value, ptr, sizeof(T));
outValue = MaybeByteSwap(value);
}
return true;
}
return false;
}
template <typename T>
MKW_MEMORY_FORCE_INLINE bool TryReadGuestScalar(uint32_t address, T& outValue) {
const uintptr_t encodedBias = g_fullReadablePageBias[address >> kPageShift];
if (encodedBias == 0) [[unlikely]]
return false;
auto* ptr = reinterpret_cast<uint8_t*>((encodedBias - 1u) + address);
if constexpr (sizeof(T) == 1) {
outValue = *ptr;
} else {
T value = 0;
std::memcpy(&value, ptr, sizeof(T));
outValue = MaybeByteSwap(value);
}
return true;
}
template <typename T>
MKW_MEMORY_FORCE_INLINE bool TryWriteGuestScalar(uint32_t address, T value) {
static_assert(sizeof(T) >= 1 && sizeof(T) <= kMaxFastScalarSize);
// A mixed executable/data 1 MiB page zeroes the coarse writable bias even though most of
// its 4 KiB sub-pages are plain data; MKW's THP buffers share such a page with .text, which
// used to force ~15% of total CPU through the cold path. The sparse sub-page tier below
// keeps those stores native while the exact 4 KiB executable guards stay authoritative.
if (address > UINT32_MAX - static_cast<uint32_t>(sizeof(T) - 1u)) [[unlikely]]
return false;
const uint32_t coarsePage = address >> kPageShift;
const uint32_t endAddress = address + static_cast<uint32_t>(sizeof(T) - 1u);
if ((endAddress >> kPageShift) != coarsePage) [[unlikely]]
return false;
const uintptr_t encodedBias = g_fullWritablePageBias[coarsePage];
uint8_t* ptr = nullptr;
if (encodedBias != 0) {
ptr = reinterpret_cast<uint8_t*>((encodedBias - 1u) + address);
} else if (!TryGetWritablePointerFast(address, sizeof(T), ptr)) [[unlikely]] {
return false;
}
if constexpr (sizeof(T) == 1) {
*ptr = static_cast<uint8_t>(value);
} else {
const T swapped = MaybeByteSwap(value);
std::memcpy(ptr, &swapped, sizeof(T));
}
return true;
}
template <typename T>
inline bool WriteStackScalarFast(uint32_t address, T value) {
uint8_t* ptr = nullptr;
if (TryGetPointerFast(address, sizeof(T), ptr)) {
if constexpr (sizeof(T) == 1) {
*ptr = static_cast<uint8_t>(value);
} else {
const T swapped = MaybeByteSwap(value);
std::memcpy(ptr, &swapped, sizeof(T));
}
return true;
}
return false;
}
inline uint8_t ReadStack8(uint32_t address) {
uint8_t value = 0;
return TryReadMappedScalar(address, value) ? value : Memory::Read8(address);
}
inline uint16_t ReadStack16(uint32_t address) {
uint16_t value = 0;
return TryReadMappedScalar(address, value) ? value : Memory::Read16(address);
}
inline uint32_t ReadStack32(uint32_t address) {
uint32_t value = 0;
return TryReadMappedScalar(address, value) ? value : Memory::Read32(address);
}
inline uint64_t ReadStack64(uint32_t address) {
uint64_t value = 0;
return TryReadMappedScalar(address, value) ? value : Memory::Read64(address);
}
inline void WriteStack8(uint32_t address, uint8_t value) {
if (!WriteStackScalarFast(address, value)) {
Memory::Write8(address, value);
}
}
inline void WriteStack16(uint32_t address, uint16_t value) {
if (!WriteStackScalarFast(address, value)) {
Memory::Write16(address, value);
}
}
inline void WriteStack32(uint32_t address, uint32_t value) {
if (!WriteStackScalarFast(address, value)) {
Memory::Write32(address, value);
}
}
inline void WriteStack64(uint32_t address, uint64_t value) {
if (!WriteStackScalarFast(address, value)) {
Memory::Write64(address, value);
}
}
inline void WriteStackFloat32(uint32_t address, double value) {
const uint32_t bits = ConvertPpcDoubleToSingleBits(value);
if (!WriteStackScalarFast(address, bits)) {
Memory::WriteFloat32(address, value);
}
}
inline void WriteStackFloat64(uint32_t address, double value) {
uint64_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
if (!WriteStackScalarFast(address, bits)) {
Memory::WriteFloat64(address, value);
}
}
template <typename T, typename SlowRead>
MKW_MEMORY_FORCE_INLINE T ReadResolved(uint8_t* host, uint32_t rangeOffset, uint32_t address,
SlowRead slow) {
T value = 0;
if (ReadResolvedScalar(host, rangeOffset, value)) {
return value;
}
[[unlikely]] return slow(address);
}
MKW_MEMORY_FORCE_INLINE uint8_t ReadResolved8(uint8_t* r, uint32_t o, uint32_t a) { return ReadResolved<uint8_t>(r, o, a, ReadResolvedFallback<uint8_t>); }
MKW_MEMORY_FORCE_INLINE uint16_t ReadResolved16(uint8_t* r, uint32_t o, uint32_t a) { return ReadResolved<uint16_t>(r, o, a, ReadResolvedFallback<uint16_t>); }
MKW_MEMORY_FORCE_INLINE uint32_t ReadResolved32(uint8_t* r, uint32_t o, uint32_t a) { return ReadResolved<uint32_t>(r, o, a, ReadResolvedFallback<uint32_t>); }
// Live via isa/ppc_isa_quantized.h (the psq resolved tier packs two lanes into
// one 64-bit access); generated code never names it directly.
MKW_MEMORY_FORCE_INLINE uint64_t ReadResolved64(uint8_t* r, uint32_t o, uint32_t a) { return ReadResolved<uint64_t>(r, o, a, ReadResolvedFallback<uint64_t>); }
MKW_MEMORY_FORCE_INLINE float ReadResolvedFloat32(uint8_t* r, uint32_t o, uint32_t a) {
uint32_t bits = 0;
if (!ReadResolvedScalar(r, o, bits)) [[unlikely]] return ReadResolvedFallbackFloat32(a);
float value; std::memcpy(&value, &bits, sizeof(value)); return value;
}
MKW_MEMORY_FORCE_INLINE double ReadResolvedFloat64(uint8_t* r, uint32_t o, uint32_t a) {
uint64_t bits = 0;
if (!ReadResolvedScalar(r, o, bits)) [[unlikely]] return ReadResolvedFallbackFloat64(a);
double value; std::memcpy(&value, &bits, sizeof(value)); return value;
}
template <typename T, typename SlowWrite>
MKW_MEMORY_FORCE_INLINE void WriteResolved(uint8_t* host, uint32_t rangeOffset, uint32_t address, T value,
SlowWrite slow) {
if (WriteResolvedScalar(host, rangeOffset, value)) {
return;
}
[[unlikely]] slow(address, value);
}
MKW_MEMORY_FORCE_INLINE void WriteResolved8(uint8_t* r, uint32_t o, uint32_t a, uint8_t v) { WriteResolved(r, o, a, v, WriteResolvedFallback<uint8_t>); }
MKW_MEMORY_FORCE_INLINE void WriteResolved16(uint8_t* r, uint32_t o, uint32_t a, uint16_t v) { WriteResolved(r, o, a, v, WriteResolvedFallback<uint16_t>); }
MKW_MEMORY_FORCE_INLINE void WriteResolved32(uint8_t* r, uint32_t o, uint32_t a, uint32_t v) { WriteResolved(r, o, a, v, WriteResolvedFallback<uint32_t>); }
// Live via isa/ppc_isa_quantized.h, as ReadResolved64 above.
MKW_MEMORY_FORCE_INLINE void WriteResolved64(uint8_t* r, uint32_t o, uint32_t a, uint64_t v) { WriteResolved(r, o, a, v, WriteResolvedFallback<uint64_t>); }
MKW_MEMORY_FORCE_INLINE void WriteResolvedFloat32(uint8_t* r, uint32_t o, uint32_t a, double v) {
const uint32_t bits = ConvertPpcDoubleToSingleBits(v);
if (WriteResolvedScalar(r, o, bits)) return;
[[unlikely]] WriteResolvedFallbackFloat32(a, v);
}
MKW_MEMORY_FORCE_INLINE void WriteResolvedFloat64(uint8_t* r, uint32_t o, uint32_t a, double v) {
uint64_t bits; std::memcpy(&bits, &v, sizeof(bits));
if (WriteResolvedScalar(r, o, bits)) return;
[[unlikely]] WriteResolvedFallbackFloat64(a, v);
}
// Flat guest memory (audit item T-MEM): the 4 GiB reservation makes a guest access a byte swap
// around `*(T*)(base + addr)`, no page-table load or limit check (interception model documented
// in guest_flat_memory.h). The one exception kept inline is the MMIO write policy, since the
// written value can't be recovered from a fault record.
template <typename T>
MKW_MEMORY_FORCE_INLINE T FlatLoad(uint32_t address) {
T value{};
std::memcpy(&value, MKW_FLAT_GUEST_BASE + address, sizeof(T));
return MaybeByteSwap(value);
}
template <typename T>
MKW_MEMORY_FORCE_INLINE void FlatStore(uint32_t address, T value) {
const T swapped = MaybeByteSwap(value);
std::memcpy(MKW_FLAT_GUEST_BASE + address, &swapped, sizeof(T));
}
MKW_MEMORY_FORCE_INLINE uint8_t FlatRead8(uint32_t address) { return FlatLoad<uint8_t>(address); }
MKW_MEMORY_FORCE_INLINE uint16_t FlatRead16(uint32_t address) { return FlatLoad<uint16_t>(address); }
MKW_MEMORY_FORCE_INLINE uint32_t FlatRead32(uint32_t address) { return FlatLoad<uint32_t>(address); }
MKW_MEMORY_FORCE_INLINE float FlatReadFloat32(uint32_t address) {
const uint32_t bits = FlatLoad<uint32_t>(address);
float value = 0.0f;
std::memcpy(&value, &bits, sizeof(value));
return value;
}
MKW_MEMORY_FORCE_INLINE double FlatReadFloat64(uint32_t address) {
const uint64_t bits = FlatLoad<uint64_t>(address);
double value = 0.0;
std::memcpy(&value, &bits, sizeof(value));
return value;
}
MKW_MEMORY_FORCE_INLINE void FlatWrite8(uint32_t address, uint8_t value) {
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write8Slow(address, value); return; }
FlatStore<uint8_t>(address, value);
}
MKW_MEMORY_FORCE_INLINE void FlatWrite16(uint32_t address, uint16_t value) {
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write16Slow(address, value); return; }
FlatStore<uint16_t>(address, value);
}
MKW_MEMORY_FORCE_INLINE void FlatWrite32(uint32_t address, uint32_t value) {
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write32Slow(address, value); return; }
FlatStore<uint32_t>(address, value);
}
MKW_MEMORY_FORCE_INLINE void FlatWriteFloat32(uint32_t address, double value) {
const uint32_t bits = ConvertPpcDoubleToSingleBits(value);
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat32Slow(address, value); return; }
FlatStore<uint32_t>(address, bits);
}
MKW_MEMORY_FORCE_INLINE void FlatWriteFloat64(uint32_t address, double value) {
uint64_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat64Slow(address, value); return; }
FlatStore<uint64_t>(address, bits);
}
// Check-free stores: emitted ONLY for addresses the translator proved at translate time are ordinary guest RAM (r1-relative stack slots, ~45%
// of flat stores), skipping the MMIO mask/compare that's pure overhead there. Still safe if that proof were ever wrong: the flat view maps
// 0xCC000000..0xCDFFFFFF PAGE_NOACCESS, so a stray MMIO store faults into the same handler and diagnostic as the checked path, just reported
// instead of dispatched inline. Never use these for an address the translator hasn't proven.
MKW_MEMORY_FORCE_INLINE void FlatWriteRam8(uint32_t address, uint8_t value) {
FlatStore<uint8_t>(address, value);
}
MKW_MEMORY_FORCE_INLINE void FlatWriteRam16(uint32_t address, uint16_t value) {
FlatStore<uint16_t>(address, value);
}
MKW_MEMORY_FORCE_INLINE void FlatWriteRam32(uint32_t address, uint32_t value) {
FlatStore<uint32_t>(address, value);
}
MKW_MEMORY_FORCE_INLINE void FlatWriteRamFloat32(uint32_t address, double value) {
FlatStore<uint32_t>(address, ConvertPpcDoubleToSingleBits(value));
}
MKW_MEMORY_FORCE_INLINE void FlatWriteRamFloat64(uint32_t address, double value) {
uint64_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
FlatStore<uint64_t>(address, bits);
}
} // namespace MemoryInline
#undef MKW_MEMORY_FORCE_INLINE
#undef MKW_MEMORY_NO_INLINE
#undef MKW_MEMORY_COLD
inline uint8_t Memory::Read8(uint32_t addr) {
uint8_t value = 0;
if (!MemoryInline::TryReadGuestScalar(addr, value)) [[unlikely]]
return MemoryInline::Read8Slow(addr);
return value;
}
inline uint16_t Memory::Read16(uint32_t addr) {
uint16_t value = 0;
if (!MemoryInline::TryReadGuestScalar(addr, value)) [[unlikely]]
return MemoryInline::Read16Slow(addr);
return value;
}
inline uint32_t Memory::Read32(uint32_t addr) {
uint32_t value = 0;
if (!MemoryInline::TryReadGuestScalar(addr, value)) [[unlikely]]
return MemoryInline::Read32Slow(addr);
return value;
}
inline uint64_t Memory::Read64(uint32_t addr) {
uint64_t value = 0;
if (!MemoryInline::TryReadGuestScalar(addr, value)) [[unlikely]]
return MemoryInline::Read64Slow(addr);
return value;
}
inline float Memory::ReadFloat32(uint32_t addr) {
uint32_t bits = 0;
if (!MemoryInline::TryReadGuestScalar(addr, bits)) [[unlikely]]
return MemoryInline::ReadFloat32Slow(addr);
float value = 0.0f;
std::memcpy(&value, &bits, sizeof(value));
return value;
}
inline double Memory::ReadFloat64(uint32_t addr) {
uint64_t bits = 0;
if (!MemoryInline::TryReadGuestScalar(addr, bits)) [[unlikely]]
return MemoryInline::ReadFloat64Slow(addr);
double value = 0.0;
std::memcpy(&value, &bits, sizeof(value));
return value;
}
inline void Memory::Write8(uint32_t addr, uint8_t val) {
if (!MemoryInline::TryWriteGuestScalar(addr, val)) [[unlikely]]
MemoryInline::Write8Slow(addr, val);
}
inline void Memory::Write16(uint32_t addr, uint16_t val) {
if (!MemoryInline::TryWriteGuestScalar(addr, val)) [[unlikely]]
MemoryInline::Write16Slow(addr, val);
}
inline void Memory::Write32(uint32_t addr, uint32_t val) {
if (!MemoryInline::TryWriteGuestScalar(addr, val)) [[unlikely]]
MemoryInline::Write32Slow(addr, val);
}
inline void Memory::Write64(uint32_t addr, uint64_t val) {
if (!MemoryInline::TryWriteGuestScalar(addr, val)) [[unlikely]]
MemoryInline::Write64Slow(addr, val);
}
inline void Memory::WriteFloat32(uint32_t addr, double val) {
{
const uint32_t bits = MemoryInline::ConvertPpcDoubleToSingleBits(val);
if (MemoryInline::TryWriteGuestScalar(addr, bits))
return;
}
[[unlikely]] MemoryInline::WriteFloat32Slow(addr, val);
}
inline void Memory::WriteFloat64(uint32_t addr, double val) {
{
uint64_t bits = 0;
std::memcpy(&bits, &val, sizeof(bits));
if (MemoryInline::TryWriteGuestScalar(addr, bits))
return;
}
[[unlikely]] MemoryInline::WriteFloat64Slow(addr, val);
}
+19
View File
@@ -0,0 +1,19 @@
// Precompiled header for WiiCompiled
// This header is precompiled to speed up builds
// All common headers used by generated functions should be included here
#ifndef MKW_RECOMPILED_PCH_H
#define MKW_RECOMPILED_PCH_H
// Standard C++ headers
#include <cstdint>
#include <cstring>
#include <cmath>
#include <iostream>
// Project headers
#include "ppc_runtime.h"
#include "abi_bridge.h"
#include "memory.h"
#endif // MKW_RECOMPILED_PCH_H
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <cstdint>
namespace MusicAttenuation {
// Enables the optional Windows media-session integration. The monitor is
// started lazily the first time this is enabled.
void SetEnabled(bool enabled) noexcept;
void SetMusicVolume(float volume) noexcept;
void SetSoundEffectsVolume(float volume) noexcept;
void SetUiVolume(float volume) noexcept;
void SetVoicesVolume(float volume) noexcept;
bool IsExternalMediaPlaying() noexcept;
bool IsMediaControlAvailable() noexcept;
bool IsMediaControlInitializationComplete() noexcept;
// Called from the guest scheduler/audio path. This applies state changes to
// the live SoundPlayer buses, so changing a category or entering/leaving
// attenuation never requires a scene or race restart.
void TickGuest() noexcept;
// Accurate native replacement for nw4r::snd::SoundPlayer::SetVolume. Only
// the category multipliers are applied here; the game's requested bus volume
// remains the base value.
void SetSoundPlayerVolume(uint32_t soundPlayer, float requestedVolume);
} // namespace MusicAttenuation
+194
View File
@@ -0,0 +1,194 @@
#pragma once
#include "runtime_config.h"
#include "runtime_log.h"
#include "system_bridge.h"
#include <cstdlib>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <optional>
#include <string>
#include <string_view>
namespace RuntimeNandPath {
inline std::optional<std::filesystem::path> ExistingDirectory(const std::filesystem::path& path) {
std::error_code ec;
if (!path.empty() && std::filesystem::is_directory(path, ec) && !ec) {
return path;
}
return std::nullopt;
}
[[noreturn]] inline void FailNandRoot(const char* message, const std::filesystem::path& path = {}) {
if (path.empty()) {
RT_LOGF(RT_TAG_NAND, "ERROR: %s\n", message);
} else {
RT_LOGF(RT_TAG_NAND, "ERROR: %s: %s\n", message, path.string().c_str());
}
RT_LOGF(RT_TAG_NAND, "Set [paths] nand_root in Config.toml.\n");
std::string details = message ? message : "The configured NAND could not be initialized.";
if (!path.empty()) {
details += "\n\nPath: ";
details += path.string();
}
details += "\n\nSet [paths] nand_root in Config.toml and try again.";
// Same fatal idiom as the DVD and OS paths: crash artifacts first so the run
// folder always has them, then a non-zero exit code, the popup, and the
// "already reported" latch so the atexit handler does not stack a second
// generic report on top of this one.
RuntimeCrash::WriteCrashArtifacts("nand_root", details);
SetRuntimeExitCode(EXIT_FAILURE);
ShowRuntimeFatalPopup("NAND initialization failed", details);
MarkFatalErrorReported();
std::exit(EXIT_FAILURE);
}
inline std::filesystem::path ResolveConfiguredPath(const std::string& value) {
return RuntimeConfigFile::ResolveRelativeToConfig(value);
}
inline std::string PathStringWithoutTrailingSeparators(std::filesystem::path path) {
std::string text = path.string();
while (!text.empty()) {
const char tail = text.back();
if (tail != '\\' && tail != '/') {
break;
}
text.pop_back();
}
return text;
}
inline std::filesystem::path ManagedNandRootPath() {
return RuntimeConfigFile::ApplicationDataDirectory() / "NAND";
}
inline std::optional<std::filesystem::path> BootstrapPayloadPath() {
if (auto executableDirectory = RuntimeConfigFile::ExecutableDirectory()) {
const auto adjacent = *executableDirectory / "wii_bootstrap";
if (ExistingDirectory(adjacent / "shared2" / "wc24")) {
return adjacent;
}
}
// This makes developer-tree launches work without changing their release layout.
for (auto base = std::filesystem::current_path(); !base.empty();) {
const auto candidate = base / "runtime" / "assets" / "wii";
if (ExistingDirectory(candidate / "shared2" / "wc24")) {
return candidate;
}
const auto parent = base.parent_path();
if (parent == base) {
break;
}
base = parent;
}
return std::nullopt;
}
inline bool CopyBootstrapFile(const std::filesystem::path& sourceRoot,
const std::filesystem::path& destinationRoot,
const std::filesystem::path& relativePath,
std::error_code& ec) {
const auto source = sourceRoot / relativePath;
const auto destination = destinationRoot / relativePath;
if (std::filesystem::exists(destination, ec)) {
return !ec;
}
std::filesystem::create_directories(destination.parent_path(), ec);
if (ec) {
return false;
}
std::filesystem::copy_file(source, destination, std::filesystem::copy_options::none, ec);
return !ec;
}
// Create these WC24 files only for a new profile; never overwrite user data.
constexpr std::string_view kBootstrapFiles[] = {
"shared2/wc24/misc.bin",
"shared2/wc24/nwc24dl.bin",
"shared2/wc24/nwc24fl.bin",
"shared2/wc24/nwc24fls.bin",
"shared2/wc24/nwc24msg.cbk",
"shared2/wc24/nwc24msg.cfg",
"shared2/wc24/mbox/Readme.txt",
"shared2/wc24/mbox/wc24recv.ctl",
"shared2/wc24/mbox/wc24recv.mbx",
"shared2/wc24/mbox/wc24send.ctl",
"shared2/wc24/mbox/wc24send.mbx",
};
// Add first-run WC24 files only when the NAND has none yet.
inline bool SeedMissingBootstrapFiles(const std::filesystem::path& root) {
const auto payload = BootstrapPayloadPath();
if (!payload) {
return false;
}
std::error_code ec;
for (const std::string_view file : kBootstrapFiles) {
const std::filesystem::path relativePath{std::string(file)};
ec.clear();
if (!CopyBootstrapFile(*payload, root, relativePath, ec)) {
RT_LOG(RT_TAG_NAND) << "could not create " << (root / relativePath).string() << std::endl;
return false;
}
}
return true;
}
inline std::filesystem::path CreateManagedNandRoot() {
const std::filesystem::path root = ManagedNandRootPath();
std::error_code ec;
std::filesystem::create_directories(root, ec);
if (ec || !std::filesystem::is_directory(root, ec)) {
FailNandRoot("Unable to create managed NAND root", root);
}
if (!SeedMissingBootstrapFiles(root)) {
FailNandRoot("Unable to initialize managed NAND", root);
}
const auto marker = root / ".mkw_recompiled_managed_nand";
ec.clear();
if (!std::filesystem::exists(marker, ec)) {
std::ofstream markerFile(marker, std::ios::trunc);
if (!markerFile) {
FailNandRoot("Managed NAND root is not writable", root);
}
markerFile << "version=1\n";
markerFile.close();
if (!markerFile) {
FailNandRoot("Unable to finish managed NAND initialization", root);
}
}
RT_LOG(RT_TAG_NAND) << "using managed NAND root: " << root.string() << std::endl;
return root;
}
inline std::filesystem::path DiscoverNandRootPath() {
const std::string configPath = RuntimeConfigFile::NandRoot();
if (!configPath.empty()) {
const auto path = ResolveConfiguredPath(configPath);
if (auto existing = ExistingDirectory(path)) {
// Seed only a new configured NAND so existing frontend data stays unchanged.
if (!SeedMissingBootstrapFiles(*existing)) {
RT_LOG(RT_TAG_NAND) << "first-run WC24 seeding failed for the configured NAND root" << std::endl;
}
return *existing;
}
FailNandRoot("Configured NAND root is not an existing directory", path);
}
return CreateManagedNandRoot();
}
inline std::string DiscoverNandRootString() {
return PathStringWithoutTrailingSeparators(DiscoverNandRootPath());
}
} // namespace RuntimeNandPath
+98
View File
@@ -0,0 +1,98 @@
// Native call catalog used by InvokeDirectCpu. Keep project-specific HLE
// address mappings here; abi_bridge.h contains only the generic ABI machinery.
//
// This is a .inc, not a header: it has no includes of its own and references
// CpuContext, KnownTypedNativeCpuCall<> and KnownNativeCpuCall<>, all of which
// are declared above its single mid-file include site in abi_bridge.h. It
// cannot compile standalone and must never be included anywhere else.
extern "C" void nw4r__lyt__detail__DrawQuad_800847c0(CpuContext* ctx);
extern "C" void nw4r__lyt__detail__DrawQuad_80084d20(CpuContext* ctx);
extern "C" void GX__SetVtxDesc_8016d3a4(uint32_t a, uint32_t t);
extern "C" void GX__ClearVtxDesc_8016dc34();
extern "C" void GX__SetVtxAttrFmt_8016dc68(uint32_t vf, uint32_t a, uint32_t c, uint32_t t, uint32_t fr);
extern "C" void GX__SetTexCoordGen2_8016e37c(uint32_t dc, uint32_t f, uint32_t sp, uint32_t m, uint32_t n, uint32_t pm);
extern "C" void GX__SetNumTexGens_8016e5a4(uint32_t n);
extern "C" void GX__DrawDone_8016eab0();
extern "C" void GX__Begin_8016f0f0(uint32_t t, uint32_t vf, uint32_t nv);
extern "C" void GX__SetLineWidth_8016f314(uint32_t width, uint32_t texOffsets);
extern "C" void GX__SetPointSize_8016f348(uint32_t pointSize, uint32_t texOffsets);
extern "C" void GX__SetCullMode_8016f3b8(uint32_t m);
extern "C" void GX__SetNumChans_8017054c(uint32_t n);
extern "C" void GX__SetChanCtrl_80170570(uint32_t ch, uint32_t en, uint32_t as, uint32_t ms, uint32_t lm, uint32_t df, uint32_t af);
extern "C" void GX__InitTexObj_801707f8(uint32_t oa, uint32_t da, uint32_t w, uint32_t h, uint32_t f, uint32_t ws, uint32_t wt, uint32_t m);
extern "C" void GX__InitTexObjLOD_80170a4c(uint32_t oa, uint32_t mif, uint32_t maf, float mil, float mal, float lb, uint32_t bc, uint32_t el, uint32_t ma);
extern "C" void GX__InvalidateTexAll_80171110();
extern "C" void GX__LoadTexObj_80170f2c(uint32_t oa, uint32_t tid);
extern "C" void GX__SetNumIndStages_80171b38(uint32_t n);
extern "C" void GX__SetTevDirect_80171b58(uint32_t s);
extern "C" void GX__SetTevColorIn_80171ce0(uint32_t s, uint32_t a, uint32_t b, uint32_t c, uint32_t d);
extern "C" void GX__SetTevAlphaIn_80171d20(uint32_t s, uint32_t a, uint32_t b, uint32_t c, uint32_t d);
extern "C" void GX__SetTevColorOp_80171d60(uint32_t s, uint32_t op, uint32_t b, uint32_t sc, uint32_t cl, uint32_t or_);
extern "C" void GX__SetTevAlphaOp_80171db8(uint32_t s, uint32_t op, uint32_t b, uint32_t sc, uint32_t cl, uint32_t or_);
extern "C" void GX__SetTevColorS10_80171e70(uint32_t id, uint32_t cp);
extern "C" void GX__SetTevKColor_80171ed4(uint32_t id, uint32_t cp);
extern "C" void GX__SetTevKColorSel_80171f30(uint32_t s, uint32_t sel);
extern "C" void GX__SetTevKAlphaSel_80171f80(uint32_t s, uint32_t sel);
extern "C" void GX__SetTevSwapMode_80171fd0(uint32_t s, uint32_t rs, uint32_t ts);
extern "C" void GX__SetTevSwapModeTable_8017200c(uint32_t id, uint32_t r, uint32_t g, uint32_t b, uint32_t a);
extern "C" void GX__SetTevOrder_8017214c(uint32_t s, uint32_t c, uint32_t m, uint32_t col);
extern "C" void GX__SetNumTevStages_801722a8(uint32_t n);
extern "C" void GX__SetBlendMode_8017277c(uint32_t t, uint32_t s, uint32_t d, uint32_t op);
extern "C" void GX__LoadPosMtxImm_8017310c(uint32_t ma, uint32_t id);
extern "C" void GX__SetCurrentMtx_80173214(uint32_t id);
#define MKW_KNOWN_TYPED_NATIVE(addr, fn, ...) \
template <> struct KnownTypedNativeCpuCall<addr> { \
static constexpr bool kAvailable = true; \
static void Invoke(CpuContext* cpu) { (void)cpu; fn(__VA_ARGS__); } \
}
MKW_KNOWN_TYPED_NATIVE(0x8016D3A4u, GX__SetVtxDesc_8016d3a4, cpu->gpr[3], cpu->gpr[4]);
MKW_KNOWN_TYPED_NATIVE(0x8016DC34u, GX__ClearVtxDesc_8016dc34);
MKW_KNOWN_TYPED_NATIVE(0x8016DC68u, GX__SetVtxAttrFmt_8016dc68, cpu->gpr[3], cpu->gpr[4], cpu->gpr[5], cpu->gpr[6], cpu->gpr[7]);
MKW_KNOWN_TYPED_NATIVE(0x8016E37Cu, GX__SetTexCoordGen2_8016e37c, cpu->gpr[3], cpu->gpr[4], cpu->gpr[5], cpu->gpr[6], cpu->gpr[7], cpu->gpr[8]);
MKW_KNOWN_TYPED_NATIVE(0x8016E5A4u, GX__SetNumTexGens_8016e5a4, cpu->gpr[3]);
MKW_KNOWN_TYPED_NATIVE(0x8016EAB0u, GX__DrawDone_8016eab0);
MKW_KNOWN_TYPED_NATIVE(0x8016F0F0u, GX__Begin_8016f0f0, cpu->gpr[3], cpu->gpr[4], cpu->gpr[5]);
MKW_KNOWN_TYPED_NATIVE(0x8016F314u, GX__SetLineWidth_8016f314, cpu->gpr[3], cpu->gpr[4]);
MKW_KNOWN_TYPED_NATIVE(0x8016F348u, GX__SetPointSize_8016f348, cpu->gpr[3], cpu->gpr[4]);
MKW_KNOWN_TYPED_NATIVE(0x8016F3B8u, GX__SetCullMode_8016f3b8, cpu->gpr[3]);
MKW_KNOWN_TYPED_NATIVE(0x8017054Cu, GX__SetNumChans_8017054c, cpu->gpr[3]);
MKW_KNOWN_TYPED_NATIVE(0x80170570u, GX__SetChanCtrl_80170570, cpu->gpr[3], cpu->gpr[4], cpu->gpr[5], cpu->gpr[6], cpu->gpr[7], cpu->gpr[8], cpu->gpr[9]);
MKW_KNOWN_TYPED_NATIVE(0x801707F8u, GX__InitTexObj_801707f8, cpu->gpr[3], cpu->gpr[4], cpu->gpr[5], cpu->gpr[6], cpu->gpr[7], cpu->gpr[8], cpu->gpr[9], cpu->gpr[10]);
MKW_KNOWN_TYPED_NATIVE(0x80170A4Cu, GX__InitTexObjLOD_80170a4c, cpu->gpr[3], cpu->gpr[4], cpu->gpr[5], static_cast<float>(cpu->fpr[1].d), static_cast<float>(cpu->fpr[2].d), static_cast<float>(cpu->fpr[3].d), cpu->gpr[6], cpu->gpr[7], cpu->gpr[8]);
MKW_KNOWN_TYPED_NATIVE(0x80171110u, GX__InvalidateTexAll_80171110);
MKW_KNOWN_TYPED_NATIVE(0x80170F2Cu, GX__LoadTexObj_80170f2c, cpu->gpr[3], cpu->gpr[4]);
MKW_KNOWN_TYPED_NATIVE(0x80171B38u, GX__SetNumIndStages_80171b38, cpu->gpr[3]);
MKW_KNOWN_TYPED_NATIVE(0x80171B58u, GX__SetTevDirect_80171b58, cpu->gpr[3]);
MKW_KNOWN_TYPED_NATIVE(0x80171CE0u, GX__SetTevColorIn_80171ce0, cpu->gpr[3], cpu->gpr[4], cpu->gpr[5], cpu->gpr[6], cpu->gpr[7]);
MKW_KNOWN_TYPED_NATIVE(0x80171D20u, GX__SetTevAlphaIn_80171d20, cpu->gpr[3], cpu->gpr[4], cpu->gpr[5], cpu->gpr[6], cpu->gpr[7]);
MKW_KNOWN_TYPED_NATIVE(0x80171D60u, GX__SetTevColorOp_80171d60, cpu->gpr[3], cpu->gpr[4], cpu->gpr[5], cpu->gpr[6], cpu->gpr[7], cpu->gpr[8]);
MKW_KNOWN_TYPED_NATIVE(0x80171DB8u, GX__SetTevAlphaOp_80171db8, cpu->gpr[3], cpu->gpr[4], cpu->gpr[5], cpu->gpr[6], cpu->gpr[7], cpu->gpr[8]);
MKW_KNOWN_TYPED_NATIVE(0x80171E70u, GX__SetTevColorS10_80171e70, cpu->gpr[3], cpu->gpr[4]);
MKW_KNOWN_TYPED_NATIVE(0x80171ED4u, GX__SetTevKColor_80171ed4, cpu->gpr[3], cpu->gpr[4]);
MKW_KNOWN_TYPED_NATIVE(0x80171F30u, GX__SetTevKColorSel_80171f30, cpu->gpr[3], cpu->gpr[4]);
MKW_KNOWN_TYPED_NATIVE(0x80171F80u, GX__SetTevKAlphaSel_80171f80, cpu->gpr[3], cpu->gpr[4]);
MKW_KNOWN_TYPED_NATIVE(0x80171FD0u, GX__SetTevSwapMode_80171fd0, cpu->gpr[3], cpu->gpr[4], cpu->gpr[5]);
MKW_KNOWN_TYPED_NATIVE(0x8017200Cu, GX__SetTevSwapModeTable_8017200c, cpu->gpr[3], cpu->gpr[4], cpu->gpr[5], cpu->gpr[6], cpu->gpr[7]);
MKW_KNOWN_TYPED_NATIVE(0x8017214Cu, GX__SetTevOrder_8017214c, cpu->gpr[3], cpu->gpr[4], cpu->gpr[5], cpu->gpr[6]);
MKW_KNOWN_TYPED_NATIVE(0x801722A8u, GX__SetNumTevStages_801722a8, cpu->gpr[3]);
MKW_KNOWN_TYPED_NATIVE(0x8017277Cu, GX__SetBlendMode_8017277c, cpu->gpr[3], cpu->gpr[4], cpu->gpr[5], cpu->gpr[6]);
MKW_KNOWN_TYPED_NATIVE(0x8017310Cu, GX__LoadPosMtxImm_8017310c, cpu->gpr[3], cpu->gpr[4]);
MKW_KNOWN_TYPED_NATIVE(0x80173214u, GX__SetCurrentMtx_80173214, cpu->gpr[3]);
#undef MKW_KNOWN_TYPED_NATIVE
#define MKW_KNOWN_NATIVE_CPU_CALL(addr, fn) \
template <> struct KnownNativeCpuCall<addr> { \
static constexpr bool kAvailable = true; \
static constexpr uint32_t kNonvolatileFprWriteMask = 0; \
static constexpr void (*Entry)(CpuContext*) = &fn; \
}
MKW_KNOWN_NATIVE_CPU_CALL(0x800847C0u, nw4r__lyt__detail__DrawQuad_800847c0);
MKW_KNOWN_NATIVE_CPU_CALL(0x80084D20u, nw4r__lyt__detail__DrawQuad_80084d20);
#undef MKW_KNOWN_NATIVE_CPU_CALL
+10
View File
@@ -0,0 +1,10 @@
#pragma once
// Host memory seam for isa/ppc_isa_quantized.h. That header includes
// "ppc_isa_memory.h" by plain quoted name and expects whatever it resolves to
// to supply the Memory / MemoryInline APIs and the flat-guest-base macros its
// psq fast paths use. This file is this project's implementation of that
// contract; memory.h chains memory_access.h, which defines MemoryInline.
#include "memory.h" // chains memory_access.h, which supplies MemoryInline and
// includes guest_flat_memory.h for the flat-base macros
+14
View File
@@ -0,0 +1,14 @@
// The PowerPC ISA package (runtime/include/isa/), re-exported under the single
// header name the generated code and the runtime include. The package has two
// host seams this project satisfies elsewhere: "ppc_isa_memory.h" (pulled by
// the quantized tier) and the ShowRuntimeFatalPopup implementation.
#pragma once
#include "isa/ppc_isa_config.h"
#include "isa/big_endian.h"
#include "isa/ppc_isa_fpenv.h"
#include "isa/ppc_isa_context.h"
#include "isa/ppc_isa_int.h"
#include "isa/ppc_isa_float.h"
#include "isa/ppc_isa_quantized.h"
+147
View File
@@ -0,0 +1,147 @@
#pragma once
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <string>
#include <string_view>
#include <vector>
struct CpuContext;
namespace RecompMod {
using InitializerFn = void (*)();
struct MemoryReservation {
uint32_t start = 0;
uint32_t end = 0;
std::string name;
};
// Diagnostic-only, read by fatal reporters (unmapped access, executable-write, CPU dumps).
// Defined here as `inline thread_local` with a constant initializer, not `extern thread_local`
// in the .cpp: every indirect dispatch scopes this, so an out-of-line ctor/dtor would cost two
// un-inlinable calls plus a register spill each for three instructions of work.
inline thread_local uint32_t g_currentTranslatedExecutionAddress = 0;
class ScopedTranslatedExecutionAddress {
public:
explicit ScopedTranslatedExecutionAddress(uint32_t address) noexcept
: previous_(g_currentTranslatedExecutionAddress) {
// Address 0 means "nothing new to report" - the enclosing scope's value
// stays visible, and the unconditional restore below keeps nesting exact.
if (address != 0) {
g_currentTranslatedExecutionAddress = address;
}
}
~ScopedTranslatedExecutionAddress() noexcept {
g_currentTranslatedExecutionAddress = previous_;
}
ScopedTranslatedExecutionAddress(const ScopedTranslatedExecutionAddress&) = delete;
ScopedTranslatedExecutionAddress& operator=(const ScopedTranslatedExecutionAddress&) = delete;
private:
uint32_t previous_ = 0;
};
inline constexpr uint32_t kExecutableWriteGuardPageShift = 12;
inline constexpr uint32_t kExecutableWriteGuardPageCount = 1u << (32 - kExecutableWriteGuardPageShift);
inline constexpr uint32_t kExecutableWriteGuardCoarsePageShift = 20;
inline constexpr uint32_t kExecutableWriteGuardCoarsePageCount = 1u << (32 - kExecutableWriteGuardCoarsePageShift);
inline constexpr uint32_t kExecutableWriteGuardMidPageShift = 16;
inline constexpr uint32_t kExecutableWriteGuardMidPageCount = 1u << (32 - kExecutableWriteGuardMidPageShift);
extern std::atomic<bool> g_executableWriteGuardEnabled;
extern std::atomic<uint8_t> g_executableWriteGuardPages[kExecutableWriteGuardPageCount];
extern std::atomic<uint8_t> g_executableWriteGuardCoarsePages[kExecutableWriteGuardCoarsePageCount];
extern std::atomic<uint8_t> g_executableWriteGuardMidPages[kExecutableWriteGuardMidPageCount];
// Two initializer phases, both emitted by the translator's mod data-patch
// writer. (A third, plain RegisterInitializer/RunInitializers pair existed with
// no registrant on either side and was removed.)
void RegisterMemoryInitializer(InitializerFn fn);
void RunMemoryInitializers();
void RegisterPostRelInitializer(InitializerFn fn);
void RunPostRelInitializers();
void RegisterDvdOverlayRoot(std::string root);
const std::vector<std::string>& DvdOverlayRoots();
// Riivolution settings pinned by the distribution's recomp.yml. The XML path is
// relative to the pack/overlay root; option selections use Riivolution's 1-based
// choice index (0 disables the option).
struct RiivolutionOptionSelection {
std::string section;
std::string option;
uint32_t choice = 0;
};
void RegisterRiivolutionXml(const char* packRelativePath);
void RegisterRiivolutionOption(const char* sectionName, const char* optionName, unsigned int choice);
const std::string& RiivolutionXml();
const std::vector<RiivolutionOptionSelection>& RiivolutionOptionSelections();
void RegisterMemoryReservation(uint32_t start, uint32_t end, std::string name);
const std::vector<MemoryReservation>& MemoryReservations();
uint32_t CurrentTranslatedExecutionAddress() noexcept;
void RegisterExecutableRange(uint32_t start, uint32_t end, std::string name);
bool HandleExecutableWrite(uint32_t address, size_t length, uint64_t value);
void CheckExecutableWrite(uint32_t address, size_t length, uint64_t value);
inline bool ExecutableWriteGuardMayHit(uint32_t address, size_t length) noexcept {
if (length == 0 || !g_executableWriteGuardEnabled.load(std::memory_order_relaxed)) {
return false;
}
const uint64_t endExclusive = static_cast<uint64_t>(address) + length;
const uint32_t firstCoarsePage = address >> kExecutableWriteGuardCoarsePageShift;
const uint64_t lastCoarsePage64 = (endExclusive - 1) >> kExecutableWriteGuardCoarsePageShift;
const uint32_t lastCoarsePage = lastCoarsePage64 >= kExecutableWriteGuardCoarsePageCount
? kExecutableWriteGuardCoarsePageCount - 1
: static_cast<uint32_t>(lastCoarsePage64);
bool coarseHit = false;
for (uint32_t page = firstCoarsePage; page <= lastCoarsePage; ++page) {
if (g_executableWriteGuardCoarsePages[page].load(std::memory_order_relaxed) != 0) {
coarseHit = true;
break;
}
}
if (!coarseHit) {
return false;
}
const uint32_t firstMidPage = address >> kExecutableWriteGuardMidPageShift;
const uint64_t lastMidPage64 = (endExclusive - 1) >> kExecutableWriteGuardMidPageShift;
const uint32_t lastMidPage = lastMidPage64 >= kExecutableWriteGuardMidPageCount
? kExecutableWriteGuardMidPageCount - 1
: static_cast<uint32_t>(lastMidPage64);
bool midHit = false;
for (uint32_t page = firstMidPage; page <= lastMidPage; ++page) {
if (g_executableWriteGuardMidPages[page].load(std::memory_order_relaxed) != 0) {
midHit = true;
break;
}
}
if (!midHit) {
return false;
}
const uint32_t firstPage = address >> kExecutableWriteGuardPageShift;
const uint64_t lastPage64 = (endExclusive - 1) >> kExecutableWriteGuardPageShift;
const uint32_t lastPage = lastPage64 >= kExecutableWriteGuardPageCount
? kExecutableWriteGuardPageCount - 1
: static_cast<uint32_t>(lastPage64);
for (uint32_t page = firstPage; page <= lastPage; ++page) {
if (g_executableWriteGuardPages[page].load(std::memory_order_relaxed) != 0) {
return true;
}
}
return false;
}
} // namespace RecompMod
+848
View File
@@ -0,0 +1,848 @@
#pragma once
#include <algorithm>
#include <array>
#include <cctype>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <limits>
#include <optional>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include <toml.hpp>
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <shlobj.h>
#endif
struct RuntimeUserConfig {
std::optional<bool> widescreen;
std::optional<int32_t> windowPosX;
std::optional<int32_t> windowPosY;
std::optional<uint32_t> windowWidth;
std::optional<uint32_t> windowHeight;
std::optional<float> resolutionMultiplier;
std::optional<std::string> graphicsApi;
std::optional<std::string> displayMode;
std::optional<uint32_t> frameInterpolationFps;
std::optional<bool> skipUnreadyPipelines;
std::optional<bool> disableCopyFilter;
std::optional<bool> textureReplacements;
std::optional<bool> textureDumps;
std::optional<bool> showFps;
std::optional<uint32_t> disabledPostProcessingPaths;
std::optional<float> audioVolume;
std::optional<float> audioMusicVolume;
std::optional<float> audioSoundEffectsVolume;
std::optional<float> audioUiVolume;
std::optional<float> audioVoicesVolume;
std::optional<bool> audioMuted;
std::optional<bool> audioMixWorker;
std::optional<bool> attenuateMusicWhenMediaPlays;
std::optional<bool> networkEnabled;
std::optional<std::string> nandRoot;
std::optional<std::string> dvdRoot;
// The one canonical Retro Rewind installation, owned and updated by the frontend. Setup records
// it here instead of copying the pack, so an asset-only update is visible on the next launch.
std::optional<std::string> retroRewindRoot;
std::vector<std::string> overlayRoots;
// Controller mappings use Wii/GameCube button names as keys and up to two
// comma-separated SDL-style physical button names ("south", or
// "dpad_up,left_shoulder") as values; pressing either bound button counts.
std::array<std::optional<std::string>, 12> controllerButtons;
};
namespace RuntimeConfigFile {
inline constexpr const char* kConfigFileName = "Config.toml";
inline constexpr const char* kApplicationDirectoryName = "WiiCompiled";
// Portable layout. A directory holding kPortableMarkerFileName is a portable root; every piece of
// runtime user state (Config.toml, NAND, Cache, Logs) lives in <root>/UserData instead of
// %LOCALAPPDATA%. The marker is searched for from the executable's directory upwards, which is what
// makes an installation survive being moved or carried on removable media.
inline constexpr const char* kPortableMarkerFileName = "portable.txt";
inline constexpr const char* kPortableUserDataDirectoryName = "UserData";
// The installed layout puts products two levels below the root (<root>/Install/Base/game.exe). The
// bound is deliberately small so an unrelated marker far up a drive can never capture an ordinary
// installation.
inline constexpr int kPortableSearchDepth = 4;
inline std::string Trim(std::string_view text) {
size_t begin = 0;
while (begin < text.size() && std::isspace(static_cast<unsigned char>(text[begin]))) {
++begin;
}
size_t end = text.size();
while (end > begin && std::isspace(static_cast<unsigned char>(text[end - 1]))) {
--end;
}
return std::string(text.substr(begin, end - begin));
}
inline std::string RemoveComment(std::string_view line) {
bool inSingle = false;
bool inDouble = false;
bool escaped = false;
for (size_t i = 0; i < line.size(); ++i) {
const char ch = line[i];
if (inDouble && ch == '\\' && !escaped) {
escaped = true;
continue;
}
if (ch == '\'' && !inDouble) {
inSingle = !inSingle;
} else if (ch == '"' && !inSingle && !escaped) {
inDouble = !inDouble;
} else if (ch == '#' && !inSingle && !inDouble) {
return std::string(line.substr(0, i));
}
escaped = false;
}
return std::string(line);
}
inline bool IsSupportedResolutionMultiplier(float value) {
static constexpr std::array values{0.0f, 1.0f, 1.5f, 2.0f, 3.0f, 4.0f, 6.0f, 8.0f};
return std::find(values.begin(), values.end(), value) != values.end();
}
// Must stay in step with the backend table in main.cpp, which is what actually
// maps these to AuroraBackend.
inline bool IsSupportedGraphicsApi(std::string_view value) {
static constexpr std::array<std::string_view, 3> values{"auto", "d3d12", "vulkan"};
return std::find(values.begin(), values.end(), value) != values.end();
}
inline bool IsSupportedDisplayMode(std::string_view value) {
static constexpr std::array<std::string_view, 3> values{
"windowed", "borderless", "exclusive",
};
return std::find(values.begin(), values.end(), value) != values.end();
}
// 240 was offered by an early build and is no longer supported; a saved 240 is
// migrated to 180 at the parse site.
inline bool IsSupportedFrameInterpolationFps(uint32_t value) {
return value == 0 || value == 120 || value == 180;
}
inline std::optional<std::filesystem::path> ExecutableDirectory() {
#ifdef _WIN32
std::wstring buffer(MAX_PATH, L'\0');
for (;;) {
const DWORD length = GetModuleFileNameW(nullptr, buffer.data(), static_cast<DWORD>(buffer.size()));
if (length == 0) {
return std::nullopt;
}
if (length < buffer.size() - 1) {
buffer.resize(length);
return std::filesystem::path(buffer).parent_path();
}
buffer.resize(buffer.size() * 2);
}
#else
return std::nullopt;
#endif
}
// The portable root this executable lives under, or nullopt for a normal installation. The answer
// cannot change while the process runs, so it is resolved exactly once: every user-state path
// derives from it and they must not disagree with each other.
inline const std::optional<std::filesystem::path>& PortableRootDirectory() {
static const std::optional<std::filesystem::path> root = []() -> std::optional<std::filesystem::path> {
const auto executableDirectory = ExecutableDirectory();
if (!executableDirectory) {
return std::nullopt;
}
std::filesystem::path current = *executableDirectory;
for (int level = 0; level <= kPortableSearchDepth; ++level) {
std::error_code ec;
if (std::filesystem::is_regular_file(current / kPortableMarkerFileName, ec)) {
return current;
}
const auto parent = current.parent_path();
if (parent.empty() || parent == current) {
break;
}
current = parent;
}
return std::nullopt;
}();
return root;
}
inline std::filesystem::path ApplicationDataDirectory() {
if (const auto& portableRoot = PortableRootDirectory()) {
return *portableRoot / kPortableUserDataDirectoryName;
}
#ifdef _WIN32
PWSTR rawPath = nullptr;
if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_LocalAppData, KF_FLAG_CREATE, nullptr, &rawPath)) && rawPath) {
const std::filesystem::path directory = std::filesystem::path(rawPath) / kApplicationDirectoryName;
CoTaskMemFree(rawPath);
return directory;
}
#endif
return std::filesystem::current_path() / kApplicationDirectoryName;
}
inline std::filesystem::path ResolveConfigPath() {
return ApplicationDataDirectory() / kConfigFileName;
}
inline void EnsureConfigFile() {
const std::filesystem::path path = ResolveConfigPath();
std::error_code ec;
std::filesystem::create_directories(path.parent_path(), ec);
if (ec || std::filesystem::exists(path, ec)) {
return;
}
std::ofstream output(path);
if (!output) {
return;
}
output << "# WiiCompiled user configuration\n"
"# Set paths.dvd_root to an extracted Mario Kart Wii DATA directory.\n\n"
"[video]\n"
"widescreen = true\n"
"resolution_multiplier = 1.0\n"
"frame_interpolation_fps = 0\n"
"display_mode = \"windowed\"\n"
"graphics_api = \"auto\"\n"
"skip_unready_pipelines = true\n"
"disable_copy_filter = true\n"
"show_fps = true\n"
"# Dolphin-style custom textures. When enabled, the renderer indexes\n"
"# texture_replacements/ next to this file at startup and substitutes\n"
"# any tex1_<W>x<H>_<hash>[_<tlut hash>]_<format>.dds or .png it finds\n"
"# there for the matching game texture. texture_dumps writes every\n"
"# unmatched texture to Cache/texture_dumps under the name a\n"
"# replacement would need. Both are read once, at startup.\n"
"texture_replacements = false\n"
"texture_dumps = false\n\n"
"[audio]\n"
"volume = 1.0\n"
"music_volume = 1.0\n"
"sound_effects_volume = 1.0\n"
"ui_volume = 1.0\n"
"voices_volume = 1.0\n"
"muted = false\n"
"attenuate_music_when_media_plays = false\n"
"# Runs the AX/DSP voice mix on its own thread, joined before the\n"
"# guest can observe it. Set to false to mix inline on the guest\n"
"# thread exactly as the runtime did before.\n"
"mix_worker = true\n\n"
"[network]\n"
"enabled = true\n\n"
"[paths]\n"
"# dvd_root = \"D:\\\\MarioKartWii\\\\DATA\"\n"
"# nand_root = \"D:\\\\WiiNand\"\n"
"# retro_rewind_root = \"D:\\\\RetroRewind\\\\RetroRewind6\"\n"
"# overlay_roots = [\"D:\\\\RetroRewind\"]\n";
}
template <typename T>
inline std::optional<T> FindConfigValue(
const toml::value& document, std::string_view section, std::string_view key) {
try {
return toml::find<T>(document, std::string(section), std::string(key));
} catch (const std::exception&) {
return std::nullopt;
}
}
inline std::optional<uint32_t> FindConfigUint(
const toml::value& document, std::string_view section, std::string_view key) {
const auto value = FindConfigValue<int64_t>(document, section, key);
if (!value || *value < 0 || static_cast<uint64_t>(*value) > UINT32_MAX) {
return std::nullopt;
}
return static_cast<uint32_t>(*value);
}
inline std::optional<int32_t> FindConfigInt(
const toml::value& document, std::string_view section, std::string_view key) {
const auto value = FindConfigValue<int64_t>(document, section, key);
if (!value || *value < INT32_MIN || *value > INT32_MAX) {
return std::nullopt;
}
return static_cast<int32_t>(*value);
}
inline std::optional<float> FindConfigFloat(
const toml::value& document, std::string_view section, std::string_view key) {
std::optional<double> value = FindConfigValue<double>(document, section, key);
if (!value) {
if (const auto integer = FindConfigValue<int64_t>(document, section, key)) {
value = static_cast<double>(*integer);
}
}
if (!value || !std::isfinite(*value) ||
*value < -static_cast<double>(std::numeric_limits<float>::max()) ||
*value > static_cast<double>(std::numeric_limits<float>::max())) {
return std::nullopt;
}
return static_cast<float>(*value);
}
inline void AppendOverlayRoots(RuntimeUserConfig& config, const std::string& roots) {
size_t begin = 0;
while (begin < roots.size()) {
const size_t end = roots.find(';', begin);
std::string root = Trim(std::string_view(roots).substr(begin, end - begin));
if (!root.empty()) {
config.overlayRoots.push_back(std::move(root));
}
if (end == std::string::npos) {
break;
}
begin = end + 1;
}
}
inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
RuntimeUserConfig config;
static constexpr std::array<std::string_view, 12> buttonKeys = {
"a", "b", "x", "y", "start", "z", "l", "r", "up", "down", "left", "right",
};
for (size_t index = 0; index < buttonKeys.size(); ++index) {
config.controllerButtons[index] =
FindConfigValue<std::string>(document, "controller", buttonKeys[index]);
}
config.widescreen = FindConfigValue<bool>(document, "video", "widescreen");
config.windowPosX = FindConfigInt(document, "video", "window_x");
config.windowPosY = FindConfigInt(document, "video", "window_y");
if (auto value = FindConfigUint(document, "video", "window_width"); value && *value != 0) {
config.windowWidth = *value;
}
if (auto value = FindConfigUint(document, "video", "window_height"); value && *value != 0) {
config.windowHeight = *value;
}
if (auto value = FindConfigFloat(document, "video", "resolution_multiplier");
value && IsSupportedResolutionMultiplier(*value)) {
config.resolutionMultiplier = *value;
}
if (auto value = FindConfigValue<std::string>(document, "video", "graphics_api")) {
if (IsSupportedGraphicsApi(*value)) {
config.graphicsApi = *value;
} else {
std::cerr << "[runtime] Unknown video.graphics_api=\"" << *value
<< "\", using the automatic backend" << std::endl;
}
}
if (auto value = FindConfigValue<std::string>(document, "video", "display_mode");
value && IsSupportedDisplayMode(*value)) {
config.displayMode = *value;
}
if (auto value = FindConfigUint(document, "video", "frame_interpolation_fps")) {
const uint32_t migrated = *value == 240u ? 180u : *value;
if (IsSupportedFrameInterpolationFps(migrated)) {
config.frameInterpolationFps = migrated;
}
}
config.skipUnreadyPipelines = FindConfigValue<bool>(document, "video", "skip_unready_pipelines");
config.disableCopyFilter = FindConfigValue<bool>(document, "video", "disable_copy_filter");
config.showFps = FindConfigValue<bool>(document, "video", "show_fps");
config.textureReplacements = FindConfigValue<bool>(document, "video", "texture_replacements");
config.textureDumps = FindConfigValue<bool>(document, "video", "texture_dumps");
if (auto value = FindConfigUint(document, "video", "disabled_post_processing_paths");
value && (*value & ~0x10u) == 0) {
config.disabledPostProcessingPaths = *value & 0x10u;
}
auto readVolume = [&](std::string_view key) -> std::optional<float> {
auto value = FindConfigFloat(document, "audio", key);
return value && *value >= 0.0f && *value <= 1.0f ? value : std::nullopt;
};
config.audioVolume = readVolume("volume");
config.audioMusicVolume = readVolume("music_volume");
config.audioSoundEffectsVolume = readVolume("sound_effects_volume");
config.audioUiVolume = readVolume("ui_volume");
config.audioVoicesVolume = readVolume("voices_volume");
config.audioMuted = FindConfigValue<bool>(document, "audio", "muted");
config.audioMixWorker = FindConfigValue<bool>(document, "audio", "mix_worker");
config.attenuateMusicWhenMediaPlays =
FindConfigValue<bool>(document, "audio", "attenuate_music_when_media_plays");
config.networkEnabled = FindConfigValue<bool>(document, "network", "enabled");
config.nandRoot = FindConfigValue<std::string>(document, "paths", "nand_root");
config.dvdRoot = FindConfigValue<std::string>(document, "paths", "dvd_root");
config.retroRewindRoot = FindConfigValue<std::string>(document, "paths", "retro_rewind_root");
if (auto roots = FindConfigValue<std::vector<std::string>>(document, "paths", "overlay_roots")) {
for (auto& root : *roots) {
root = Trim(root);
if (!root.empty()) {
config.overlayRoots.push_back(std::move(root));
}
}
} else if (auto roots = FindConfigValue<std::string>(document, "paths", "overlay_roots")) {
AppendOverlayRoots(config, *roots);
}
return config;
}
inline RuntimeUserConfig ParseConfig(std::istream& input, std::string sourceName = "Config.toml") {
try {
return ParseConfigDocument(toml::parse(input, std::move(sourceName)));
} catch (const std::exception& exception) {
std::cerr << "[runtime-config] Invalid TOML; using built-in defaults: "
<< exception.what() << std::endl;
return {};
}
}
inline RuntimeUserConfig LoadConfigFile() {
EnsureConfigFile();
std::ifstream file(ResolveConfigPath(), std::ios::binary);
return file ? ParseConfig(file, ResolveConfigPath().string()) : RuntimeUserConfig{};
}
inline const RuntimeUserConfig& Get() {
static RuntimeUserConfig config = LoadConfigFile();
return config;
}
inline RuntimeUserConfig& Mutable() {
return const_cast<RuntimeUserConfig&>(Get());
}
inline constexpr std::array<std::string_view, 12> kControllerButtonKeys = {
"a", "b", "x", "y", "start", "z", "l", "r", "up", "down", "left", "right",
};
inline const std::optional<std::string>& ControllerButton(size_t index) {
static const std::optional<std::string> empty;
return index < Get().controllerButtons.size() ? Get().controllerButtons[index] : empty;
}
// Update one TOML value without discarding comments, unrelated settings, or
// user-specific paths. This is used by the in-game F10 settings bar.
inline bool WriteSetting(std::string_view section, std::string_view key, std::string_view value) {
const auto path = ResolveConfigPath();
std::ifstream input(path);
std::vector<std::string> lines;
std::string line;
while (std::getline(input, line)) {
if (!line.empty() && line.back() == '\r') {
line.pop_back();
}
lines.push_back(std::move(line));
}
const std::string normalizedSection = Trim(section);
const std::string normalizedKey = Trim(key);
size_t sectionStart = lines.size();
size_t sectionEnd = lines.size();
for (size_t i = 0; i < lines.size(); ++i) {
const std::string trimmed = Trim(RemoveComment(lines[i]));
if (trimmed.size() >= 2 && trimmed.front() == '[' && trimmed.back() == ']') {
const std::string found = Trim(std::string_view(trimmed).substr(1, trimmed.size() - 2));
if (sectionStart != lines.size()) {
sectionEnd = i;
break;
}
if (found == normalizedSection) {
sectionStart = i;
}
}
}
const std::string replacement = normalizedKey + " = " + std::string(value);
if (sectionStart == lines.size()) {
if (!lines.empty() && !lines.back().empty()) {
lines.emplace_back();
}
lines.emplace_back("[" + normalizedSection + "]");
lines.push_back(replacement);
} else {
bool replaced = false;
for (size_t i = sectionStart + 1; i < sectionEnd; ++i) {
const std::string uncommented = Trim(RemoveComment(lines[i]));
const size_t equals = uncommented.find('=');
if (equals != std::string::npos && Trim(std::string_view(uncommented).substr(0, equals)) == normalizedKey) {
lines[i] = replacement;
replaced = true;
break;
}
}
if (!replaced) {
// Append after the section's last real line rather than after the blank line that
// separates it from the next header: this file is edited by hand and by the host
// installer as well, and a key parked below the separator reads as if it belonged to
// the next section. The host writer (Launcher/WiiCompiled.Setup/RuntimeConfiguration.cs)
// applies exactly this rule.
size_t insertAt = sectionEnd;
while (insertAt > sectionStart + 1 && Trim(lines[insertAt - 1]).empty()) {
--insertAt;
}
lines.insert(lines.begin() + static_cast<std::ptrdiff_t>(insertAt), replacement);
}
}
std::error_code ec;
if (path.has_parent_path()) {
std::filesystem::create_directories(path.parent_path(), ec);
}
std::ofstream output(path, std::ios::trunc);
if (!output) {
std::cerr << "[runtime-config] Unable to write " << path.string() << std::endl;
return false;
}
for (const auto& outputLine : lines) {
output << outputLine << '\n';
}
return static_cast<bool>(output);
}
inline std::string FormatString(std::string_view value) {
return toml::format(toml::value(std::string(value)));
}
inline bool SetResolutionMultiplier(float value) {
Mutable().resolutionMultiplier = value;
std::ostringstream formatted;
formatted << value;
return WriteSetting("video", "resolution_multiplier", formatted.str());
}
inline bool SetWindowSize(uint32_t width, uint32_t height) {
if (width == 0 || height == 0) {
return false;
}
Mutable().windowWidth = width;
Mutable().windowHeight = height;
const bool wroteWidth = WriteSetting("video", "window_width", std::to_string(width));
const bool wroteHeight = WriteSetting("video", "window_height", std::to_string(height));
return wroteWidth && wroteHeight;
}
inline bool SetWindowPosition(int32_t x, int32_t y) {
Mutable().windowPosX = x;
Mutable().windowPosY = y;
const bool wroteX = WriteSetting("video", "window_x", std::to_string(x));
const bool wroteY = WriteSetting("video", "window_y", std::to_string(y));
return wroteX && wroteY;
}
inline bool SetFrameInterpolationFps(uint32_t value) {
if (!IsSupportedFrameInterpolationFps(value)) {
return false;
}
Mutable().frameInterpolationFps = value;
return WriteSetting("video", "frame_interpolation_fps", std::to_string(value));
}
inline bool SetDisplayMode(std::string value) {
if (!IsSupportedDisplayMode(value)) {
return false;
}
Mutable().displayMode = value;
return WriteSetting("video", "display_mode", FormatString(value));
}
inline bool SetSkipUnreadyPipelines(bool value) {
Mutable().skipUnreadyPipelines = value;
return WriteSetting("video", "skip_unready_pipelines", value ? "true" : "false");
}
inline bool SetDisableCopyFilter(bool value) {
Mutable().disableCopyFilter = value;
return WriteSetting("video", "disable_copy_filter", value ? "true" : "false");
}
inline bool SetShowFps(bool value) {
Mutable().showFps = value;
return WriteSetting("video", "show_fps", value ? "true" : "false");
}
inline bool SetDisabledPostProcessingPaths(uint32_t value) {
Mutable().disabledPostProcessingPaths = value;
std::ostringstream formatted;
formatted << "0x" << std::hex << std::uppercase << value;
return WriteSetting("video", "disabled_post_processing_paths", formatted.str());
}
inline bool SetControllerButton(size_t index, std::string value) {
if (index >= kControllerButtonKeys.size()) {
return false;
}
Mutable().controllerButtons[index] = value;
return WriteSetting("controller", kControllerButtonKeys[index], FormatString(value));
}
inline bool SetAudioVolume(float value) {
value = std::clamp(value, 0.0f, 1.0f);
Mutable().audioVolume = value;
std::ostringstream formatted;
formatted << value;
return WriteSetting("audio", "volume", formatted.str());
}
inline bool SetMusicVolume(float value) {
value = std::clamp(value, 0.0f, 1.0f);
Mutable().audioMusicVolume = value;
std::ostringstream formatted;
formatted << value;
return WriteSetting("audio", "music_volume", formatted.str());
}
inline bool SetSoundEffectsVolume(float value) {
value = std::clamp(value, 0.0f, 1.0f);
Mutable().audioSoundEffectsVolume = value;
std::ostringstream formatted;
formatted << value;
return WriteSetting("audio", "sound_effects_volume", formatted.str());
}
inline bool SetUiVolume(float value) {
value = std::clamp(value, 0.0f, 1.0f);
Mutable().audioUiVolume = value;
std::ostringstream formatted;
formatted << value;
return WriteSetting("audio", "ui_volume", formatted.str());
}
inline bool SetVoicesVolume(float value) {
value = std::clamp(value, 0.0f, 1.0f);
Mutable().audioVoicesVolume = value;
std::ostringstream formatted;
formatted << value;
return WriteSetting("audio", "voices_volume", formatted.str());
}
inline bool SetAudioMuted(bool value) {
Mutable().audioMuted = value;
return WriteSetting("audio", "muted", value ? "true" : "false");
}
inline bool SetAudioMixWorker(bool value) {
Mutable().audioMixWorker = value;
return WriteSetting("audio", "mix_worker", value ? "true" : "false");
}
inline bool SetAttenuateMusicWhenMediaPlays(bool value) {
Mutable().attenuateMusicWhenMediaPlays = value;
return WriteSetting("audio", "attenuate_music_when_media_plays", value ? "true" : "false");
}
inline bool WidescreenEnabled(bool fallback = false) {
return Get().widescreen.value_or(fallback);
}
inline bool WindowPosition(int32_t& x, int32_t& y) {
if (!Get().windowPosX || !Get().windowPosY) {
return false;
}
x = *Get().windowPosX;
y = *Get().windowPosY;
return true;
}
inline uint32_t WindowWidth(uint32_t fallback) {
return Get().windowWidth.value_or(fallback);
}
inline uint32_t WindowHeight(uint32_t fallback) {
return Get().windowHeight.value_or(fallback);
}
inline float ResolutionMultiplier(float fallback = 1.0f) {
return std::max(0.0f, Get().resolutionMultiplier.value_or(fallback));
}
inline float AudioVolume(float fallback = 1.0f) {
return std::clamp(Get().audioVolume.value_or(fallback), 0.0f, 1.0f);
}
inline float MusicVolume(float fallback = 1.0f) {
return std::clamp(Get().audioMusicVolume.value_or(fallback), 0.0f, 1.0f);
}
inline float SoundEffectsVolume(float fallback = 1.0f) {
return std::clamp(Get().audioSoundEffectsVolume.value_or(fallback), 0.0f, 1.0f);
}
inline float UiVolume(float fallback = 1.0f) {
return std::clamp(Get().audioUiVolume.value_or(fallback), 0.0f, 1.0f);
}
inline float VoicesVolume(float fallback = 1.0f) {
return std::clamp(Get().audioVoicesVolume.value_or(fallback), 0.0f, 1.0f);
}
inline bool AudioMuted(bool fallback = false) {
return Get().audioMuted.value_or(fallback);
}
// Off-thread AX/DSP mix. Default on; false restores the fully synchronous mix.
inline bool AudioMixWorkerEnabled(bool fallback = true) {
return Get().audioMixWorker.value_or(fallback);
}
inline bool AttenuateMusicWhenMediaPlays(bool fallback = false) {
return Get().attenuateMusicWhenMediaPlays.value_or(fallback);
}
inline uint32_t FrameInterpolationFps(uint32_t fallback = 0) {
return Get().frameInterpolationFps.value_or(fallback);
}
inline bool SkipUnreadyPipelines(bool fallback = true) {
return Get().skipUnreadyPipelines.value_or(fallback);
}
inline bool DisableCopyFilter(bool fallback = true) {
return Get().disableCopyFilter.value_or(fallback);
}
inline bool ShowFps(bool fallback = true) {
return Get().showFps.value_or(fallback);
}
inline bool TextureReplacements(bool fallback = false) {
return Get().textureReplacements.value_or(fallback);
}
// Dumping only produces the names a replacement would need, so main.cpp
// gates it on TextureReplacements() as well.
inline bool TextureDumps(bool fallback = false) {
return Get().textureDumps.value_or(fallback);
}
inline uint32_t DisabledPostProcessingPaths(uint32_t fallback = 0) {
return Get().disabledPostProcessingPaths.value_or(fallback) & 0x10u;
}
inline std::string GraphicsApi(std::string fallback = "auto") {
return Get().graphicsApi.value_or(std::move(fallback));
}
inline std::string DisplayMode(std::string fallback = "windowed") {
return Get().displayMode.value_or(std::move(fallback));
}
inline bool NetworkEnabled(bool fallback = true) {
return Get().networkEnabled.value_or(fallback);
}
inline std::string NandRoot(std::string fallback = "") {
return Get().nandRoot.value_or(std::move(fallback));
}
inline std::string DvdRoot(std::string fallback = "") {
return Get().dvdRoot.value_or(std::move(fallback));
}
// The one resolver for configured paths. A relative value means the same thing
// everywhere it can be configured: relative to the config file that named it,
// never to the process working directory (docs/WHEELWIZARD_CONTRACT.md).
inline std::filesystem::path ResolveRelativeTo(const std::filesystem::path& base,
const std::string& value) {
std::filesystem::path path(value);
if (path.is_relative()) {
path = base / path;
}
return path.lexically_normal();
}
inline std::filesystem::path ResolveRelativeToConfig(const std::string& value) {
return ResolveRelativeTo(ResolveConfigPath().parent_path(), value);
}
// The extracted DATA directory. Empty when nothing is configured.
inline std::filesystem::path ResolvedDvdRoot() {
const std::string configured = DvdRoot();
return configured.empty() ? std::filesystem::path{} : ResolveRelativeToConfig(configured);
}
/// The canonical Retro Rewind installation the frontend owns, or "" when none is recorded.
inline std::string RetroRewindRoot(std::string fallback = "") {
return Get().retroRewindRoot.value_or(std::move(fallback));
}
inline const std::vector<std::string>& OverlayRoots() {
return Get().overlayRoots;
}
inline void LogLoadedConfig() {
static const bool logged = [] {
const auto& config = Get();
const auto configPath = ResolveConfigPath();
std::cout << "[runtime-config] " << configPath.string();
if (!std::filesystem::exists(configPath)) {
std::cout << " not found; using built-in defaults";
} else {
std::cout << " loaded";
if (config.widescreen) {
std::cout << " widescreen=" << (*config.widescreen ? "true" : "false");
}
if (config.windowWidth || config.windowHeight) {
std::cout << " window=" << config.windowWidth.value_or(0) << "x"
<< config.windowHeight.value_or(0);
}
if (config.resolutionMultiplier) {
std::cout << " resolution_multiplier=" << *config.resolutionMultiplier;
}
if (config.dvdRoot) {
std::cout << " dvd_root=" << *config.dvdRoot;
}
if (config.graphicsApi) {
std::cout << " graphics_api=" << *config.graphicsApi;
}
if (config.frameInterpolationFps) {
std::cout << " frame_interpolation_fps=" << *config.frameInterpolationFps;
}
if (config.skipUnreadyPipelines) {
std::cout << " skip_unready_pipelines=" << (*config.skipUnreadyPipelines ? "true" : "false");
}
if (config.disableCopyFilter) {
std::cout << " disable_copy_filter=" << (*config.disableCopyFilter ? "true" : "false");
}
if (config.showFps) {
std::cout << " show_fps=" << (*config.showFps ? "true" : "false");
}
if (config.textureReplacements) {
std::cout << " texture_replacements=" << (*config.textureReplacements ? "true" : "false");
}
if (config.textureDumps) {
std::cout << " texture_dumps=" << (*config.textureDumps ? "true" : "false");
}
if (config.audioVolume) {
std::cout << " audio_volume=" << *config.audioVolume;
}
if (config.audioMuted) {
std::cout << " audio_muted=" << (*config.audioMuted ? "true" : "false");
}
if (config.networkEnabled) {
std::cout << " network_enabled=" << (*config.networkEnabled ? "true" : "false");
}
if (config.nandRoot) {
std::cout << " nand_root=" << *config.nandRoot;
}
if (config.retroRewindRoot) {
std::cout << " retro_rewind_root=" << *config.retroRewindRoot;
}
}
std::cout << std::endl;
return true;
}();
(void)logged;
}
} // namespace RuntimeConfigFile
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#ifndef MKW_RUNTIME_LOG_H
#define MKW_RUNTIME_LOG_H
#include <cstdio>
#include <iostream>
#include "memory.h"
// Canonical module tags. Use one of these; never write a bare "[...]" prefix
// into a message. (A message whose text spans several output lines repeats the
// tag inline on the continuation lines - the macros only prefix the first.)
#define RT_TAG_RUNTIME "runtime"
#define RT_TAG_CONFIG "runtime-config"
#define RT_TAG_MEMORY "memory"
#define RT_TAG_MOD "mod"
#define RT_TAG_HLE "hle"
#define RT_TAG_OS "os"
#define RT_TAG_GX "gx"
#define RT_TAG_AUDIO "audio"
#define RT_TAG_NET "net"
#define RT_TAG_DVD "dvd"
#define RT_TAG_NAND "nand"
#define RT_TAG_RIIVOLUTION "riivolution"
#define RT_TAG_VI "vi"
// Stream form: RT_LOG(RT_TAG_OS) << "OSCreateThread failed" << std::endl;
#define RT_LOG(tag) (std::cerr << "[" tag "] ")
// printf form: RT_LOGF(RT_TAG_GX, "invalid GXTexObj @0x%08X\n", addr);
// `tag` and the format string must both be literals; they are concatenated.
#define RT_LOGF(tag, ...) std::fprintf(stderr, "[" tag "] " __VA_ARGS__)
// Shared epilogue for the `catch (const Memory::AccessViolation& e)` handlers
// spread across the HLE. `who` is the guest function or operation that faulted;
// the tag is its module.
inline void LogMemoryError(const char* tag, const char* who,
const ::Memory::AccessViolation& e)
{
std::cerr << "[" << tag << "] " << who << ": memory error at 0x" << std::hex
<< e.address() << std::dec << " (" << e.reason() << ")" << std::endl;
}
#endif // MKW_RUNTIME_LOG_H
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <string_view>
namespace RuntimeProduct {
enum class Kind {
BaseGame,
RetroRewind,
};
struct Descriptor {
Kind kind;
std::string_view displayName;
};
// Each public executable links exactly one small provider definition. Keeping
// this selection out of target-wide preprocessor definitions lets the native
// runtime be compiled once and shared by every product.
const Descriptor& Active() noexcept;
inline bool IsRetroRewind() noexcept {
return Active().kind == Kind::RetroRewind;
}
} // namespace RuntimeProduct
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <aurora/aurora.h>
#include <aurora/event.h>
namespace settings_overlay {
// Apply persistent controller settings once Aurora has discovered host devices.
void InitializeRuntimeSettings() noexcept;
// Draw the F10 settings bar before each Aurora present.
void HandleEvents(const AuroraEvent* events) noexcept;
void Draw() noexcept;
bool StartupScreenVisible() noexcept;
void NotifyStrapInputAccepted() noexcept;
void AdvancePresentedFrame() noexcept;
} // namespace settings_overlay
+76
View File
@@ -0,0 +1,76 @@
#pragma once
#include <cstdint>
#include <optional>
#include <ostream>
#include <string>
#include <string_view>
#include <csetjmp>
#include "memory.h"
// Global flag to suppress SEH reporting (caught by system_bridge)
extern bool g_suppressSehReporting;
// Jump buffer for SEH recovery
extern thread_local jmp_buf* g_sehJumpTarget;
// SEH details for the most recent trapped exception (used during ctor execution).
extern thread_local uint32_t g_sehLastExceptionCode;
extern thread_local uintptr_t g_sehLastExceptionAddress;
extern thread_local uintptr_t g_sehLastAccessedAddress;
extern thread_local uint32_t g_sehLastAccessType;
void WriteFatalLog(std::string_view reason);
void SetRuntimeExitCode(int code);
// Centralized crash reporting (defined in main.cpp). Every fatal path funnels
// through these so the per-run log folder always receives the same artifact
// set: crash_<reason>.txt with registers/backtrace/heuristics, plus MEM1/MEM2
// snapshots a developer can walk offline.
namespace RuntimeCrash {
// Writes crash_<reason>.txt (and, once per process, the guest memory
// snapshots) into the current run's log folder. Safe to call from any fatal
// path; never throws.
void WriteCrashArtifacts(std::string_view reason,
std::string_view extraDetails = {},
const uint32_t* missingGuestTarget = nullptr) noexcept;
// Full fatal path for a guest jump to an untranslated/invalid target:
// stderr diagnostics, crash artifacts, popup, exit.
[[noreturn]] void FatalMissingGuestTarget(uint32_t target, struct CpuContext* cpu) noexcept;
} // namespace RuntimeCrash
// Shows a user-facing explanation for a fatal runtime error. Declared here with
// its two siblings; all three are defined in main.cpp so translated dispatch,
// HLE, memory and Aurora callbacks share one popup and duplicate failures do
// not stack dialogs. (The ISA package declares this independently at
// isa/ppc_isa_context.h - that is its standalone host seam, not a duplicate.)
void ShowRuntimeFatalPopup(std::string_view category, std::string_view details) noexcept;
// Mario Kart Wii's translated entry point. The products always boot here, so
// this is applied as the default while parsing the command line; there is no
// flag to override it.
inline constexpr uint32_t kDefaultEntryAddress = 0x800060A4u;
class SystemBridge {
public:
static void Initialize();
private:
static const Memory::RegionConfig* FindRegionConfig(const Memory::Config& config, std::string_view name);
static void SeedLowMemDefaults(const Memory::Config& config);
public:
static void DumpCpuState(const struct CpuContext* cpu);
static void DumpCpuState(std::ostream& os, const struct CpuContext* cpu);
// Crash-path helpers shared by the centralized reporter in main.cpp.
// DumpCrashHeuristics prints plain-language "likely cause" hints derived
// from the register state; WriteGuestMemorySnapshot writes MEM1 to
// `mem1Path` and MEM2 to `mem1Path + ".mem2"`, logging outcomes to `os`.
static void DumpCrashHeuristics(std::ostream& os, const struct CpuContext* cpu,
const uint32_t* missingGuestTarget);
static void WriteGuestMemorySnapshot(std::ostream& os, const char* mem1Path);
};
+50
View File
@@ -0,0 +1,50 @@
#pragma once
#include <chrono>
#include <cstdint>
namespace TimeBaseContract {
// Broadway's time base runs at one quarter of the 243 MHz Wii bus clock.
// Reduced against one billion nanoseconds per second, that is exactly
// 243 time-base ticks per 4000 nanoseconds (60.75 MHz).
inline constexpr uint64_t kBusClockHz = 243'000'000u;
inline constexpr uint64_t kTimeBaseDivider = 4u;
inline constexpr uint64_t kTicksPerSecond = kBusClockHz / kTimeBaseDivider;
inline constexpr uint64_t kNanosecondsPerSecond = 1'000'000'000u;
inline constexpr uint64_t kTickRatioNumerator = 243u;
inline constexpr uint64_t kTickRatioDenominator = 4'000u;
static_assert(kTicksPerSecond == 60'750'000u);
static_assert(kTicksPerSecond * kTickRatioDenominator ==
kNanosecondsPerSecond * kTickRatioNumerator);
// Split the rational conversion around the division so the intermediate
// product cannot overflow. The result is floor(nanoseconds * 243 / 4000).
constexpr uint64_t NanosecondsToTicks(uint64_t nanoseconds) noexcept
{
return (nanoseconds / kTickRatioDenominator) * kTickRatioNumerator +
((nanoseconds % kTickRatioDenominator) * kTickRatioNumerator) /
kTickRatioDenominator;
}
// Convert guest ticks to a host duration without applying scheduling policy.
// Oversized durations retain the existing zero-duration failure behavior.
constexpr std::chrono::nanoseconds TicksToDuration(uint64_t ticks) noexcept
{
constexpr uint64_t kMaxNanoseconds =
static_cast<uint64_t>(std::chrono::nanoseconds::max().count());
constexpr uint64_t kMaxTicks = NanosecondsToTicks(kMaxNanoseconds);
if (ticks == 0 || ticks > kMaxTicks) {
return std::chrono::nanoseconds::zero();
}
const uint64_t nanoseconds =
(ticks / kTickRatioNumerator) * kTickRatioDenominator +
((ticks % kTickRatioNumerator) * kTickRatioDenominator) /
kTickRatioNumerator;
return std::chrono::nanoseconds(static_cast<int64_t>(nanoseconds));
}
} // namespace TimeBaseContract
+236
View File
@@ -0,0 +1,236 @@
// SPDX-License-Identifier: GPL-2.0-or-later
//
// Copyright 2010 Dolphin Emulator Project
// Copyright 2007,2008 Segher Boessenkool
//
// Console identity constants and certificate layout details are derived from
// the Dolphin Emulator
// (https://github.com/dolphin-emu/dolphin):
//
// * Source/Core/Core/IOS/IOSC.cpp - the default console identity constants
// (device id, CA/MS ids, NG key id, default ECC private key and signature).
// * Source/Core/Common/Crypto/ec.cpp - the sect233r1 public-key and signature
// encodings used by IOS. Arithmetic, hashing, randomness and ECDSA are now
// provided by Crypto++.
//
// This file is GPL-2.0-or-later as a consequence; see THIRD-PARTY-NOTICES.md.
#pragma once
#include "isa/big_endian.h"
#include "nand_path.h"
#include <cryptopp/eccrypto.h>
#include <cryptopp/ec2n.h>
#include <cryptopp/oids.h>
#include <cryptopp/osrng.h>
#include <cryptopp/sha.h>
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <optional>
#include <stdexcept>
#include <string>
namespace WiiEsCrypto {
using EcSignature = std::array<uint8_t, 60>;
using EcPublicKey = std::array<uint8_t, 60>;
using EccCert = std::array<uint8_t, 0x180>;
constexpr uint32_t kDeviceId = 0x0403AC68;
constexpr uint32_t kCaId = 1;
constexpr uint32_t kMsId = 2;
constexpr uint32_t kNgKeyId = 0x6AAB8C59;
constexpr std::array<uint8_t, 30> kDefaultPrivateKey{{
0x00, 0xAB, 0xEE, 0xC1, 0xDD, 0xB4, 0xA6, 0x16, 0x6B, 0x70, 0xFD, 0x7E, 0x56, 0x67, 0x70,
0x57, 0x55, 0x27, 0x38, 0xA3, 0x26, 0xC5, 0x46, 0x16, 0xF7, 0x62, 0xC9, 0xED, 0x73, 0xF2,
}};
constexpr EcSignature kDefaultSignature{{
0x00, 0xD8, 0x81, 0x63, 0xB2, 0x00, 0x6B, 0x0B, 0x54, 0x82, 0x88, 0x63, 0x81, 0x1C, 0x00,
0x71, 0x12, 0xED, 0xB7, 0xFD, 0x21, 0xAB, 0x0E, 0x50, 0x0E, 0x1F, 0xBF, 0x78, 0xAD, 0x37,
0x00, 0x71, 0x8D, 0x82, 0x41, 0xEE, 0x45, 0x11, 0xC7, 0x3B, 0xAC, 0x08, 0xB6, 0x83, 0xDC,
0x05, 0xB8, 0xA8, 0x90, 0x1F, 0xA8, 0x2A, 0x0E, 0x4E, 0x76, 0xEF, 0x44, 0x72, 0x99, 0xF8,
}};
struct Identity {
uint32_t deviceId = kDeviceId;
uint32_t caId = kCaId;
uint32_t msId = kMsId;
uint32_t ngKeyId = kNgKeyId;
std::array<uint8_t, 30> privateKey = kDefaultPrivateKey;
EcSignature signature = kDefaultSignature;
bool fromNand = false;
};
inline std::optional<Identity> LoadIdentityFromKeysBin(const std::filesystem::path& path) {
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file) {
return std::nullopt;
}
const std::streamoff fileSize = file.tellg();
if (fileSize < 0x400) {
return std::nullopt;
}
file.seekg(0, std::ios::beg);
std::array<uint8_t, 0x400> dump{};
file.read(reinterpret_cast<char*>(dump.data()), static_cast<std::streamsize>(dump.size()));
if (!file) {
return std::nullopt;
}
Identity identity{};
identity.deviceId = BigEndian::Read32(dump.data() + 0x124);
identity.msId = BigEndian::Read32(dump.data() + 0x200);
identity.caId = BigEndian::Read32(dump.data() + 0x204);
identity.ngKeyId = BigEndian::Read32(dump.data() + 0x208);
std::copy_n(dump.data() + 0x128, identity.privateKey.size(), identity.privateKey.begin());
std::copy_n(dump.data() + 0x20C, identity.signature.size(), identity.signature.begin());
identity.fromNand = true;
const bool privateKeyEmpty = std::all_of(identity.privateKey.begin(), identity.privateKey.end(),
[](uint8_t b) { return b == 0; });
if (identity.deviceId == 0 || identity.caId == 0 || identity.msId == 0 ||
identity.ngKeyId == 0 || privateKeyEmpty) {
return std::nullopt;
}
return identity;
}
inline std::optional<Identity> LoadIdentityFromNand(const std::filesystem::path& nandBase) {
if (nandBase.empty()) {
return std::nullopt;
}
return LoadIdentityFromKeysBin(nandBase / "keys.bin");
}
inline const Identity& CurrentIdentity() {
static const Identity identity = [] {
if (auto loaded = LoadIdentityFromNand(RuntimeNandPath::DiscoverNandRootPath())) {
return *loaded;
}
return Identity{};
}();
return identity;
}
inline std::array<uint8_t, 20> Sha1(const uint8_t* data, size_t size) {
std::array<uint8_t, 20> digest{};
CryptoPP::SHA1 hash;
hash.CalculateDigest(digest.data(), data, size);
return digest;
}
using CryptoEcdsa = CryptoPP::ECDSA<CryptoPP::EC2N, CryptoPP::SHA1>;
inline CryptoEcdsa::PrivateKey MakePrivateKey(const uint8_t* key) {
CryptoPP::DL_GroupParameters_EC<CryptoPP::EC2N> parameters(CryptoPP::ASN1::sect233r1());
const CryptoPP::Integer exponent(key, 30);
if (exponent <= CryptoPP::Integer::Zero() || exponent >= parameters.GetSubgroupOrder()) {
throw std::invalid_argument("Wii ES private key is outside the sect233r1 subgroup");
}
CryptoEcdsa::PrivateKey privateKey;
privateKey.Initialize(parameters, exponent);
return privateKey;
}
inline EcSignature SignMessage(const uint8_t* key, const uint8_t* data, size_t size) {
const CryptoEcdsa::PrivateKey privateKey = MakePrivateKey(key);
const CryptoEcdsa::Signer signer(privateKey);
if (signer.SignatureLength() != EcSignature{}.size()) {
throw std::runtime_error("Crypto++ returned an unexpected sect233r1 signature size");
}
thread_local CryptoPP::AutoSeededRandomPool random;
EcSignature signature{};
const size_t written = signer.SignMessage(random, data, size, signature.data());
if (written != signature.size()) {
throw std::runtime_error("Crypto++ produced a truncated Wii ES signature");
}
return signature;
}
inline EcPublicKey PrivToPub(const uint8_t* key) {
const CryptoEcdsa::PrivateKey privateKey = MakePrivateKey(key);
CryptoEcdsa::PublicKey publicKey;
privateKey.MakePublicKey(publicKey);
const auto& point = publicKey.GetPublicElement();
EcPublicKey out{};
point.x.Encode(out.data(), 30);
point.y.Encode(out.data() + 30, 30);
return out;
}
inline void WriteString(uint8_t* dst, size_t dstSize, const std::string& text) {
std::memset(dst, 0, dstSize);
std::memcpy(dst, text.data(), std::min(dstSize, text.size()));
}
inline std::string Hex8(uint32_t value) {
static constexpr char kHex[] = "0123456789abcdef";
std::string out(8, '0');
for (int i = 7; i >= 0; --i) {
out[static_cast<size_t>(i)] = kHex[value & 0xFu];
value >>= 4;
}
return out;
}
inline std::string Hex16(uint64_t value) {
return Hex8(static_cast<uint32_t>(value >> 32)) + Hex8(static_cast<uint32_t>(value));
}
inline EccCert MakeBlankEccCert(const std::string& issuer, const std::string& name,
const uint8_t* privateKey, uint32_t keyId) {
EccCert cert{};
BigEndian::Write32(cert.data() + 0x00, 0x00010002u);
WriteString(cert.data() + 0x80, 0x40, issuer);
BigEndian::Write32(cert.data() + 0xC0, 2);
WriteString(cert.data() + 0xC4, 0x40, name);
BigEndian::Write32(cert.data() + 0x104, keyId);
const EcPublicKey pub = PrivToPub(privateKey);
std::copy(pub.begin(), pub.end(), cert.begin() + 0x108);
return cert;
}
inline EccCert GetDeviceCertificate(const Identity& identity = CurrentIdentity()) {
const std::string issuer = "Root-CA" + Hex8(identity.caId) + "-MS" + Hex8(identity.msId);
const std::string name = "NG" + Hex8(identity.deviceId);
EccCert cert = MakeBlankEccCert(issuer, name, identity.privateKey.data(), identity.ngKeyId);
std::copy(identity.signature.begin(), identity.signature.end(), cert.begin() + 0x04);
return cert;
}
inline void Sign(uint64_t titleId, const uint8_t* data, size_t dataSize,
const Identity& identity,
EcSignature& sigOut, EccCert& apCertOut) {
std::array<uint8_t, 30> apPrivate{};
apPrivate[0x1D] = 1;
const std::string signer = "Root-CA" + Hex8(identity.caId) + "-MS" + Hex8(identity.msId) +
"-NG" + Hex8(identity.deviceId);
const std::string name = "AP" + Hex16(titleId);
apCertOut = MakeBlankEccCert(signer, name, apPrivate.data(), 0);
const EcSignature apCertSig = SignMessage(
identity.privateKey.data(), apCertOut.data() + 0x80, apCertOut.size() - 0x80);
std::copy(apCertSig.begin(), apCertSig.end(), apCertOut.begin() + 0x04);
sigOut = SignMessage(apPrivate.data(), data, dataSize);
}
inline void Sign(uint64_t titleId, const uint8_t* data, size_t dataSize,
EcSignature& sigOut, EccCert& apCertOut) {
Sign(titleId, data, dataSize, CurrentIdentity(), sigOut, apCertOut);
}
} // namespace WiiEsCrypto