#include "graphics/host_gpu/pageManager.h" #include "common/platform/sysDbg.h" #include "graphics/host_gpu/regionDefinitions.h" #include "kernel/memory.h" #include #include #include #include #include #include #include #include #include #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS #ifndef NOMINMAX #define NOMINMAX #endif #include #undef min #undef max #elif defined(__APPLE__) #include #else #include #include #endif namespace Libs::Graphics { namespace { constexpr uint64_t PAGE_SIZE = TRACKER_PAGE_SIZE; constexpr uint64_t REGION_SIZE = TRACKER_REGION_SIZE; constexpr uint64_t ADDRESS_SIZE = TRACKER_ADDRESS_SIZE; constexpr uint64_t REGION_COUNT = ADDRESS_SIZE / REGION_SIZE; #if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS // The tracker reuses Win32 memory-protection tags as internal page-state values. // Mirror their canonical numeric values so the shared state-machine logic is identical. constexpr uint32_t PAGE_NOACCESS = 0x01; constexpr uint32_t PAGE_READONLY = 0x02; constexpr uint32_t PAGE_READWRITE = 0x04; #endif constexpr uint64_t REGION_PAGES = REGION_SIZE / PAGE_SIZE; constexpr uint32_t NO_ACCESS_PROTECTION = PAGE_NOACCESS; constexpr uint32_t READ_ONLY_PROTECTION = PAGE_READONLY; constexpr uint32_t READ_WRITE_PROTECTION = PAGE_READWRITE; [[noreturn]] void FailFast(const char* reason = nullptr) noexcept { std::fputs("PageManager fail-fast: ", stderr); std::fputs(reason != nullptr ? reason : "invalid page state", stderr); std::fputc('\n', stderr); #if !defined(__APPLE__) void* frames[16] {}; int frame_count = static_cast(std::size(frames)); SysStackWalk(frames, &frame_count); #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS const auto image_base = reinterpret_cast(GetModuleHandleW(nullptr)); for (int i = 0; i < frame_count; i++) { const auto address = reinterpret_cast(frames[i]); std::fprintf(stderr, " frame[%d]=0x%016" PRIxPTR " image_rva=0x%016" PRIxPTR "\n", i, address, address >= image_base ? address - image_base : 0); } #else ::backtrace_symbols_fd(frames, frame_count, STDERR_FILENO); #endif #endif std::fflush(stderr); #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS TerminateProcess(GetCurrentProcess(), static_cast(EXCEPTION_NONCONTINUABLE_EXCEPTION)); #endif std::_Exit(322); } [[noreturn]] void Fatal(const char* format, ...) { std::fputs("PageManager fatal: ", stderr); va_list args; va_start(args, format); std::vfprintf(stderr, format, args); va_end(args); std::fputc('\n', stderr); std::fflush(stderr); std::_Exit(322); } Common::VirtualMemory::Mode ToMemoryMode(uint32_t protection) { switch (protection) { case NO_ACCESS_PROTECTION: return Common::VirtualMemory::Mode::NoAccess; case READ_ONLY_PROTECTION: return Common::VirtualMemory::Mode::Read; case READ_WRITE_PROTECTION: return Common::VirtualMemory::Mode::ReadWrite; default: Fatal("unmappable protection 0x%08" PRIx32, protection); } } class SpinGuard final { public: explicit SpinGuard(std::atomic_flag& lock): m_lock(lock) { while (m_lock.test_and_set(std::memory_order_acquire)) { std::atomic_signal_fence(std::memory_order_seq_cst); } } ~SpinGuard() { m_lock.clear(std::memory_order_release); } KYTY_CLASS_NO_COPY(SpinGuard); private: std::atomic_flag& m_lock; }; void ValidateRange(uint64_t vaddr, uint64_t size) { if (vaddr == 0 || size == 0 || vaddr >= ADDRESS_SIZE || size > ADDRESS_SIZE - vaddr) { Fatal("invalid range vaddr=0x%016" PRIx64 ", size=0x%016" PRIx64, vaddr, size); } } uint64_t PageStart(uint64_t vaddr) { return vaddr & ~(PAGE_SIZE - 1); } uint64_t PageEnd(uint64_t vaddr, uint64_t size) { ValidateRange(vaddr, size); return PageStart(vaddr + size - 1) + PAGE_SIZE; } } // namespace struct PageManager::Impl { struct PageState { uint8_t write_watchers : 7 = 0; uint8_t access_watchers : 1 = 0; [[nodiscard]] uint32_t Perms() const noexcept { if (access_watchers != 0) { return NO_ACCESS_PROTECTION; } if (write_watchers != 0) { return READ_ONLY_PROTECTION; } return READ_WRITE_PROTECTION; } template uint32_t AddDelta(uint64_t address) { static_assert(delta >= -1 && delta <= 1); if constexpr (is_read) { if constexpr (delta == 1) { if (access_watchers != 0) { Fatal("read-watcher overflow at 0x%016" PRIx64, address); } return ++access_watchers; } else if constexpr (delta == -1) { if (access_watchers == 0) { Fatal("read-watcher underflow at 0x%016" PRIx64, address); } return --access_watchers; } else { return access_watchers; } } else { if constexpr (delta == 1) { if (write_watchers == 0x7f) { Fatal("write-watcher overflow at 0x%016" PRIx64, address); } return ++write_watchers; } else if constexpr (delta == -1) { if (write_watchers == 0) { Fatal("write-watcher underflow at 0x%016" PRIx64, address); } return --write_watchers; } else { return write_watchers; } } } }; static_assert(sizeof(PageState) == 1); struct Region { std::atomic_flag lock = ATOMIC_FLAG_INIT; std::array pages; }; Impl() { #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS SYSTEM_INFO info {}; GetSystemInfo(&info); if (info.dwPageSize != PAGE_SIZE) { Fatal("unsupported host page size 0x%08" PRIx32, static_cast(info.dwPageSize)); } #elif defined(__APPLE__) // Under Rosetta the host page size is 4 KB, matching TRACKER_PAGE_SIZE. if (static_cast(getpagesize()) != PAGE_SIZE) { Fatal("unsupported host page size 0x%08" PRIx32, static_cast(getpagesize())); } #else const auto host_page_size = ::sysconf(_SC_PAGESIZE); if (host_page_size < 0 || static_cast(host_page_size) != PAGE_SIZE) { Fatal("unsupported host page size %ld", static_cast(host_page_size)); } #endif regions = std::make_unique[]>(REGION_COUNT); for (uint64_t i = 0; i < REGION_COUNT; i++) { regions[i].store(nullptr, std::memory_order_relaxed); } } ~Impl() { for (const auto& region: region_storage) { SpinGuard lock(region->lock); for (auto& page: region->pages) { if (page.write_watchers != 0 || page.access_watchers != 0) { FailFast("PageManager destroyed with live page state"); } } } } Region* FindRegion(uint64_t vaddr) const noexcept { return vaddr < ADDRESS_SIZE ? regions[vaddr / REGION_SIZE].load(std::memory_order_acquire) : nullptr; } Region* GetOrCreateRegion(uint64_t vaddr) { const auto index = vaddr / REGION_SIZE; if (auto* region = regions[index].load(std::memory_order_acquire); region != nullptr) { return region; } std::lock_guard lock(region_mutex); if (auto* region = regions[index].load(std::memory_order_acquire); region != nullptr) { return region; } auto region = std::make_unique(); auto* ptr = region.get(); region_storage.push_back(std::move(region)); regions[index].store(ptr, std::memory_order_release); return ptr; } void Protect(uint64_t vaddr, uint64_t size, uint32_t protection) noexcept { if (!Libs::LibKernel::Memory::ProtectGuestHostMemory(vaddr, size, ToMemoryMode(protection))) { Fatal("address-space protection failed at 0x%016" PRIx64 ", new=0x%08" PRIx32, vaddr, protection); } } template void UpdateRegionWatchers(Region& region, uint64_t base_addr, size_t first, size_t last, const RegionBits* mask = nullptr) { SpinGuard lock(region.lock); auto perms = region.pages[first].Perms(); uint64_t range_begin = 0; uint64_t range_bytes = 0; uint64_t potential_range_bytes = 0; const auto release_pending = [&] { if (range_bytes != 0) { Protect(base_addr + range_begin * PAGE_SIZE, range_bytes, perms); range_bytes = 0; potential_range_bytes = 0; } }; for (size_t page_index = first; page_index < last; page_index++) { auto& page = region.pages[page_index]; const auto address = base_addr + page_index * PAGE_SIZE; const bool update = !masked || mask->Get(page_index); const auto old_perms = page.Perms(); const auto new_count = update ? page.AddDelta(address) : page.AddDelta<0, is_read>(address); const auto new_perms = page.Perms(); if (new_perms != perms) [[unlikely]] { release_pending(); perms = new_perms; } else if (range_bytes != 0) { potential_range_bytes += PAGE_SIZE; } if (!update) { continue; } const bool watcher_edge = (track && new_count == 1) || (!track && new_count == 0); if (watcher_edge && old_perms != new_perms) { if (range_bytes == 0) { range_begin = page_index; potential_range_bytes = PAGE_SIZE; } range_bytes = potential_range_bytes; } } release_pending(); } template void UpdatePageWatchers(uint64_t vaddr, uint64_t size) { const auto begin = PageStart(vaddr); const auto end = PageEnd(vaddr, size); for (auto chunk_begin = begin; chunk_begin < end;) { const auto chunk_end = std::min(end, (chunk_begin / REGION_SIZE + 1) * REGION_SIZE); const auto region_base = chunk_begin / REGION_SIZE * REGION_SIZE; auto* region = track ? GetOrCreateRegion(chunk_begin) : FindRegion(chunk_begin); if (region == nullptr) { Fatal("untracking unknown page 0x%016" PRIx64, chunk_begin); } const auto first = static_cast((chunk_begin - region_base) / PAGE_SIZE); const auto last = static_cast((chunk_end - region_base) / PAGE_SIZE); UpdateRegionWatchers(*region, region_base, first, last); chunk_begin = chunk_end; } } std::unique_ptr[]> regions; std::vector> region_storage; std::mutex region_mutex; }; static_assert(std::atomic::is_always_lock_free); PageManager::PageManager(): m_impl(std::make_unique()) {} PageManager::~PageManager() = default; uint64_t PageManager::GetPageSize() const { return PAGE_SIZE; } template void PageManager::UpdatePageWatchers(uint64_t vaddr, uint64_t size) { m_impl->UpdatePageWatchers(vaddr, size); } template void PageManager::UpdatePageWatchers(uint64_t, uint64_t); template void PageManager::UpdatePageWatchers(uint64_t, uint64_t); template void PageManager::UpdatePageWatchersForRegion(uint64_t base_addr, RegionBits& mask) { if (base_addr % REGION_SIZE != 0 || base_addr >= ADDRESS_SIZE || REGION_SIZE > ADDRESS_SIZE - base_addr) { Fatal("invalid tracking region base 0x%016" PRIx64, base_addr); } const auto start_range = mask.FirstRange(); const auto end_range = mask.LastRange(); if (start_range.first == REGION_PAGES) { FailFast("empty region watcher mask"); } const auto first = start_range.first; const auto last = end_range.second; if (start_range.second == end_range.second) { m_impl->UpdatePageWatchers(base_addr + first * PAGE_SIZE, (last - first) * PAGE_SIZE); return; } auto* region = track ? m_impl->GetOrCreateRegion(base_addr) : m_impl->FindRegion(base_addr); if (region == nullptr) { Fatal("untracking unknown region 0x%016" PRIx64, base_addr); } m_impl->UpdateRegionWatchers(*region, base_addr, first, last, &mask); } template void PageManager::UpdatePageWatchersForRegion(uint64_t, RegionBits&); template void PageManager::UpdatePageWatchersForRegion(uint64_t, RegionBits&); template void PageManager::UpdatePageWatchersForRegion(uint64_t, RegionBits&); template void PageManager::UpdatePageWatchersForRegion(uint64_t, RegionBits&); void PageManager::OnGpuMap(uint64_t, uint64_t) {} void PageManager::OnGpuUnmap(uint64_t, uint64_t) {} } // namespace Libs::Graphics