mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-11 09:25:05 -04:00
796 lines
31 KiB
C++
796 lines
31 KiB
C++
#include "memory.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <atomic>
|
|
#include <cctype>
|
|
#include <chrono>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <cstdio>
|
|
#include <iomanip>
|
|
#include <iostream>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <optional>
|
|
#include "ppc_runtime.h"
|
|
#include "runtime_log.h"
|
|
#include "system_bridge.h"
|
|
#include <mutex>
|
|
#include <sstream>
|
|
#include <unordered_map>
|
|
|
|
#if defined(_WIN32)
|
|
#ifndef NOMINMAX
|
|
#define NOMINMAX
|
|
#endif
|
|
#include <windows.h>
|
|
#include <dbghelp.h>
|
|
#endif
|
|
|
|
MemoryInline::PageEntry MemoryInline::g_pageTable[MemoryInline::kPageCount]{};
|
|
uintptr_t MemoryInline::g_fullPageBias[MemoryInline::kPageCount]{};
|
|
uintptr_t MemoryInline::g_fullReadablePageBias[MemoryInline::kPageCount]{};
|
|
uintptr_t MemoryInline::g_fullWritablePageBias[MemoryInline::kPageCount]{};
|
|
const MemoryInline::SparseWritablePageTable*
|
|
MemoryInline::g_sparseWritablePageTables[MemoryInline::kPageCount]{};
|
|
uint8_t MemoryInline::g_deferredReadCoveredPages[MemoryInline::kPageCount]{};
|
|
template <typename T>
|
|
T MemoryInline::ReadResolvedFallback(uint32_t addr) {
|
|
if constexpr (sizeof(T) == 1) return Memory::Read8(addr);
|
|
if constexpr (sizeof(T) == 2) return Memory::Read16(addr);
|
|
if constexpr (sizeof(T) == 4) return Memory::Read32(addr);
|
|
return Memory::Read64(addr);
|
|
}
|
|
template uint8_t MemoryInline::ReadResolvedFallback<uint8_t>(uint32_t);
|
|
template uint16_t MemoryInline::ReadResolvedFallback<uint16_t>(uint32_t);
|
|
template uint32_t MemoryInline::ReadResolvedFallback<uint32_t>(uint32_t);
|
|
template uint64_t MemoryInline::ReadResolvedFallback<uint64_t>(uint32_t);
|
|
float MemoryInline::ReadResolvedFallbackFloat32(uint32_t addr) { return Memory::ReadFloat32(addr); }
|
|
double MemoryInline::ReadResolvedFallbackFloat64(uint32_t addr) { return Memory::ReadFloat64(addr); }
|
|
template <typename T>
|
|
void MemoryInline::WriteResolvedFallback(uint32_t addr, T value) {
|
|
if constexpr (sizeof(T) == 1) Memory::Write8(addr, value);
|
|
else if constexpr (sizeof(T) == 2) Memory::Write16(addr, value);
|
|
else if constexpr (sizeof(T) == 4) Memory::Write32(addr, value);
|
|
else Memory::Write64(addr, value);
|
|
}
|
|
template void MemoryInline::WriteResolvedFallback<uint8_t>(uint32_t, uint8_t);
|
|
template void MemoryInline::WriteResolvedFallback<uint16_t>(uint32_t, uint16_t);
|
|
template void MemoryInline::WriteResolvedFallback<uint32_t>(uint32_t, uint32_t);
|
|
template void MemoryInline::WriteResolvedFallback<uint64_t>(uint32_t, uint64_t);
|
|
void MemoryInline::WriteResolvedFallbackFloat32(uint32_t addr, double val) { Memory::WriteFloat32(addr, val); }
|
|
void MemoryInline::WriteResolvedFallbackFloat64(uint32_t addr, double val) { Memory::WriteFloat64(addr, val); }
|
|
namespace {
|
|
struct DeferredRead {
|
|
uint64_t token = 0;
|
|
uint32_t start = 0;
|
|
size_t length = 0;
|
|
Memory::DeferredReadCallback callback = nullptr;
|
|
void* user = nullptr;
|
|
};
|
|
|
|
std::mutex& DeferredReadMutex() {
|
|
static std::mutex mutex;
|
|
return mutex;
|
|
}
|
|
|
|
std::vector<DeferredRead>& DeferredReads() {
|
|
static std::vector<DeferredRead> reads;
|
|
return reads;
|
|
}
|
|
|
|
uint64_t& NextDeferredReadToken() {
|
|
static uint64_t token = 1;
|
|
return token;
|
|
}
|
|
|
|
bool RangesOverlap(uint32_t firstStart, size_t firstLength,
|
|
uint32_t secondStart, size_t secondLength) {
|
|
const uint64_t firstEnd = static_cast<uint64_t>(firstStart) + firstLength;
|
|
const uint64_t secondEnd = static_cast<uint64_t>(secondStart) + secondLength;
|
|
return static_cast<uint64_t>(firstStart) < secondEnd &&
|
|
static_cast<uint64_t>(secondStart) < firstEnd;
|
|
}
|
|
|
|
void RefreshDeferredReadPage(uint32_t page) {
|
|
const uint32_t pageStart = page << MemoryInline::kPageShift;
|
|
const auto& reads = DeferredReads();
|
|
const bool covered = std::any_of(reads.begin(), reads.end(), [pageStart](const DeferredRead& read) {
|
|
return RangesOverlap(pageStart, MemoryInline::kPageSize, read.start, read.length);
|
|
});
|
|
MemoryInline::g_fullReadablePageBias[page] =
|
|
!covered ? MemoryInline::g_fullPageBias[page] : 0;
|
|
MemoryInline::g_deferredReadCoveredPages[page] = covered ? 1 : 0;
|
|
}
|
|
|
|
struct Region {
|
|
Memory::RegionConfig config;
|
|
uint8_t* storagePtr = nullptr;
|
|
size_t storageSize = 0;
|
|
};
|
|
|
|
std::vector<Region>& Regions() {
|
|
static std::vector<Region> regions;
|
|
return regions;
|
|
}
|
|
|
|
std::mutex& RegionMutex() {
|
|
static std::mutex mutex;
|
|
return mutex;
|
|
}
|
|
|
|
std::array<std::unique_ptr<MemoryInline::SparseWritablePageTable>,
|
|
MemoryInline::kPageCount>& SparseWritablePageTableStorage() {
|
|
static std::array<std::unique_ptr<MemoryInline::SparseWritablePageTable>,
|
|
MemoryInline::kPageCount> storage;
|
|
return storage;
|
|
}
|
|
|
|
void ClearPageTable() {
|
|
auto* table = MemoryInline::g_pageTable;
|
|
for (uint32_t i = 0; i < MemoryInline::kPageCount; ++i) {
|
|
auto& entry = table[i];
|
|
entry.base = nullptr;
|
|
entry.limit = 0;
|
|
MemoryInline::g_fullPageBias[i] = 0;
|
|
MemoryInline::g_fullReadablePageBias[i] = 0;
|
|
MemoryInline::g_fullWritablePageBias[i] = 0;
|
|
MemoryInline::g_deferredReadCoveredPages[i] = 0;
|
|
MemoryInline::g_sparseWritablePageTables[i] = nullptr;
|
|
SparseWritablePageTableStorage()[i].reset();
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
uint64_t Memory::RegisterDeferredRead(uint32_t addr, size_t length,
|
|
DeferredReadCallback callback, void* user) {
|
|
if (length == 0 || callback == nullptr ||
|
|
static_cast<uint64_t>(addr) + length > (uint64_t{1} << 32)) {
|
|
return 0;
|
|
}
|
|
|
|
std::lock_guard lock{DeferredReadMutex()};
|
|
uint64_t token = NextDeferredReadToken()++;
|
|
if (token == 0) token = NextDeferredReadToken()++;
|
|
DeferredReads().push_back({token, addr, length, callback, user});
|
|
const uint32_t firstPage = addr >> MemoryInline::kPageShift;
|
|
const uint32_t lastPage = static_cast<uint32_t>(
|
|
(static_cast<uint64_t>(addr) + length - 1) >> MemoryInline::kPageShift);
|
|
for (uint32_t page = firstPage; page <= lastPage; ++page) {
|
|
MemoryInline::g_fullReadablePageBias[page] = 0;
|
|
MemoryInline::g_deferredReadCoveredPages[page] = 1;
|
|
}
|
|
// Clearing the readable bias only intercepts the checked path. A flat read
|
|
// needs the host pages themselves to trap, which is what PAGE_NOACCESS on
|
|
// the guest view does; the vectored handler materializes the copy, restores
|
|
// the protection and re-runs the access.
|
|
GuestFlat::ProtectDeferredRange(addr, length);
|
|
return token;
|
|
}
|
|
|
|
void Memory::ClearDeferredReads() {
|
|
std::lock_guard lock{DeferredReadMutex()};
|
|
auto& reads = DeferredReads();
|
|
std::vector<uint32_t> pages;
|
|
for (const auto& read : reads) {
|
|
const uint32_t firstPage = read.start >> MemoryInline::kPageShift;
|
|
const uint32_t lastPage = static_cast<uint32_t>(
|
|
(static_cast<uint64_t>(read.start) + read.length - 1) >> MemoryInline::kPageShift);
|
|
for (uint32_t page = firstPage; page <= lastPage; ++page) pages.push_back(page);
|
|
GuestFlat::UnprotectDeferredRange(read.start, read.length);
|
|
}
|
|
reads.clear();
|
|
std::sort(pages.begin(), pages.end());
|
|
pages.erase(std::unique(pages.begin(), pages.end()), pages.end());
|
|
for (uint32_t page : pages) RefreshDeferredReadPage(page);
|
|
}
|
|
|
|
bool MemoryInline::ResolveDeferredReads(uint32_t addr, size_t length) {
|
|
std::vector<DeferredRead> matches;
|
|
std::vector<uint32_t> affectedPages;
|
|
{
|
|
std::lock_guard lock{DeferredReadMutex()};
|
|
auto& reads = DeferredReads();
|
|
for (auto it = reads.begin(); it != reads.end();) {
|
|
if (!RangesOverlap(addr, length, it->start, it->length)) {
|
|
++it;
|
|
continue;
|
|
}
|
|
const uint32_t firstPage = it->start >> kPageShift;
|
|
const uint32_t lastPage = static_cast<uint32_t>(
|
|
(static_cast<uint64_t>(it->start) + it->length - 1) >> kPageShift);
|
|
for (uint32_t page = firstPage; page <= lastPage; ++page) affectedPages.push_back(page);
|
|
matches.push_back(*it);
|
|
// Idempotent: the vectored handler already dropped the protection
|
|
// for the range whose trap brought us here.
|
|
GuestFlat::UnprotectDeferredRange(it->start, it->length);
|
|
it = reads.erase(it);
|
|
}
|
|
std::sort(affectedPages.begin(), affectedPages.end());
|
|
affectedPages.erase(std::unique(affectedPages.begin(), affectedPages.end()), affectedPages.end());
|
|
for (uint32_t page : affectedPages) RefreshDeferredReadPage(page);
|
|
}
|
|
|
|
// The range is removed before entering renderer code so any memory reads
|
|
// made while submitting the copy cannot recursively trigger it.
|
|
for (const auto& match : matches) {
|
|
if (!match.callback(match.user)) {
|
|
throw Memory::AccessViolation(addr, length, "deferred read materialization failed");
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
namespace {
|
|
|
|
void BuildPageTable() {
|
|
auto* table = MemoryInline::g_pageTable;
|
|
for (const auto& region : Regions()) {
|
|
const uint64_t base = region.config.baseAddress;
|
|
const uint64_t size = region.storageSize;
|
|
const uint64_t end = base + size;
|
|
if (size == 0) {
|
|
continue;
|
|
}
|
|
|
|
const uint32_t startPage = static_cast<uint32_t>(base >> MemoryInline::kPageShift);
|
|
const uint32_t endPage = static_cast<uint32_t>((end - 1) >> MemoryInline::kPageShift);
|
|
for (uint32_t page = startPage; page <= endPage; ++page) {
|
|
const uint64_t pageBase = static_cast<uint64_t>(page) << MemoryInline::kPageShift;
|
|
const uint64_t offset = pageBase - base;
|
|
if (offset >= size) {
|
|
continue;
|
|
}
|
|
const uint32_t limit = static_cast<uint32_t>(std::min<uint64_t>(MemoryInline::kPageSize, size - offset));
|
|
table[page].base = region.storagePtr + static_cast<size_t>(offset);
|
|
table[page].limit = limit;
|
|
}
|
|
}
|
|
|
|
// The last page of each contiguous mapping remains on the checked path so
|
|
// an access straddling its end cannot escape the mapped region. Every
|
|
// preceding full page can safely service native accesses up to 8 bytes,
|
|
// including a cross-page access into its contiguous successor.
|
|
for (uint32_t page = 0; page + 1 < MemoryInline::kPageCount; ++page) {
|
|
const auto& current = table[page];
|
|
const auto& next = table[page + 1];
|
|
if (!current.base || current.limit != MemoryInline::kPageSize ||
|
|
!next.base || next.limit < MemoryInline::kMaxFastScalarSize - 1u ||
|
|
next.base != current.base + MemoryInline::kPageSize)
|
|
continue;
|
|
const uintptr_t guestPageBase = static_cast<uintptr_t>(page) << MemoryInline::kPageShift;
|
|
const uintptr_t bias = reinterpret_cast<uintptr_t>(current.base) - guestPageBase;
|
|
MemoryInline::g_fullPageBias[page] = bias + 1u;
|
|
}
|
|
}
|
|
|
|
void RefreshFastPathTables() {
|
|
auto& sparseStorage = SparseWritablePageTableStorage();
|
|
for (uint32_t page = 0; page < MemoryInline::kPageCount; ++page) {
|
|
const uintptr_t bias = MemoryInline::g_fullPageBias[page];
|
|
MemoryInline::g_fullReadablePageBias[page] = bias;
|
|
MemoryInline::g_deferredReadCoveredPages[page] = 0;
|
|
MemoryInline::g_sparseWritablePageTables[page] = nullptr;
|
|
sparseStorage[page].reset();
|
|
|
|
const bool coarseExecutable =
|
|
RecompMod::g_executableWriteGuardCoarsePages[page].load(
|
|
std::memory_order_relaxed) != 0;
|
|
MemoryInline::g_fullWritablePageBias[page] = coarseExecutable ? 0 : bias;
|
|
if (!coarseExecutable || bias == 0)
|
|
continue;
|
|
|
|
auto sparse = std::make_unique<MemoryInline::SparseWritablePageTable>();
|
|
bool hasExecutableSubPage = false;
|
|
bool hasWritableSubPage = false;
|
|
const uint32_t firstExactPage =
|
|
page * MemoryInline::kWritableSubPagesPerPage;
|
|
for (uint32_t subPage = 0;
|
|
subPage < MemoryInline::kWritableSubPagesPerPage; ++subPage) {
|
|
const bool executable =
|
|
RecompMod::g_executableWriteGuardPages[firstExactPage + subPage].load(
|
|
std::memory_order_relaxed) != 0;
|
|
hasExecutableSubPage |= executable;
|
|
hasWritableSubPage |= !executable;
|
|
sparse->encodedBias[subPage] = executable ? 0 : bias;
|
|
}
|
|
|
|
// A homogeneous executable page has no writable fast path; a
|
|
// homogeneous data page already uses g_fullWritablePageBias. Retain an
|
|
// allocation only for the intended mixed case.
|
|
if (hasExecutableSubPage && hasWritableSubPage) {
|
|
MemoryInline::g_sparseWritablePageTables[page] = sparse.get();
|
|
sparseStorage[page] = std::move(sparse);
|
|
}
|
|
}
|
|
std::lock_guard lock{DeferredReadMutex()};
|
|
for (const auto& read : DeferredReads()) {
|
|
const uint32_t firstPage = read.start >> MemoryInline::kPageShift;
|
|
const uint32_t lastPage = static_cast<uint32_t>(
|
|
(static_cast<uint64_t>(read.start) + read.length - 1) >> MemoryInline::kPageShift);
|
|
for (uint32_t page = firstPage; page <= lastPage; ++page) {
|
|
MemoryInline::g_fullReadablePageBias[page] = 0;
|
|
MemoryInline::g_deferredReadCoveredPages[page] = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
Region* TryResolveRegion(uint32_t address, size_t length) {
|
|
auto& regions = Regions();
|
|
for (auto& region : regions) {
|
|
const uint64_t base = region.config.baseAddress;
|
|
const uint64_t limit = base + region.storageSize;
|
|
const uint64_t addr = address;
|
|
const uint64_t end = addr + length;
|
|
if (addr >= base && end <= limit) {
|
|
return ®ion;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
Region& ResolveRegion(uint32_t address, size_t length) {
|
|
if (Region* region = TryResolveRegion(address, length)) {
|
|
return *region;
|
|
}
|
|
// Emit extra context to help diagnose early-boot accesses that miss the map.
|
|
if (auto* cpu = TryGetCpuContext()) {
|
|
const uint32_t active = RecompMod::CurrentTranslatedExecutionAddress();
|
|
RT_LOG(RT_TAG_MEMORY) << "AccessViolation ctx=" << cpu
|
|
<< " pc=0x" << std::hex << cpu->pc
|
|
<< " active=0x" << active
|
|
<< " lr=0x" << cpu->lr
|
|
<< " r1=0x" << cpu->gpr[1]
|
|
<< " r8=0x" << cpu->gpr[8]
|
|
<< " r9=0x" << cpu->gpr[9]
|
|
<< " r12=0x" << cpu->gpr[12]
|
|
<< " r30=0x" << cpu->gpr[30]
|
|
<< " r31=0x" << cpu->gpr[31]
|
|
<< std::dec
|
|
<< " addr=0x" << std::hex << address
|
|
<< " len=" << length << std::dec
|
|
<< " reason=no mapped region" << std::endl;
|
|
} else {
|
|
RT_LOG(RT_TAG_MEMORY) << "AccessViolation ctx=(null) addr=0x" << std::hex << address
|
|
<< " len=" << length << std::dec << " reason=no mapped region" << std::endl;
|
|
}
|
|
RT_LOG(RT_TAG_MEMORY) << "===== DUMPING CPU STATE =====" << std::endl;
|
|
SystemBridge::DumpCpuState(TryGetCpuContext());
|
|
// Hexdump guest memory around pointer-carrying registers so a corrupted
|
|
// structure's surroundings (e.g. ASCII sprayed over a link pointer) are
|
|
// visible in the report without a debugger attached.
|
|
if (auto* cpu = TryGetCpuContext()) {
|
|
for (const int reg : {4, 5, 6, 7, 8, 26, 27, 28, 29, 30, 31}) {
|
|
const uint32_t base = cpu->gpr[reg];
|
|
if (base < 0x80000000u || base >= 0x94000000u)
|
|
continue;
|
|
const uint32_t start = (base - 0x40u) & ~0xFu;
|
|
RT_LOG(RT_TAG_MEMORY) << "hexdump around r" << reg << "=0x" << std::hex << base << ":" << std::endl;
|
|
for (uint32_t row = 0; row < 16; ++row) {
|
|
const uint32_t rowAddr = start + row * 16u;
|
|
RT_LOG(RT_TAG_MEMORY) << " 0x" << std::hex << rowAddr << ":";
|
|
char ascii[17] = {};
|
|
for (uint32_t i = 0; i < 16; ++i) {
|
|
uint8_t byte = 0;
|
|
if (!MemoryInline::TryReadGuestScalar(rowAddr + i, byte)) {
|
|
std::cerr << " ??";
|
|
ascii[i] = '?';
|
|
continue;
|
|
}
|
|
std::cerr << " " << std::setw(2) << std::setfill('0') << static_cast<uint32_t>(byte);
|
|
ascii[i] = (byte >= 0x20 && byte < 0x7F) ? static_cast<char>(byte) : '.';
|
|
}
|
|
std::cerr << " |" << ascii << "|" << std::dec << std::setfill(' ') << std::endl;
|
|
}
|
|
}
|
|
}
|
|
#if defined(_WIN32)
|
|
void* frames[32]{};
|
|
const USHORT captured = CaptureStackBackTrace(0, static_cast<DWORD>(std::size(frames)), frames, nullptr);
|
|
const auto imageBase = reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
|
|
HANDLE process = GetCurrentProcess();
|
|
static const bool symbolsReady = [] {
|
|
SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS | SYMOPT_LOAD_LINES);
|
|
if (SymInitialize(GetCurrentProcess(), nullptr, TRUE) != FALSE)
|
|
return true;
|
|
// The runtime crash reporter may already own the process-wide DbgHelp
|
|
// session. Reuse it rather than treating ERROR_INVALID_PARAMETER as
|
|
// symbol unavailability.
|
|
return GetLastError() == ERROR_INVALID_PARAMETER;
|
|
}();
|
|
RT_LOG(RT_TAG_MEMORY) << "host stack at first invalid guest access:" << std::endl;
|
|
for (USHORT index = 0; index < captured; ++index) {
|
|
const auto addressValue = reinterpret_cast<uintptr_t>(frames[index]);
|
|
RT_LOG(RT_TAG_MEMORY) << " #" << index << " absolute=0x" << std::hex << addressValue;
|
|
if (imageBase != 0 && addressValue >= imageBase)
|
|
std::cerr << " image+0x" << (addressValue - imageBase);
|
|
std::array<char, sizeof(SYMBOL_INFO) + MAX_SYM_NAME> symbolBuffer{};
|
|
auto* symbol = reinterpret_cast<SYMBOL_INFO*>(symbolBuffer.data());
|
|
symbol->SizeOfStruct = sizeof(SYMBOL_INFO);
|
|
symbol->MaxNameLen = MAX_SYM_NAME;
|
|
DWORD64 displacement = 0;
|
|
if (symbolsReady && SymFromAddr(process, addressValue, &displacement, symbol))
|
|
std::cerr << " " << symbol->Name << "+0x" << displacement;
|
|
std::cerr << std::dec << std::endl;
|
|
}
|
|
#endif
|
|
std::cerr.flush();
|
|
throw Memory::AccessViolation(address, length, "no mapped region");
|
|
}
|
|
|
|
GuestFlat::Backing ClassifyBacking(uint32_t baseAddress) {
|
|
// MEM1: physical (0x00000000), cached (0x80000000), uncached (0xC0000000)
|
|
if ((baseAddress >= Memory::kMem1PhysicalBase &&
|
|
baseAddress < Memory::kMem1PhysicalBase + Memory::kMem1Size) ||
|
|
(baseAddress >= Memory::kMem1CachedBase &&
|
|
baseAddress < Memory::kMem1CachedBase + Memory::kMem1Size) ||
|
|
(baseAddress >= Memory::kMem1UncachedBase &&
|
|
baseAddress < Memory::kMem1UncachedBase + Memory::kMem1Size)) {
|
|
return GuestFlat::Backing::Mem1;
|
|
}
|
|
|
|
// NDEV-sized MEM2: physical (0x10000000), cached (0x90000000),
|
|
// uncached (0xD0000000). Mario Kart Wii detects this configuration and
|
|
// creates its original EGGRootDebug expansion heap.
|
|
if ((baseAddress >= Memory::kMem2PhysicalBase &&
|
|
baseAddress < Memory::kMem2PhysicalEnd) ||
|
|
(baseAddress >= Memory::kMem2CachedBase &&
|
|
baseAddress < Memory::kMem2CachedEnd) ||
|
|
(baseAddress >= Memory::kMem2UncachedBase &&
|
|
baseAddress < Memory::kMem2UncachedEnd)) {
|
|
return GuestFlat::Backing::Mem2;
|
|
}
|
|
|
|
return GuestFlat::Backing::Owned;
|
|
}
|
|
|
|
|
|
template <typename T>
|
|
T ReadScalar(uint32_t address) {
|
|
// This is the cold *Slow path; Memory::GetPointer already tries the fast
|
|
// probe first, so repeating it here only guaranteed a second miss.
|
|
auto* ptr = Memory::GetPointer(address, sizeof(T));
|
|
if constexpr (sizeof(T) == 1) {
|
|
return *ptr;
|
|
} else {
|
|
T value = 0;
|
|
std::memcpy(&value, ptr, sizeof(T));
|
|
return MemoryInline::MaybeByteSwap(value);
|
|
}
|
|
}
|
|
|
|
template <typename T>
|
|
void WriteScalar(uint32_t address, T value) {
|
|
if (RecompMod::HandleExecutableWrite(address, sizeof(T), static_cast<uint64_t>(value))) {
|
|
return;
|
|
}
|
|
|
|
auto* ptr = Memory::GetPointer(address, sizeof(T));
|
|
if constexpr (sizeof(T) == 1) {
|
|
*ptr = static_cast<uint8_t>(value);
|
|
} else {
|
|
const T swapped = MemoryInline::MaybeByteSwap(value);
|
|
std::memcpy(ptr, &swapped, sizeof(T));
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Memory::AccessViolation::AccessViolation(uint32_t address, size_t length, std::string_view reason)
|
|
: std::runtime_error([&]() {
|
|
std::ostringstream oss;
|
|
oss << "Memory access violation at 0x" << std::hex << std::uppercase << address
|
|
<< " (+0x" << length << ") :: " << reason;
|
|
return oss.str();
|
|
}()),
|
|
address_(address),
|
|
length_(length),
|
|
reason_(reason) {}
|
|
|
|
void Memory::RefreshWritableFastPathsForExecutableRanges() {
|
|
std::lock_guard<std::mutex> lock(RegionMutex());
|
|
RefreshFastPathTables();
|
|
}
|
|
|
|
Memory::Config Memory::Config::WiiDefaults() {
|
|
// Physical / cached / uncached views of MEM1 and MEM2, then the locked cache.
|
|
Config config;
|
|
|
|
config.regions.push_back(RegionConfig{
|
|
.name = "MEM1_PHYS",
|
|
.baseAddress = Memory::kMem1PhysicalBase,
|
|
.sizeBytes = Memory::kMem1Size,
|
|
});
|
|
|
|
config.regions.push_back(RegionConfig{
|
|
.name = "MEM1",
|
|
.baseAddress = Memory::kMem1CachedBase,
|
|
.sizeBytes = Memory::kMem1Size,
|
|
});
|
|
|
|
config.regions.push_back(RegionConfig{
|
|
.name = "MEM1_UNCACHED",
|
|
.baseAddress = Memory::kMem1UncachedBase,
|
|
.sizeBytes = Memory::kMem1Size,
|
|
});
|
|
|
|
config.regions.push_back(RegionConfig{
|
|
.name = "MEM2_PHYS",
|
|
.baseAddress = Memory::kMem2PhysicalBase,
|
|
.sizeBytes = Memory::kMem2Size,
|
|
});
|
|
|
|
config.regions.push_back(RegionConfig{
|
|
.name = "MEM2",
|
|
.baseAddress = Memory::kMem2CachedBase,
|
|
.sizeBytes = Memory::kMem2Size,
|
|
});
|
|
|
|
config.regions.push_back(RegionConfig{
|
|
.name = "MEM2_UNCACHED",
|
|
.baseAddress = Memory::kMem2UncachedBase,
|
|
.sizeBytes = Memory::kMem2Size,
|
|
});
|
|
|
|
// Kamek module overlay: 2 MiB above MEM1, which the game believes ends at 0x81800000, so this
|
|
// costs no arena space (unlike the old in-arena reservation that shrank the race scene heaps).
|
|
// Base must stay within +/-32 MiB of every DOL/StaticR hook site for Kamek Rel24 branches to encode.
|
|
config.regions.push_back(RegionConfig{
|
|
.name = "MEM1_KAMEK_OVERLAY",
|
|
.baseAddress = 0x81800000,
|
|
.sizeBytes = 0x200000,
|
|
});
|
|
|
|
// Locked cache (THP decoder fast RAM) is really 16KB at 0xE0000000, but a sub-page mapping can
|
|
// never enter the coarse 1MiB bias tables, forcing every THP load/store through the checked
|
|
// fallback (the dominant cost of THP-heavy screens). Back a full 1MiB page plus the successor
|
|
// page the bias builder needs, so locked-cache access stays on the two-instruction native path.
|
|
constexpr size_t lcSize = MemoryInline::kPageSize + 4096u;
|
|
config.regions.push_back(RegionConfig{
|
|
.name = "LOCKED_CACHE",
|
|
.baseAddress = 0xE0000000,
|
|
.sizeBytes = lcSize,
|
|
});
|
|
|
|
return config;
|
|
}
|
|
|
|
void Memory::Init(size_t mem1Size) {
|
|
Config config;
|
|
config.regions.push_back(RegionConfig{.name = "MEM1", .baseAddress = 0x80000000, .sizeBytes = mem1Size});
|
|
Init(config);
|
|
}
|
|
|
|
void Memory::Init(const Config& config) {
|
|
std::lock_guard<std::mutex> lock(RegionMutex());
|
|
auto& regions = Regions();
|
|
regions.clear();
|
|
ClearPageTable();
|
|
regions.reserve(config.regions.size());
|
|
|
|
// The flat reservation backs every guest view from one shared section object, so cached/uncached/
|
|
// physical mirrors alias as before. GetPointer still hands out the unprotected host alias, so
|
|
// native code (image loading, DVD reads, HLE) is unaffected by the guest view's protections.
|
|
{
|
|
std::vector<GuestFlat::RegionRequest> flatRegions;
|
|
flatRegions.reserve(config.regions.size());
|
|
for (const auto& regionConfig : config.regions) {
|
|
flatRegions.push_back(GuestFlat::RegionRequest{
|
|
regionConfig.baseAddress, regionConfig.sizeBytes,
|
|
ClassifyBacking(regionConfig.baseAddress)});
|
|
}
|
|
GuestFlat::Initialize(flatRegions);
|
|
}
|
|
|
|
// Map virtual regions onto that backing store, with mirroring.
|
|
for (const auto& regionConfig : config.regions) {
|
|
Region instance;
|
|
instance.config = regionConfig;
|
|
|
|
instance.storagePtr = GuestFlat::HostPointer(regionConfig.baseAddress);
|
|
if (instance.storagePtr == nullptr && regionConfig.sizeBytes != 0) {
|
|
throw std::runtime_error("Flat guest mapping is missing region '" + regionConfig.name + "'");
|
|
}
|
|
instance.storageSize = regionConfig.sizeBytes;
|
|
|
|
regions.emplace_back(std::move(instance));
|
|
}
|
|
|
|
BuildPageTable();
|
|
RefreshFastPathTables();
|
|
}
|
|
|
|
void Memory::Reset() {
|
|
ClearDeferredReads();
|
|
std::lock_guard<std::mutex> lock(RegionMutex());
|
|
Regions().clear();
|
|
ClearPageTable();
|
|
}
|
|
|
|
namespace {
|
|
// MMIO reads have no backing store or generic HLE. Answering zero (the old sparse fallback) turned
|
|
// unimplemented devices into silent hangs, so unmapped reads throw like MMIO writes; every device
|
|
// we do implement is hooked at the function level, so a hit here means a missing HLE hook.
|
|
[[noreturn]] void ThrowMmioReadBlocked(uint32_t addr, size_t length) {
|
|
std::ostringstream reason;
|
|
reason << (MemoryInline::IsGpuFifoAddress(addr)
|
|
? "GPU FIFO read blocked (the gather pipe is write-only)"
|
|
: "MMIO read blocked (non-GPU)")
|
|
<< "; add HLE for this device instead of answering zero"
|
|
<< " (active=0x" << std::hex << std::uppercase
|
|
<< RecompMod::CurrentTranslatedExecutionAddress() << std::dec << std::nouppercase << ")";
|
|
throw Memory::AccessViolation(addr, length, reason.str());
|
|
}
|
|
} // namespace
|
|
|
|
uint8_t MemoryInline::Read8Slow(uint32_t addr) {
|
|
if (IsMmioAddress(addr)) {
|
|
ThrowMmioReadBlocked(addr, sizeof(uint8_t));
|
|
}
|
|
ResolveDeferredReads(addr, sizeof(uint8_t));
|
|
return ReadScalar<uint8_t>(addr);
|
|
}
|
|
|
|
uint16_t MemoryInline::Read16Slow(uint32_t addr) {
|
|
if (IsMmioAddress(addr)) {
|
|
ThrowMmioReadBlocked(addr, sizeof(uint16_t));
|
|
}
|
|
ResolveDeferredReads(addr, sizeof(uint16_t));
|
|
return ReadScalar<uint16_t>(addr);
|
|
}
|
|
|
|
uint32_t MemoryInline::Read32Slow(uint32_t addr) {
|
|
if (IsMmioAddress(addr)) {
|
|
ThrowMmioReadBlocked(addr, sizeof(uint32_t));
|
|
}
|
|
ResolveDeferredReads(addr, sizeof(uint32_t));
|
|
const uint32_t value = ReadScalar<uint32_t>(addr);
|
|
return value;
|
|
}
|
|
|
|
uint64_t MemoryInline::Read64Slow(uint32_t addr) {
|
|
if (IsMmioAddress(addr)) {
|
|
ThrowMmioReadBlocked(addr, sizeof(uint64_t));
|
|
}
|
|
ResolveDeferredReads(addr, sizeof(uint64_t));
|
|
return ReadScalar<uint64_t>(addr);
|
|
}
|
|
|
|
float MemoryInline::ReadFloat32Slow(uint32_t addr) {
|
|
if (IsMmioAddress(addr)) {
|
|
ThrowMmioReadBlocked(addr, sizeof(float));
|
|
}
|
|
ResolveDeferredReads(addr, sizeof(uint32_t));
|
|
const auto bits = ReadScalar<uint32_t>(addr);
|
|
float value;
|
|
std::memcpy(&value, &bits, sizeof(value));
|
|
return value;
|
|
}
|
|
|
|
double MemoryInline::ReadFloat64Slow(uint32_t addr) {
|
|
if (IsMmioAddress(addr)) {
|
|
ThrowMmioReadBlocked(addr, sizeof(double));
|
|
}
|
|
ResolveDeferredReads(addr, sizeof(uint64_t));
|
|
const auto bits = ReadScalar<uint64_t>(addr);
|
|
double value;
|
|
std::memcpy(&value, &bits, sizeof(value));
|
|
return value;
|
|
}
|
|
|
|
void MemoryInline::Write8Slow(uint32_t addr, uint8_t val) {
|
|
if (IsGpuFifoAddress(addr)) {
|
|
GX_HLE_FIFO_Write8(val);
|
|
return;
|
|
}
|
|
if (IsMmioAddress(addr)) {
|
|
throw Memory::AccessViolation(addr, sizeof(val), "MMIO write blocked (non-GPU)");
|
|
}
|
|
WriteScalar(addr, val);
|
|
}
|
|
|
|
void MemoryInline::Write16Slow(uint32_t addr, uint16_t val) {
|
|
if (IsGpuFifoAddress(addr)) {
|
|
GX_HLE_FIFO_Write16(val);
|
|
return;
|
|
}
|
|
if (IsMmioAddress(addr)) {
|
|
throw Memory::AccessViolation(addr, sizeof(val), "MMIO write blocked (non-GPU)");
|
|
}
|
|
WriteScalar(addr, val);
|
|
}
|
|
|
|
void MemoryInline::Write32Slow(uint32_t addr, uint32_t val) {
|
|
if (IsGpuFifoAddress(addr)) {
|
|
GX_HLE_FIFO_Write32(val);
|
|
return;
|
|
}
|
|
if (IsMmioAddress(addr)) {
|
|
throw Memory::AccessViolation(addr, sizeof(val), "MMIO write blocked (non-GPU)");
|
|
}
|
|
WriteScalar(addr, val);
|
|
}
|
|
|
|
void MemoryInline::Write64Slow(uint32_t addr, uint64_t val) {
|
|
if (IsGpuFifoAddress(addr)) {
|
|
// The gather pipe is a byte-stream FIFO; a 64-bit store is two big-endian
|
|
// 32-bit pushes, high word first. Without this arm the write fell through
|
|
// to WriteScalar, which found no mapped region behind 0xCC008000 and threw
|
|
// "no mapped region" instead of reaching GX.
|
|
GX_HLE_FIFO_Write32(static_cast<uint32_t>(val >> 32));
|
|
GX_HLE_FIFO_Write32(static_cast<uint32_t>(val));
|
|
return;
|
|
}
|
|
if (IsMmioAddress(addr)) {
|
|
throw Memory::AccessViolation(addr, sizeof(val), "MMIO write blocked (non-GPU)");
|
|
}
|
|
WriteScalar(addr, val);
|
|
}
|
|
|
|
void MemoryInline::WriteFloat32Slow(uint32_t addr, double val) {
|
|
// stfs stores the IEEE-754 bit pattern of the single-precision value, never a
|
|
// truncated integer.
|
|
const uint32_t bits = ConvertPpcDoubleToSingleBits(val);
|
|
if (IsGpuFifoAddress(addr)) {
|
|
GX_HLE_FIFO_WriteFloat(PpcSingleBitsToFloat(bits));
|
|
return;
|
|
}
|
|
if (IsMmioAddress(addr)) {
|
|
throw Memory::AccessViolation(addr, sizeof(float), "MMIO write blocked (non-GPU)");
|
|
}
|
|
|
|
WriteScalar(addr, bits);
|
|
}
|
|
|
|
void MemoryInline::WriteFloat64Slow(uint32_t addr, double val) {
|
|
// stfd stores the full 64-bit FPR bit pattern - load-bearing for the
|
|
// fctiwz->stfd->lwz idiom, where the integer result lives in the low word.
|
|
uint64_t bits;
|
|
std::memcpy(&bits, &val, sizeof(bits));
|
|
|
|
if (IsGpuFifoAddress(addr)) {
|
|
GX_HLE_FIFO_WriteFloat(static_cast<float>(val));
|
|
return;
|
|
}
|
|
if (IsMmioAddress(addr)) {
|
|
throw Memory::AccessViolation(addr, sizeof(double), "MMIO write blocked (non-GPU)");
|
|
}
|
|
|
|
WriteScalar(addr, bits);
|
|
}
|
|
|
|
uint8_t* Memory::GetPointer(uint32_t addr) {
|
|
return GetPointer(addr, 1);
|
|
}
|
|
|
|
uint8_t* Memory::GetPointer(uint32_t addr, size_t length) {
|
|
if (auto* ptr = MemoryInline::GetPointerFast(addr, length)) {
|
|
return ptr;
|
|
}
|
|
auto& region = ResolveRegion(addr, length);
|
|
auto offset = static_cast<size_t>(addr - region.config.baseAddress);
|
|
return region.storagePtr + offset;
|
|
}
|
|
|
|
bool Memory::Contains(uint32_t addr, size_t length) {
|
|
if (MemoryInline::GetPointerFast(addr, length)) {
|
|
return true;
|
|
}
|
|
// Contains is a membership predicate: callers probe arbitrary guest values
|
|
// (e.g. gfx-node slots that may hold floats), so a miss must stay silent
|
|
// rather than take ResolveRegion's crash-dump path.
|
|
return TryResolveRegion(addr, length) != nullptr;
|
|
}
|
|
|
|
std::vector<Memory::RegionConfig> Memory::DescribeRegions() {
|
|
std::lock_guard<std::mutex> lock(RegionMutex());
|
|
std::vector<RegionConfig> info;
|
|
info.reserve(Regions().size());
|
|
for (const auto& region : Regions()) {
|
|
info.push_back(RegionConfig{region.config.name, region.config.baseAddress, region.storageSize});
|
|
}
|
|
return info;
|
|
}
|