#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 #include #include #include // 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(Memory::kMem2PhysicalEnd); inline constexpr size_t kGranuleCount = static_cast(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, 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(CanonicalizeGxMainRamAddress(addr)); const uint64_t end = start + static_cast(nbytes); if (end <= start || end > kTrackedSpan) { return kUntracked; } const size_t firstGranule = static_cast(start >> kGranuleShift); const size_t lastGranule = static_cast((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(CanonicalizeGxMainRamAddress(addr)); const uint64_t end = start + static_cast(size); if (end <= start || start >= kTrackedSpan) { return; } const uint64_t clampedEnd = end < kTrackedSpan ? end : kTrackedSpan; const size_t firstGranule = static_cast(start >> kGranuleShift); const size_t lastGranule = static_cast((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(hostBase); const uintptr_t ptr = reinterpret_cast(hostPtr); if (ptr < base) { return false; } const uint64_t offset = static_cast(ptr - base); if (offset >= hostSize || static_cast(size) > hostSize - offset) { return false; } outAddr = guestBase + static_cast(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