mirror of
https://github.com/shadps4-emu/shadPS4.git
synced 2025-06-25 11:56:18 +00:00
* Bit array test * Some corrections * Fix AVX path on SetRange * Finish bitArray * Batched protect progress * Inclusion fix * Last logic fixes for BitArray * Page manager: batch protect, masked ranges * Page manager bitarray * clang-format * Fix out of bounds read * clang * clang * Lock during callbacks * Rename untracked to writeable * Construct and mask in one step * Sync on region mutex for thw whole protection This is a temporary workarround until a fix is found for the page manager having issues when multiple threads update the same page at the same time. * Bring back the gpu masking until properly handled * Sync page manager protections * clang-format * Rename and fixups * I fucked up clang-formatting one more time... * kek
345 lines
12 KiB
C++
345 lines
12 KiB
C++
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#include <boost/container/small_vector.hpp>
|
|
#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 <sys/mman.h>
|
|
#ifdef ENABLE_USERFAULTFD
|
|
#include <thread>
|
|
#include <fcntl.h>
|
|
#include <linux/userfaultfd.h>
|
|
#include <poll.h>
|
|
#include <sys/ioctl.h>
|
|
#include "common/error.h"
|
|
#endif
|
|
#else
|
|
#include <windows.h>
|
|
#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 <s32 delta>
|
|
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<u32>::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<VAddr>(fault_address);
|
|
if (Common::IsWriteError(context)) {
|
|
return rasterizer->InvalidateMemory(addr, 1);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
#endif
|
|
template <bool track>
|
|
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<track ? 1 : -1>();
|
|
|
|
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 <bool track>
|
|
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<track>(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<track ? 1 : -1>() : 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<PageState, NUM_ADDRESS_PAGES> cached_pages{};
|
|
#ifdef __linux__
|
|
Common::AdaptiveMutex lock;
|
|
#else
|
|
Common::SpinLock lock;
|
|
#endif
|
|
};
|
|
|
|
PageManager::PageManager(Vulkan::Rasterizer* rasterizer_)
|
|
: impl{std::make_unique<Impl>(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 <bool track>
|
|
void PageManager::UpdatePageWatchers(VAddr addr, u64 size) const {
|
|
impl->UpdatePageWatchers<track>(addr, size);
|
|
}
|
|
|
|
template <bool track>
|
|
void PageManager::UpdatePageWatchersForRegion(VAddr base_addr, RegionBits& mask) const {
|
|
impl->UpdatePageWatchersForRegion<track>(base_addr, mask);
|
|
}
|
|
|
|
template void PageManager::UpdatePageWatchers<true>(VAddr addr, u64 size) const;
|
|
template void PageManager::UpdatePageWatchers<false>(VAddr addr, u64 size) const;
|
|
template void PageManager::UpdatePageWatchersForRegion<true>(VAddr base_addr,
|
|
RegionBits& mask) const;
|
|
template void PageManager::UpdatePageWatchersForRegion<false>(VAddr base_addr,
|
|
RegionBits& mask) const;
|
|
|
|
} // namespace VideoCore
|