#include "memory.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ppc_runtime.h" #include "runtime_log.h" #include "system_bridge.h" #include #include #include #if defined(_WIN32) #ifndef NOMINMAX #define NOMINMAX #endif #include #include #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 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(uint32_t); template uint16_t MemoryInline::ReadResolvedFallback(uint32_t); template uint32_t MemoryInline::ReadResolvedFallback(uint32_t); template uint64_t MemoryInline::ReadResolvedFallback(uint32_t); float MemoryInline::ReadResolvedFallbackFloat32(uint32_t addr) { return Memory::ReadFloat32(addr); } double MemoryInline::ReadResolvedFallbackFloat64(uint32_t addr) { return Memory::ReadFloat64(addr); } template 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(uint32_t, uint8_t); template void MemoryInline::WriteResolvedFallback(uint32_t, uint16_t); template void MemoryInline::WriteResolvedFallback(uint32_t, uint32_t); template void MemoryInline::WriteResolvedFallback(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& DeferredReads() { static std::vector 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(firstStart) + firstLength; const uint64_t secondEnd = static_cast(secondStart) + secondLength; return static_cast(firstStart) < secondEnd && static_cast(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& Regions() { static std::vector regions; return regions; } std::mutex& RegionMutex() { static std::mutex mutex; return mutex; } std::array, MemoryInline::kPageCount>& SparseWritablePageTableStorage() { static std::array, 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(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( (static_cast(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 pages; for (const auto& read : reads) { const uint32_t firstPage = read.start >> MemoryInline::kPageShift; const uint32_t lastPage = static_cast( (static_cast(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 matches; std::vector 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( (static_cast(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(base >> MemoryInline::kPageShift); const uint32_t endPage = static_cast((end - 1) >> MemoryInline::kPageShift); for (uint32_t page = startPage; page <= endPage; ++page) { const uint64_t pageBase = static_cast(page) << MemoryInline::kPageShift; const uint64_t offset = pageBase - base; if (offset >= size) { continue; } const uint32_t limit = static_cast(std::min(MemoryInline::kPageSize, size - offset)); table[page].base = region.storagePtr + static_cast(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(page) << MemoryInline::kPageShift; const uintptr_t bias = reinterpret_cast(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(); 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( (static_cast(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(byte); ascii[i] = (byte >= 0x20 && byte < 0x7F) ? static_cast(byte) : '.'; } std::cerr << " |" << ascii << "|" << std::dec << std::setfill(' ') << std::endl; } } } #if defined(_WIN32) void* frames[32]{}; const USHORT captured = CaptureStackBackTrace(0, static_cast(std::size(frames)), frames, nullptr); const auto imageBase = reinterpret_cast(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(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 symbolBuffer{}; auto* symbol = reinterpret_cast(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 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 void WriteScalar(uint32_t address, T value) { if (RecompMod::HandleExecutableWrite(address, sizeof(T), static_cast(value))) { return; } auto* ptr = Memory::GetPointer(address, sizeof(T)); if constexpr (sizeof(T) == 1) { *ptr = static_cast(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 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 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 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 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(addr); } uint16_t MemoryInline::Read16Slow(uint32_t addr) { if (IsMmioAddress(addr)) { ThrowMmioReadBlocked(addr, sizeof(uint16_t)); } ResolveDeferredReads(addr, sizeof(uint16_t)); return ReadScalar(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(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(addr); } float MemoryInline::ReadFloat32Slow(uint32_t addr) { if (IsMmioAddress(addr)) { ThrowMmioReadBlocked(addr, sizeof(float)); } ResolveDeferredReads(addr, sizeof(uint32_t)); const auto bits = ReadScalar(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(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(val >> 32)); GX_HLE_FIFO_Write32(static_cast(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(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(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::DescribeRegions() { std::lock_guard lock(RegionMutex()); std::vector info; info.reserve(Regions().size()); for (const auto& region : Regions()) { info.push_back(RegionConfig{region.config.name, region.config.baseAddress, region.storageSize}); } return info; }