Browse Source
[common/core/dynarmic] Optimize page table allocations (#4219)
[common/core/dynarmic] Optimize page table allocations (#4219)
Reduces the page entries from 32 bytes to 8 and rewrites `VirtualBuffer` to be more efficient in memory usage and specifically for large zero regions. The page table will now only reserve 1GiB instead of 4GiB and of this memory it should only use at most ~8MiB. This PR has the side effect of using Eden on Windows on low memory systems much more plausible since it would previously require ~10GiB of committable memory at front (despite using ~5-6 at most, inadvertently stalling other processes) where as now it should only require around the amount that it'll actually use. Co-authored-by: Lizzie <lizzie@eden-emu.dev> Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4219 Reviewed-by: Maufeat <sahyno1996@gmail.com> Reviewed-by: lizzie <lizzie@eden-emu.dev> Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>pull/4181/head^2
committed by
crueter
No known key found for this signature in database
GPG Key ID: 425ACD2D4830EBC6
27 changed files with 727 additions and 390 deletions
-
4src/common/CMakeLists.txt
-
1src/common/fiber.cpp
-
52src/common/host_memory.cpp
-
3src/common/host_memory.h
-
38src/common/page_table.cpp
-
120src/common/page_table.h
-
143src/common/sparse_large_vector.cpp
-
194src/common/sparse_large_vector.h
-
44src/common/virtual_buffer.cpp
-
84src/common/virtual_buffer.h
-
18src/core/arm/dynarmic/arm_dynarmic_32.cpp
-
19src/core/arm/dynarmic/arm_dynarmic_64.cpp
-
10src/core/device_memory.h
-
10src/core/device_memory_manager.h
-
49src/core/device_memory_manager.inc
-
110src/core/hle/kernel/k_page_table_base.cpp
-
13src/core/hle/kernel/k_page_table_base.h
-
79src/core/memory.cpp
-
4src/dynarmic/src/dynarmic/backend/arm64/a32_address_space.cpp
-
4src/dynarmic/src/dynarmic/backend/arm64/a64_address_space.cpp
-
4src/dynarmic/src/dynarmic/backend/arm64/emit_arm64.h
-
15src/dynarmic/src/dynarmic/backend/arm64/emit_arm64_memory.cpp
-
53src/dynarmic/src/dynarmic/backend/x64/emit_x64_memory.h
-
19src/dynarmic/src/dynarmic/interface/A32/config.h
-
19src/dynarmic/src/dynarmic/interface/A64/config.h
-
4src/video_core/memory_manager.cpp
-
4src/video_core/memory_manager.h
@ -0,0 +1,143 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|||
|
|||
/* virtual_buffer.cpp */ |
|||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|||
|
|||
#ifdef _WIN32
|
|||
#include <windows.h>
|
|||
#include <mutex>
|
|||
#else
|
|||
#include <sys/mman.h>
|
|||
#endif
|
|||
|
|||
#include "common/alignment.h"
|
|||
#include "common/assert.h"
|
|||
#include "common/sparse_large_vector.h"
|
|||
|
|||
namespace Common { |
|||
|
|||
#ifdef _WIN32
|
|||
static std::vector<std::pair<u64, u64>> vector_regions {}; |
|||
|
|||
// Workaround for handling non-commited memory accessed by Dynarmic; usually result of an error
|
|||
static LONG WINAPI FakePageFaultHandler(PEXCEPTION_POINTERS info) { |
|||
DWORD code = info->ExceptionRecord->ExceptionCode; |
|||
u64 exception_addr = reinterpret_cast<u64>(info->ExceptionRecord->ExceptionAddress); |
|||
|
|||
if (code != EXCEPTION_ACCESS_VIOLATION) { |
|||
// Not our problem
|
|||
return EXCEPTION_CONTINUE_SEARCH; |
|||
} |
|||
|
|||
u64 addr = 0, addr2 = 0; |
|||
|
|||
for (auto region: vector_regions) { |
|||
auto addr_shifted = exception_addr >> HostPageBits; |
|||
if (region.first <= addr_shifted && addr_shifted <= region.second) { |
|||
addr = addr_shifted; |
|||
} |
|||
|
|||
// Page-boundary accesses
|
|||
if (auto addr_ = (exception_addr + 0x40) >> HostPageBits; addr_ != addr_shifted && region.first <= addr_ && addr_ <= region.second) { |
|||
addr2 = addr_; |
|||
} |
|||
|
|||
if (addr != 0 || addr2 != 0) { |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (addr == 0 && addr2 == 0) { |
|||
// Not our problem
|
|||
return EXCEPTION_CONTINUE_SEARCH; |
|||
} |
|||
|
|||
LOG_ERROR(HW_Memory, "Accessing an unallocated region of a SparseLargeVector at {:#x}; this shouldn't happen and is likely a Dynarmic error!", exception_addr); |
|||
|
|||
// Commit this region
|
|||
if (addr != 0) { |
|||
if (!CommitVectorPage(addr << HostPageBits, false)) { |
|||
return EXCEPTION_CONTINUE_SEARCH; |
|||
} |
|||
} |
|||
// Commit next region if needed
|
|||
if (addr2 != 0) { |
|||
if (!CommitVectorPage(addr2 << HostPageBits, false)) { |
|||
return EXCEPTION_CONTINUE_SEARCH; |
|||
} |
|||
} |
|||
|
|||
return EXCEPTION_CONTINUE_EXECUTION; |
|||
} |
|||
|
|||
bool CommitVectorPage(uintptr_t addr, bool write) noexcept { |
|||
MEMORY_BASIC_INFORMATION info {}; |
|||
auto res = VirtualQuery(reinterpret_cast<void*>(addr), &info, sizeof(info)); |
|||
if (res == 0) { |
|||
LOG_CRITICAL(HW_Memory, "Failed to query large buffer region at {:#x} with error {}, will try committing anyway", addr, GetLastError()); |
|||
} else if (info.State != MEM_RESERVE) { |
|||
LOG_ERROR(HW_Memory, "Tried to commit an unreserved large buffer region at {:#x} that is not mapped or is already committed (state {:#x})", addr, info.State); |
|||
return false; |
|||
} |
|||
|
|||
auto perm = write ? PAGE_READWRITE : PAGE_READONLY; |
|||
void* res2 = VirtualAlloc(reinterpret_cast<LPVOID>(addr), HostPageSize, MEM_COMMIT, perm); |
|||
if (res2 == nullptr) { |
|||
LOG_ERROR(HW_Memory, "Failed to commit large buffer region at {:#x}, error {}", addr, GetLastError()); |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
#endif
|
|||
|
|||
#ifndef MAP_NOCORE
|
|||
#define MAP_NOCORE 0
|
|||
#endif
|
|||
|
|||
void* AllocateMemoryPages(std::size_t size) noexcept { |
|||
if (auto page = HostPageSize; size % page != 0) { |
|||
LOG_WARNING(HW_Memory, "Allocating unaligned large vector with size {:#x}; aligning to {} page size", size, page); |
|||
size = AlignUp(size, page); |
|||
} |
|||
|
|||
#ifdef _WIN32
|
|||
// We will never use this memory entirely so instead of committing it up front let's just reserve it and commit each page individually
|
|||
void* base = VirtualAlloc(nullptr, size, MEM_RESERVE, PAGE_READWRITE); |
|||
|
|||
if (base != nullptr) { |
|||
vector_regions.emplace_back(reinterpret_cast<u64>(base), reinterpret_cast<u64>(base) + size); |
|||
|
|||
static std::once_flag flag; |
|||
std::call_once(flag, []() { AddVectoredExceptionHandler(1, FakePageFaultHandler); }); |
|||
} else { |
|||
// Try committing everything instead??
|
|||
LOG_WARNING(HW_Memory, "Failed to reserve large vector region with error {}, trying to commit instead..", GetLastError()); |
|||
base = VirtualAlloc(nullptr, size, MEM_COMMIT, PAGE_READWRITE); |
|||
} |
|||
ASSERT_MSG(base, "Failed to reserve {:#x} sized region with error {}", size, GetLastError()); |
|||
#else
|
|||
void* base = mmap(nullptr, size, PROT_READ, MAP_ANON | MAP_PRIVATE | MAP_NOCORE, -1, 0); |
|||
if (base == MAP_FAILED) |
|||
base = nullptr; |
|||
ASSERT_MSG(base, "Failed to allocate {:#x} sized region with error {}", size, strerror(errno)); |
|||
#endif
|
|||
return base; |
|||
} |
|||
|
|||
void FreeMemoryPages(void* base, [[maybe_unused]] std::size_t size) noexcept { |
|||
if (auto page = HostPageSize; size % page != 0) { |
|||
size = AlignUp(size, page); |
|||
} |
|||
if (!base) |
|||
return; |
|||
#ifdef _WIN32
|
|||
ASSERT(VirtualFree(base, 0, MEM_RELEASE)); |
|||
#else
|
|||
ASSERT(munmap(base, size) == 0); |
|||
#endif
|
|||
} |
|||
|
|||
} // namespace Common
|
|||
@ -0,0 +1,194 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
/* virtual_buffer.h */ |
|||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project |
|||
// SPDX-License-Identifier: GPL-2.0-or-later |
|||
|
|||
#pragma once |
|||
|
|||
#include <atomic> |
|||
#include <bit> |
|||
#include <utility> |
|||
#include <vector> |
|||
|
|||
#ifndef _WIN32 |
|||
#include <unistd.h> |
|||
#include <sys/mman.h> |
|||
#endif |
|||
|
|||
#include "common/alignment.h" |
|||
#include "common/assert.h" |
|||
|
|||
namespace Common { |
|||
|
|||
#ifdef _WIN32 |
|||
constexpr u64 HostPageSize = 0x1000; |
|||
constexpr u64 HostPageBits = 12; |
|||
constexpr u64 HostPageMask = ~(HostPageSize - 1); |
|||
bool CommitVectorPage(uintptr_t addr, bool write) noexcept; |
|||
#else |
|||
const u64 HostPageSize = sysconf(_SC_PAGESIZE); |
|||
const u64 HostPageBits = std::countr_zero(HostPageSize); |
|||
const u64 HostPageMask = ~(HostPageSize - 1); |
|||
#endif |
|||
|
|||
void* AllocateMemoryPages(std::size_t size) noexcept; |
|||
void FreeMemoryPages(void* base, std::size_t size) noexcept; |
|||
|
|||
/// A large page-aligned buffer that has optimized memory usage for zero-writes. |
|||
template <typename T> |
|||
requires std::is_trivially_copyable_v<T> |
|||
class SparseLargeVector final { |
|||
public: |
|||
constexpr SparseLargeVector() = default; |
|||
|
|||
explicit SparseLargeVector(std::size_t count) noexcept |
|||
: alloc_size{count * sizeof(T)} |
|||
{ |
|||
base_ptr = static_cast<T*>(AllocateMemoryPages(alloc_size)); |
|||
|
|||
// each item in vector holds information for 64 pages |
|||
auto denom = HostPageSize * 64; |
|||
committed_pages = std::vector<std::atomic<u64>>((alloc_size + denom - 1) / denom); |
|||
} |
|||
|
|||
~SparseLargeVector() noexcept { |
|||
FreeMemoryPages(base_ptr, alloc_size); |
|||
} |
|||
|
|||
SparseLargeVector(const SparseLargeVector&) = delete; |
|||
SparseLargeVector& operator=(const SparseLargeVector&) = delete; |
|||
SparseLargeVector(SparseLargeVector&& other) = delete; |
|||
SparseLargeVector& operator=(SparseLargeVector&& other) = delete; |
|||
|
|||
void ResizeAndClear(std::size_t count) noexcept { |
|||
if (auto const new_size = count * sizeof(T); new_size != alloc_size) { |
|||
FreeMemoryPages(base_ptr, alloc_size); |
|||
alloc_size = new_size; |
|||
base_ptr = static_cast<T*>(AllocateMemoryPages(alloc_size)); |
|||
|
|||
auto denom = HostPageSize * 64; |
|||
committed_pages = std::vector<std::atomic<u64>>((alloc_size + denom - 1) / denom); |
|||
} |
|||
} |
|||
|
|||
/// Returns a reference to the value of the requested index and allocates memory if needed. |
|||
T& GetAndFault(std::size_t index) noexcept { |
|||
if (index > alloc_size / sizeof(T)) { |
|||
UNREACHABLE_MSG("Out of bounds RW access on SparseLargeVector @ {}", index); |
|||
} |
|||
|
|||
if (!IsCommittedPage(index)) { |
|||
CommitPage(index); |
|||
} |
|||
return base_ptr[index]; |
|||
} |
|||
|
|||
/// Returns a reference to the value of the requested index if initialized, or will otherwise return a zero-initialized object. |
|||
const T& GetOrDefault(std::size_t index) const { |
|||
#ifdef _WIN32 |
|||
if (!IsCommittedPage(index)) { |
|||
return *reinterpret_cast<const T*>(&default_val); |
|||
} |
|||
#endif |
|||
// On non-Windows, OS page table should optimize this by pointing to a zero page if unallocated. |
|||
return base_ptr[index]; |
|||
} |
|||
|
|||
void Set(std::size_t index, const T& value) noexcept { |
|||
if (index > alloc_size / sizeof(T)) { |
|||
LOG_CRITICAL(Common_Memory, "Out of bounds write on SparseLargeVector @ {}", index); |
|||
return; |
|||
} |
|||
if (!IsCommittedPage(index)) |
|||
CommitPage(index); |
|||
base_ptr[index] = value; |
|||
} |
|||
|
|||
void ZeroRegion(std::size_t start, std::size_t end_) noexcept { |
|||
u64 base = reinterpret_cast<u64>(&base_ptr[start]); |
|||
const u64 end = reinterpret_cast<u64>(&base_ptr[end_]); |
|||
|
|||
const u64 end_page = AlignUp(base, HostPageSize); |
|||
const u64 first_size = (std::min)(end_page, end) - base; |
|||
|
|||
if (IsCommittedPage(start / sizeof(T))) { |
|||
std::memset(reinterpret_cast<void*>(base), 0, first_size); |
|||
} |
|||
|
|||
if (end <= end_page) |
|||
return; |
|||
|
|||
base = end_page; |
|||
|
|||
for (u64 page = base; page < end; page += HostPageSize) { |
|||
if (!IsCommittedPage((page - reinterpret_cast<u64>(base_ptr)) / sizeof(T))) { |
|||
continue; |
|||
} |
|||
|
|||
std::memset(reinterpret_cast<void*>(page), 0, (std::min)( HostPageSize, end - page)); |
|||
} |
|||
} |
|||
|
|||
constexpr void CommitRegion(size_t index, size_t end_) { |
|||
const u64 base = static_cast<u64>(index) * sizeof(T); |
|||
const u64 end = static_cast<u64>(end_) * sizeof(T); |
|||
|
|||
for (u64 page = AlignDown(base, HostPageSize); page < end; page += HostPageSize) { |
|||
if (!IsCommittedPage(page / sizeof(T))) { |
|||
CommitPage(page / sizeof(T)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
constexpr T& GetUnchecked(size_t index) { |
|||
return base_ptr[index]; |
|||
} |
|||
|
|||
[[nodiscard]] constexpr const T& operator[](std::size_t index) const noexcept { |
|||
return GetOrDefault(index); |
|||
} |
|||
|
|||
[[nodiscard]] constexpr const T* data() const noexcept { |
|||
return base_ptr; |
|||
} |
|||
|
|||
[[nodiscard]] constexpr std::size_t size() const noexcept { |
|||
return alloc_size / sizeof(T); |
|||
} |
|||
|
|||
private: |
|||
[[nodiscard]] constexpr bool IsCommittedPage(std::size_t index) const noexcept { |
|||
if (index > alloc_size / sizeof(T)) { |
|||
LOG_CRITICAL(Common_Memory, "Out of bounds access on large vector @ {}", index); |
|||
return false; |
|||
} |
|||
|
|||
auto page = (index * sizeof(T)) >> HostPageBits; |
|||
auto val = committed_pages[page >> 6].load(std::memory_order_acquire); |
|||
return (val >> (page & 63)) & 1; |
|||
} |
|||
|
|||
constexpr void CommitPage(std::size_t index) noexcept { |
|||
auto page_index = (index * sizeof(T)) >> HostPageBits; |
|||
auto page = reinterpret_cast<uintptr_t>(base_ptr + index) & HostPageMask; |
|||
#if defined(_WIN32) |
|||
CommitVectorPage(page, true); |
|||
#else |
|||
mprotect(reinterpret_cast<void*>(page), HostPageSize, PROT_READ | PROT_WRITE); |
|||
#endif |
|||
|
|||
committed_pages[page_index >> 6].fetch_or(1ULL << (page_index & 63), std::memory_order_release); |
|||
} |
|||
|
|||
std::size_t alloc_size{}; |
|||
T* base_ptr{}; |
|||
|
|||
std::vector<std::atomic<u64>> committed_pages{}; |
|||
#ifdef _WIN32 |
|||
const std::array<u8, sizeof(T)> default_val{}; |
|||
#endif |
|||
}; |
|||
|
|||
} // namespace Common |
|||
@ -1,44 +0,0 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|||
|
|||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|||
|
|||
#ifdef _WIN32
|
|||
#include <windows.h>
|
|||
#else
|
|||
#include <sys/mman.h>
|
|||
#endif
|
|||
|
|||
#include "common/assert.h"
|
|||
#include "common/virtual_buffer.h"
|
|||
|
|||
namespace Common { |
|||
|
|||
void* AllocateMemoryPages(std::size_t size) noexcept { |
|||
#ifdef _WIN32
|
|||
void* base = VirtualAlloc(nullptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); |
|||
if (base == nullptr) { |
|||
// Probably failing to reserve is less likely than failing to commit
|
|||
base = VirtualAlloc(nullptr, size, MEM_COMMIT, PAGE_READWRITE); |
|||
} |
|||
#else
|
|||
void* base = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0); |
|||
if (base == MAP_FAILED) |
|||
base = nullptr; |
|||
#endif
|
|||
ASSERT(base); |
|||
return base; |
|||
} |
|||
|
|||
void FreeMemoryPages(void* base, [[maybe_unused]] std::size_t size) noexcept { |
|||
if (!base) |
|||
return; |
|||
#ifdef _WIN32
|
|||
ASSERT(VirtualFree(base, 0, MEM_RELEASE)); |
|||
#else
|
|||
ASSERT(munmap(base, size) == 0); |
|||
#endif
|
|||
} |
|||
|
|||
} // namespace Common
|
|||
@ -1,84 +0,0 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project |
|||
// SPDX-License-Identifier: GPL-2.0-or-later |
|||
|
|||
#pragma once |
|||
|
|||
#include <utility> |
|||
|
|||
namespace Common { |
|||
|
|||
void* AllocateMemoryPages(std::size_t size) noexcept; |
|||
void FreeMemoryPages(void* base, std::size_t size) noexcept; |
|||
|
|||
template <typename T> |
|||
class VirtualBuffer final { |
|||
public: |
|||
// TODO: Uncomment this and change Common::PageTable::PageInfo to be trivially constructible |
|||
// using std::atomic_ref once libc++ has support for it |
|||
// static_assert( |
|||
// std::is_trivially_constructible_v<T>, |
|||
// "T must be trivially constructible, as non-trivial constructors will not be executed " |
|||
// "with the current allocator"); |
|||
|
|||
constexpr VirtualBuffer() = default; |
|||
explicit VirtualBuffer(std::size_t count) noexcept |
|||
: alloc_size{count * sizeof(T)} |
|||
{ |
|||
base_ptr = reinterpret_cast<T*>(AllocateMemoryPages(alloc_size)); |
|||
} |
|||
|
|||
~VirtualBuffer() noexcept { |
|||
FreeMemoryPages(base_ptr, alloc_size); |
|||
} |
|||
|
|||
VirtualBuffer(const VirtualBuffer&) = delete; |
|||
VirtualBuffer& operator=(const VirtualBuffer&) = delete; |
|||
|
|||
VirtualBuffer(VirtualBuffer&& other) noexcept |
|||
: alloc_size{std::exchange(other.alloc_size, 0)} |
|||
, base_ptr{std::exchange(other.base_ptr, nullptr)} |
|||
{} |
|||
|
|||
VirtualBuffer& operator=(VirtualBuffer&& other) noexcept { |
|||
alloc_size = std::exchange(other.alloc_size, 0); |
|||
base_ptr = std::exchange(other.base_ptr, nullptr); |
|||
return *this; |
|||
} |
|||
|
|||
void resize(std::size_t count) noexcept { |
|||
if (auto const new_size = count * sizeof(T); new_size != alloc_size) { |
|||
FreeMemoryPages(base_ptr, alloc_size); |
|||
alloc_size = new_size; |
|||
base_ptr = reinterpret_cast<T*>(AllocateMemoryPages(alloc_size)); |
|||
} |
|||
} |
|||
|
|||
[[nodiscard]] constexpr const T& operator[](std::size_t index) const noexcept { |
|||
return base_ptr[index]; |
|||
} |
|||
|
|||
[[nodiscard]] constexpr T& operator[](std::size_t index) noexcept { |
|||
return base_ptr[index]; |
|||
} |
|||
|
|||
[[nodiscard]] constexpr T* data() noexcept { |
|||
return base_ptr; |
|||
} |
|||
|
|||
[[nodiscard]] constexpr const T* data() const noexcept { |
|||
return base_ptr; |
|||
} |
|||
|
|||
[[nodiscard]] constexpr std::size_t size() const noexcept { |
|||
return alloc_size / sizeof(T); |
|||
} |
|||
|
|||
private: |
|||
std::size_t alloc_size{}; |
|||
T* base_ptr{}; |
|||
}; |
|||
|
|||
} // namespace Common |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue