mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-11 17:31:29 -04:00
init
This commit is contained in:
@@ -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)
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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_(®isters_) {}
|
||||
|
||||
GuestInterruptCallbackContext(const GuestInterruptCallbackContext&) = delete;
|
||||
GuestInterruptCallbackContext& operator=(const GuestInterruptCallbackContext&) = delete;
|
||||
|
||||
CpuContext* get() noexcept { return ®isters_; }
|
||||
|
||||
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_;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)));
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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"
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user