// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include "common/assert.h" #include "common/debug.h" #include "common/signal_context.h" #include "core/memory.h" #include "core/signals.h" #include "video_core/page_manager.h" #include "video_core/renderer_vulkan/vk_rasterizer.h" #ifndef _WIN64 #include #ifdef ENABLE_USERFAULTFD #include #include #include #include #include #include "common/error.h" #endif #else #include #endif #ifdef __linux__ #include "common/adaptive_mutex.h" #else #include "common/spin_lock.h" #endif namespace VideoCore { constexpr size_t PAGE_SIZE = 4_KB; constexpr size_t PAGE_BITS = 12; struct PageManager::Impl { struct PageState { u8 num_watchers{}; Core::MemoryPermission Perm() const noexcept { return num_watchers == 0 ? Core::MemoryPermission::ReadWrite : Core::MemoryPermission::Read; } template u8 AddDelta() { if constexpr (delta == 1) { return ++num_watchers; } else if constexpr (delta == -1) { ASSERT_MSG(num_watchers > 0, "Not enough watchers"); return --num_watchers; } else { return num_watchers; } } }; static constexpr size_t ADDRESS_BITS = 40; static constexpr size_t NUM_ADDRESS_PAGES = 1ULL << (40 - PAGE_BITS); inline static Vulkan::Rasterizer* rasterizer; #ifdef ENABLE_USERFAULTFD Impl(Vulkan::Rasterizer* rasterizer_) { rasterizer = rasterizer_; uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK | UFFD_USER_MODE_ONLY); ASSERT_MSG(uffd != -1, "{}", Common::GetLastErrorMsg()); // Request uffdio features from kernel. uffdio_api api; api.api = UFFD_API; api.features = UFFD_FEATURE_THREAD_ID; const int ret = ioctl(uffd, UFFDIO_API, &api); ASSERT(ret == 0 && api.api == UFFD_API); // Create uffd handler thread ufd_thread = std::jthread([&](std::stop_token token) { UffdHandler(token); }); } void OnMap(VAddr address, size_t size) { uffdio_register reg; reg.range.start = address; reg.range.len = size; reg.mode = UFFDIO_REGISTER_MODE_WP; const int ret = ioctl(uffd, UFFDIO_REGISTER, ®); ASSERT_MSG(ret != -1, "Uffdio register failed"); } void OnUnmap(VAddr address, size_t size) { uffdio_range range; range.start = address; range.len = size; const int ret = ioctl(uffd, UFFDIO_UNREGISTER, &range); ASSERT_MSG(ret != -1, "Uffdio unregister failed"); } void Protect(VAddr address, size_t size, Core::MemoryPermission perms) { bool allow_write = True(perms & Core::MemoryPermission::Write); uffdio_writeprotect wp; wp.range.start = address; wp.range.len = size; wp.mode = allow_write ? 0 : UFFDIO_WRITEPROTECT_MODE_WP; const int ret = ioctl(uffd, UFFDIO_WRITEPROTECT, &wp); ASSERT_MSG(ret != -1, "Uffdio writeprotect failed with error: {}", Common::GetLastErrorMsg()); } void UffdHandler(std::stop_token token) { while (!token.stop_requested()) { pollfd pollfd; pollfd.fd = uffd; pollfd.events = POLLIN; // Block until the descriptor is ready for data reads. const int pollres = poll(&pollfd, 1, -1); switch (pollres) { case -1: perror("Poll userfaultfd"); continue; break; case 0: continue; case 1: break; default: UNREACHABLE_MSG("Unexpected number of descriptors {} out of poll", pollres); } // We don't want an error condition to have occured. ASSERT_MSG(!(pollfd.revents & POLLERR), "POLLERR on userfaultfd"); // We waited until there is data to read, we don't care about anything else. if (!(pollfd.revents & POLLIN)) { continue; } // Read message from kernel. uffd_msg msg; const int readret = read(uffd, &msg, sizeof(msg)); ASSERT_MSG(readret != -1 || errno == EAGAIN, "Unexpected result of uffd read"); if (errno == EAGAIN) { continue; } ASSERT_MSG(readret == sizeof(msg), "Unexpected short read, exiting"); ASSERT(msg.arg.pagefault.flags & UFFD_PAGEFAULT_FLAG_WP); // Notify rasterizer about the fault. const VAddr addr = msg.arg.pagefault.address; rasterizer->InvalidateMemory(addr, 1); } } std::jthread ufd_thread; int uffd; #else Impl(Vulkan::Rasterizer* rasterizer_) { rasterizer = rasterizer_; // Should be called first. constexpr auto priority = std::numeric_limits::min(); Core::Signals::Instance()->RegisterAccessViolationHandler(GuestFaultSignalHandler, priority); } void OnMap(VAddr address, size_t size) { // No-op } void OnUnmap(VAddr address, size_t size) { // No-op } void Protect(VAddr address, size_t size, Core::MemoryPermission perms) { RENDERER_TRACE; auto* memory = Core::Memory::Instance(); auto& impl = memory->GetAddressSpace(); impl.Protect(address, size, perms); } static bool GuestFaultSignalHandler(void* context, void* fault_address) { const auto addr = reinterpret_cast(fault_address); if (Common::IsWriteError(context)) { return rasterizer->InvalidateMemory(addr, 1); } return false; } #endif template void UpdatePageWatchers(VAddr addr, u64 size) { RENDERER_TRACE; size_t page = addr >> PAGE_BITS; auto perms = cached_pages[page].Perm(); u64 range_begin = 0; u64 range_bytes = 0; const auto release_pending = [&] { if (range_bytes > 0) { RENDERER_TRACE; // Perform pending (un)protect action Protect(range_begin << PAGE_BITS, range_bytes, perms); range_bytes = 0; } }; std::scoped_lock lk(lock); // Iterate requested pages const u64 page_end = Common::DivCeil(addr + size, PAGE_SIZE); const u64 aligned_addr = page << PAGE_BITS; const u64 aligned_end = page_end << PAGE_BITS; ASSERT_MSG(rasterizer->IsMapped(aligned_addr, aligned_end - aligned_addr), "Attempted to track non-GPU memory at address {:#x}, size {:#x}.", aligned_addr, aligned_end - aligned_addr); for (; page != page_end; ++page) { PageState& state = cached_pages[page]; // Apply the change to the page state const u8 new_count = state.AddDelta(); if (auto new_perms = state.Perm(); new_perms != perms) [[unlikely]] { // If the protection changed add pending (un)protect action release_pending(); perms = new_perms; } else if (range_bytes != 0) { // If the protection did not change, extend the current range range_bytes += PAGE_SIZE; } // Only start a new range if the page must be (un)protected if (range_bytes == 0 && ((new_count == 0 && !track) || (new_count == 1 && track))) { range_begin = page; range_bytes = PAGE_SIZE; } } // Add pending (un)protect action release_pending(); } template void UpdatePageWatchersForRegion(VAddr base_addr, RegionBits& mask) { RENDERER_TRACE; auto start_range = mask.FirstRange(); auto end_range = mask.LastRange(); if (start_range.second == end_range.second) { // Optimization: if all pages are contiguous, use the regular UpdatePageWatchers const VAddr start_addr = base_addr + (start_range.first << PAGE_BITS); const u64 size = (start_range.second - start_range.first) << PAGE_BITS; UpdatePageWatchers(start_addr, size); return; } size_t base_page = (base_addr >> PAGE_BITS); auto perms = cached_pages[base_page + start_range.first].Perm(); u64 range_begin = 0; u64 range_bytes = 0; const auto release_pending = [&] { if (range_bytes > 0) { RENDERER_TRACE; // Perform pending (un)protect action Protect((range_begin << PAGE_BITS), range_bytes, perms); range_bytes = 0; } }; std::scoped_lock lk(lock); // Iterate pages for (size_t page = start_range.first; page < end_range.second; ++page) { PageState& state = cached_pages[base_page + page]; const bool update = mask.Get(page); // Apply the change to the page state const u8 new_count = update ? state.AddDelta() : state.AddDelta<0>(); if (auto new_perms = state.Perm(); new_perms != perms) [[unlikely]] { // If the protection changed add pending (un)protect action release_pending(); perms = new_perms; } else if (range_bytes != 0) { // If the protection did not change, extend the current range range_bytes += PAGE_SIZE; } // If the page is not being updated, skip it if (!update) { continue; } // Only start a new range if the page must be (un)protected if (range_bytes == 0 && ((new_count == 0 && !track) || (new_count == 1 && track))) { range_begin = base_page + page; range_bytes = PAGE_SIZE; } } // Add pending (un)protect action release_pending(); } std::array cached_pages{}; #ifdef __linux__ Common::AdaptiveMutex lock; #else Common::SpinLock lock; #endif }; PageManager::PageManager(Vulkan::Rasterizer* rasterizer_) : impl{std::make_unique(rasterizer_)} {} PageManager::~PageManager() = default; void PageManager::OnGpuMap(VAddr address, size_t size) { impl->OnMap(address, size); } void PageManager::OnGpuUnmap(VAddr address, size_t size) { impl->OnUnmap(address, size); } template void PageManager::UpdatePageWatchers(VAddr addr, u64 size) const { impl->UpdatePageWatchers(addr, size); } template void PageManager::UpdatePageWatchersForRegion(VAddr base_addr, RegionBits& mask) const { impl->UpdatePageWatchersForRegion(base_addr, mask); } template void PageManager::UpdatePageWatchers(VAddr addr, u64 size) const; template void PageManager::UpdatePageWatchers(VAddr addr, u64 size) const; template void PageManager::UpdatePageWatchersForRegion(VAddr base_addr, RegionBits& mask) const; template void PageManager::UpdatePageWatchersForRegion(VAddr base_addr, RegionBits& mask) const; } // namespace VideoCore