From eb235d6ee30c0f15c775a08842aa070a7839b137 Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Tue, 28 Nov 2023 21:15:27 +0100 Subject: [PATCH 1/2] Common/MemArenaWin: Move the advanced Windows memory function address initialization into its own struct and function so it can be reused. --- Source/Core/Common/MemArena.h | 15 +++-- Source/Core/Common/MemArenaWin.cpp | 95 ++++++++++++++++-------------- 2 files changed, 61 insertions(+), 49 deletions(-) diff --git a/Source/Core/Common/MemArena.h b/Source/Core/Common/MemArena.h index 33ebb66eb4..4dd92ed72e 100644 --- a/Source/Core/Common/MemArena.h +++ b/Source/Core/Common/MemArena.h @@ -14,6 +14,15 @@ namespace Common { #ifdef _WIN32 struct WindowsMemoryRegion; + +struct WindowsMemoryFunctions +{ + Common::DynamicLibrary m_kernel32_handle; + Common::DynamicLibrary m_api_ms_win_core_memory_l1_1_6_handle; + void* m_address_UnmapViewOfFileEx = nullptr; + void* m_address_VirtualAlloc2 = nullptr; + void* m_address_MapViewOfFile3 = nullptr; +}; #endif // This class lets you create a block of anonymous RAM, and then arbitrarily map views into it. @@ -110,11 +119,7 @@ private: std::vector m_regions; void* m_reserved_region = nullptr; void* m_memory_handle = nullptr; - Common::DynamicLibrary m_kernel32_handle; - Common::DynamicLibrary m_api_ms_win_core_memory_l1_1_6_handle; - void* m_address_UnmapViewOfFileEx = nullptr; - void* m_address_VirtualAlloc2 = nullptr; - void* m_address_MapViewOfFile3 = nullptr; + WindowsMemoryFunctions m_memory_functions; #else int m_shm_fd = 0; void* m_reserved_region = nullptr; diff --git a/Source/Core/Common/MemArenaWin.cpp b/Source/Core/Common/MemArenaWin.cpp index b147ced53f..03a8a463e4 100644 --- a/Source/Core/Common/MemArenaWin.cpp +++ b/Source/Core/Common/MemArenaWin.cpp @@ -49,48 +49,55 @@ struct WindowsMemoryRegion } }; +static bool InitWindowsMemoryFunctions(WindowsMemoryFunctions* functions) +{ + DynamicLibrary kernelBase{"KernelBase.dll"}; + if (!kernelBase.IsOpen()) + return false; + + void* const ptr_IsApiSetImplemented = kernelBase.GetSymbolAddress("IsApiSetImplemented"); + if (!ptr_IsApiSetImplemented) + return false; + if (!static_cast(ptr_IsApiSetImplemented)("api-ms-win-core-memory-l1-1-6")) + return false; + + functions->m_api_ms_win_core_memory_l1_1_6_handle.Open("api-ms-win-core-memory-l1-1-6.dll"); + functions->m_kernel32_handle.Open("Kernel32.dll"); + if (!functions->m_api_ms_win_core_memory_l1_1_6_handle.IsOpen() || + !functions->m_kernel32_handle.IsOpen()) + { + functions->m_api_ms_win_core_memory_l1_1_6_handle.Close(); + functions->m_kernel32_handle.Close(); + return false; + } + + void* const address_VirtualAlloc2 = + functions->m_api_ms_win_core_memory_l1_1_6_handle.GetSymbolAddress("VirtualAlloc2FromApp"); + void* const address_MapViewOfFile3 = + functions->m_api_ms_win_core_memory_l1_1_6_handle.GetSymbolAddress("MapViewOfFile3FromApp"); + void* const address_UnmapViewOfFileEx = + functions->m_kernel32_handle.GetSymbolAddress("UnmapViewOfFileEx"); + if (address_VirtualAlloc2 && address_MapViewOfFile3 && address_UnmapViewOfFileEx) + { + functions->m_address_VirtualAlloc2 = address_VirtualAlloc2; + functions->m_address_MapViewOfFile3 = address_MapViewOfFile3; + functions->m_address_UnmapViewOfFileEx = address_UnmapViewOfFileEx; + return true; + } + + // at least one function is not available, use legacy logic + functions->m_api_ms_win_core_memory_l1_1_6_handle.Close(); + functions->m_kernel32_handle.Close(); + return false; +} + MemArena::MemArena() { // Check if VirtualAlloc2 and MapViewOfFile3 are available, which provide functionality to reserve // a memory region no other allocation may occupy while still allowing us to allocate and map // stuff within it. If they're not available we'll instead fall back to the 'legacy' logic and // just hope that nothing allocates in our address range. - DynamicLibrary kernelBase{"KernelBase.dll"}; - if (!kernelBase.IsOpen()) - return; - - void* const ptr_IsApiSetImplemented = kernelBase.GetSymbolAddress("IsApiSetImplemented"); - if (!ptr_IsApiSetImplemented) - return; - if (!static_cast(ptr_IsApiSetImplemented)("api-ms-win-core-memory-l1-1-6")) - return; - - m_api_ms_win_core_memory_l1_1_6_handle.Open("api-ms-win-core-memory-l1-1-6.dll"); - m_kernel32_handle.Open("Kernel32.dll"); - if (!m_api_ms_win_core_memory_l1_1_6_handle.IsOpen() || !m_kernel32_handle.IsOpen()) - { - m_api_ms_win_core_memory_l1_1_6_handle.Close(); - m_kernel32_handle.Close(); - return; - } - - void* const address_VirtualAlloc2 = - m_api_ms_win_core_memory_l1_1_6_handle.GetSymbolAddress("VirtualAlloc2FromApp"); - void* const address_MapViewOfFile3 = - m_api_ms_win_core_memory_l1_1_6_handle.GetSymbolAddress("MapViewOfFile3FromApp"); - void* const address_UnmapViewOfFileEx = m_kernel32_handle.GetSymbolAddress("UnmapViewOfFileEx"); - if (address_VirtualAlloc2 && address_MapViewOfFile3 && address_UnmapViewOfFileEx) - { - m_address_VirtualAlloc2 = address_VirtualAlloc2; - m_address_MapViewOfFile3 = address_MapViewOfFile3; - m_address_UnmapViewOfFileEx = address_UnmapViewOfFileEx; - } - else - { - // at least one function is not available, use legacy logic - m_api_ms_win_core_memory_l1_1_6_handle.Close(); - m_kernel32_handle.Close(); - } + InitWindowsMemoryFunctions(&m_memory_functions); } MemArena::~MemArena() @@ -146,9 +153,9 @@ u8* MemArena::ReserveMemoryRegion(size_t memory_size) } u8* base; - if (m_api_ms_win_core_memory_l1_1_6_handle.IsOpen()) + if (m_memory_functions.m_api_ms_win_core_memory_l1_1_6_handle.IsOpen()) { - base = static_cast(static_cast(m_address_VirtualAlloc2)( + base = static_cast(static_cast(m_memory_functions.m_address_VirtualAlloc2)( nullptr, nullptr, memory_size, MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS, nullptr, 0)); if (base) @@ -177,7 +184,7 @@ u8* MemArena::ReserveMemoryRegion(size_t memory_size) void MemArena::ReleaseMemoryRegion() { - if (m_api_ms_win_core_memory_l1_1_6_handle.IsOpen() && m_reserved_region) + if (m_memory_functions.m_api_ms_win_core_memory_l1_1_6_handle.IsOpen() && m_reserved_region) { // user should have unmapped everything by this point, check if that's true and yell if not // (it indicates a bug in the emulated memory mapping logic) @@ -314,7 +321,7 @@ WindowsMemoryRegion* MemArena::EnsureSplitRegionForMapping(void* start_address, void* MemArena::MapInMemoryRegion(s64 offset, size_t size, void* base) { - if (m_api_ms_win_core_memory_l1_1_6_handle.IsOpen()) + if (m_memory_functions.m_api_ms_win_core_memory_l1_1_6_handle.IsOpen()) { WindowsMemoryRegion* const region = EnsureSplitRegionForMapping(base, size); if (!region) @@ -323,7 +330,7 @@ void* MemArena::MapInMemoryRegion(s64 offset, size_t size, void* base) return nullptr; } - void* rv = static_cast(m_address_MapViewOfFile3)( + void* rv = static_cast(m_memory_functions.m_address_MapViewOfFile3)( m_memory_handle, nullptr, base, offset, size, MEM_REPLACE_PLACEHOLDER, PAGE_READWRITE, nullptr, 0); if (rv) @@ -416,10 +423,10 @@ bool MemArena::JoinRegionsAfterUnmap(void* start_address, size_t size) void MemArena::UnmapFromMemoryRegion(void* view, size_t size) { - if (m_api_ms_win_core_memory_l1_1_6_handle.IsOpen()) + if (m_memory_functions.m_api_ms_win_core_memory_l1_1_6_handle.IsOpen()) { - if (static_cast(m_address_UnmapViewOfFileEx)(view, - MEM_PRESERVE_PLACEHOLDER)) + if (static_cast(m_memory_functions.m_address_UnmapViewOfFileEx)( + view, MEM_PRESERVE_PLACEHOLDER)) { if (!JoinRegionsAfterUnmap(view, size)) PanicAlertFmt("Joining memory region failed."); From 3364d571cca88344bd7cc1364951f5f7b4a11ce9 Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Tue, 28 Nov 2023 21:17:12 +0100 Subject: [PATCH 2/2] Common/MemArenaWin: Rewrite LazyMemoryRegion to manually map memory blocks into the large memory region as needed. Internal details: The large region is split into individual same-sized blocks of memory. On creation, we allocate a single block of memory that will always remain zero, and map that into the entire memory region. Then, the first time any of these blocks is written to, we swap the mapped zero block out with a newly allocated block of memory. On clear, we swap back to the zero block and deallocate the data blocks. That way we only actually allocate one zero block as well as a handful of real data blocks where the JitCache actually writes to. --- Source/Core/Common/MemArena.h | 25 +++ Source/Core/Common/MemArenaWin.cpp | 156 +++++++++++++++++- .../Core/Core/PowerPC/JitCommon/JitCache.cpp | 10 ++ 3 files changed, 184 insertions(+), 7 deletions(-) diff --git a/Source/Core/Common/MemArena.h b/Source/Core/Common/MemArena.h index 4dd92ed72e..4f414afa27 100644 --- a/Source/Core/Common/MemArena.h +++ b/Source/Core/Common/MemArena.h @@ -160,9 +160,34 @@ public: /// void Release(); + /// + /// Ensure that the memory page at the given byte offset from the start of the memory region is + /// writable. We use this on Windows as a workaround to only actually commit pages as they are + /// written to. On other OSes this does nothing. + /// + /// @param offset The offset into the memory region that should be made writable if it isn't. + /// + void EnsureMemoryPageWritable(size_t offset) + { +#ifdef _WIN32 + const size_t block_index = offset / BLOCK_SIZE; + if (m_writable_block_handles[block_index] == nullptr) + MakeMemoryBlockWritable(block_index); +#endif + } + private: void* m_memory = nullptr; size_t m_size = 0; + +#ifdef _WIN32 + void* m_zero_block = nullptr; + constexpr static size_t BLOCK_SIZE = 8 * 1024 * 1024; // size of allocated memory blocks + WindowsMemoryFunctions m_memory_functions; + std::vector m_writable_block_handles; + + void MakeMemoryBlockWritable(size_t offset); +#endif }; } // namespace Common diff --git a/Source/Core/Common/MemArenaWin.cpp b/Source/Core/Common/MemArenaWin.cpp index 03a8a463e4..a991687d91 100644 --- a/Source/Core/Common/MemArenaWin.cpp +++ b/Source/Core/Common/MemArenaWin.cpp @@ -12,6 +12,7 @@ #include +#include "Common/Align.h" #include "Common/Assert.h" #include "Common/CommonFuncs.h" #include "Common/CommonTypes.h" @@ -441,7 +442,10 @@ void MemArena::UnmapFromMemoryRegion(void* view, size_t size) UnmapViewOfFile(view); } -LazyMemoryRegion::LazyMemoryRegion() = default; +LazyMemoryRegion::LazyMemoryRegion() +{ + InitWindowsMemoryFunctions(&m_memory_functions); +} LazyMemoryRegion::~LazyMemoryRegion() { @@ -455,15 +459,67 @@ void* LazyMemoryRegion::Create(size_t size) if (size == 0) return nullptr; - void* memory = VirtualAlloc(nullptr, size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); + if (!m_memory_functions.m_api_ms_win_core_memory_l1_1_6_handle.IsOpen()) + return nullptr; + + // reserve block of memory + const size_t memory_size = Common::AlignUp(size, BLOCK_SIZE); + const size_t block_count = memory_size / BLOCK_SIZE; + u8* memory = + static_cast(static_cast(m_memory_functions.m_address_VirtualAlloc2)( + nullptr, nullptr, memory_size, MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS, + nullptr, 0)); if (!memory) { - NOTICE_LOG_FMT(MEMMAP, "Memory allocation of {} bytes failed.", size); + NOTICE_LOG_FMT(MEMMAP, "Memory reservation of {} bytes failed.", size); return nullptr; } + // split into individual block-sized regions + for (size_t i = 0; i < block_count - 1; ++i) + { + if (!VirtualFree(memory + i * BLOCK_SIZE, BLOCK_SIZE, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) + { + NOTICE_LOG_FMT(MEMMAP, "Region splitting failed: {}", GetLastErrorString()); + + // release every split block as well as the remaining unsplit one + for (size_t j = 0; j < i + 1; ++j) + VirtualFree(memory + j * BLOCK_SIZE, 0, MEM_RELEASE); + + return nullptr; + } + } + m_memory = memory; - m_size = size; + m_size = memory_size; + + // allocate a single block of real memory in the page file + HANDLE zero_block = CreateFileMapping(INVALID_HANDLE_VALUE, nullptr, PAGE_READONLY, + GetHighDWORD(BLOCK_SIZE), GetLowDWORD(BLOCK_SIZE), nullptr); + if (zero_block == nullptr) + { + NOTICE_LOG_FMT(MEMMAP, "CreateFileMapping() failed for zero block: {}", GetLastErrorString()); + Release(); + return nullptr; + } + + m_zero_block = zero_block; + + // map the zero page into every block + for (size_t i = 0; i < block_count; ++i) + { + void* result = static_cast(m_memory_functions.m_address_MapViewOfFile3)( + zero_block, nullptr, memory + i * BLOCK_SIZE, 0, BLOCK_SIZE, MEM_REPLACE_PLACEHOLDER, + PAGE_READONLY, nullptr, 0); + if (!result) + { + NOTICE_LOG_FMT(MEMMAP, "Mapping the zero block failed: {}", GetLastErrorString()); + Release(); + return nullptr; + } + } + + m_writable_block_handles.resize(block_count, nullptr); return memory; } @@ -471,19 +527,105 @@ void* LazyMemoryRegion::Create(size_t size) void LazyMemoryRegion::Clear() { ASSERT(m_memory); + u8* const memory = static_cast(m_memory); - VirtualFree(m_memory, m_size, MEM_DECOMMIT); - VirtualAlloc(m_memory, m_size, MEM_COMMIT, PAGE_READWRITE); + // reset every writable block back to the zero block + for (size_t i = 0; i < m_writable_block_handles.size(); ++i) + { + if (m_writable_block_handles[i] == nullptr) + continue; + + // unmap the writable block + if (!static_cast(m_memory_functions.m_address_UnmapViewOfFileEx)( + memory + i * BLOCK_SIZE, MEM_PRESERVE_PLACEHOLDER)) + { + PanicAlertFmt("Failed to unmap the writable block: {}", GetLastErrorString()); + } + + // free the writable block + if (!CloseHandle(m_writable_block_handles[i])) + { + PanicAlertFmt("Failed to free the writable block: {}", GetLastErrorString()); + } + m_writable_block_handles[i] = nullptr; + + // map the zero block + void* map_result = static_cast(m_memory_functions.m_address_MapViewOfFile3)( + m_zero_block, nullptr, memory + i * BLOCK_SIZE, 0, BLOCK_SIZE, MEM_REPLACE_PLACEHOLDER, + PAGE_READONLY, nullptr, 0); + if (!map_result) + { + PanicAlertFmt("Failed to re-map the zero block: {}", GetLastErrorString()); + } + } } void LazyMemoryRegion::Release() { if (m_memory) { - VirtualFree(m_memory, 0, MEM_RELEASE); + // unmap all pages and release the not-zero block handles + u8* const memory = static_cast(m_memory); + for (size_t i = 0; i < m_writable_block_handles.size(); ++i) + { + static_cast(m_memory_functions.m_address_UnmapViewOfFileEx)( + memory + i * BLOCK_SIZE, MEM_PRESERVE_PLACEHOLDER); + if (m_writable_block_handles[i]) + { + CloseHandle(m_writable_block_handles[i]); + m_writable_block_handles[i] = nullptr; + } + } + } + if (m_zero_block) + { + CloseHandle(m_zero_block); + m_zero_block = nullptr; + } + if (m_memory) + { + u8* const memory = static_cast(m_memory); + const size_t block_count = m_size / BLOCK_SIZE; + for (size_t i = 0; i < block_count; ++i) + VirtualFree(memory + i * BLOCK_SIZE, 0, MEM_RELEASE); m_memory = nullptr; m_size = 0; } } +void LazyMemoryRegion::MakeMemoryBlockWritable(size_t block_index) +{ + u8* const memory = static_cast(m_memory); + + // unmap the zero block + if (!static_cast(m_memory_functions.m_address_UnmapViewOfFileEx)( + memory + block_index * BLOCK_SIZE, MEM_PRESERVE_PLACEHOLDER)) + { + PanicAlertFmt("Failed to unmap the zero block: {}", GetLastErrorString()); + return; + } + + // allocate a fresh block to map + HANDLE block = CreateFileMapping(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, + GetHighDWORD(BLOCK_SIZE), GetLowDWORD(BLOCK_SIZE), nullptr); + if (block == nullptr) + { + PanicAlertFmt("CreateFileMapping() failed for writable block: {}", GetLastErrorString()); + return; + } + + // map the new block + void* map_result = static_cast(m_memory_functions.m_address_MapViewOfFile3)( + block, nullptr, memory + block_index * BLOCK_SIZE, 0, BLOCK_SIZE, MEM_REPLACE_PLACEHOLDER, + PAGE_READWRITE, nullptr, 0); + if (!map_result) + { + PanicAlertFmt("Failed to map the writable block: {}", GetLastErrorString()); + CloseHandle(block); + return; + } + + m_writable_block_handles[block_index] = block; +} + } // namespace Common diff --git a/Source/Core/Core/PowerPC/JitCommon/JitCache.cpp b/Source/Core/Core/PowerPC/JitCommon/JitCache.cpp index 04cb3f49f2..af3842929f 100644 --- a/Source/Core/Core/PowerPC/JitCommon/JitCache.cpp +++ b/Source/Core/Core/PowerPC/JitCommon/JitCache.cpp @@ -121,9 +121,14 @@ void JitBaseBlockCache::FinalizeBlock(JitBlock& block, bool block_link, { size_t index = FastLookupIndexForAddress(block.effectiveAddress, block.feature_flags); if (m_entry_points_ptr) + { + m_entry_points_arena.EnsureMemoryPageWritable(index * sizeof(u8*)); m_entry_points_ptr[index] = block.normalEntry; + } else + { m_fast_block_map_fallback[index] = █ + } block.fast_block_map_index = index; block.physical_addresses = physical_addresses; @@ -485,9 +490,14 @@ JitBlock* JitBaseBlockCache::MoveBlockIntoFastCache(u32 addr, CPUEmuFeatureFlags // And create a new one size_t index = FastLookupIndexForAddress(addr, feature_flags); if (m_entry_points_ptr) + { + m_entry_points_arena.EnsureMemoryPageWritable(index * sizeof(u8*)); m_entry_points_ptr[index] = block->normalEntry; + } else + { m_fast_block_map_fallback[index] = block; + } block->fast_block_map_index = index; return block;