From 79405de1037e93042b6357e19a43058d4a6b159f Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Mon, 21 Jun 2021 06:15:56 +0200 Subject: [PATCH] MemArena: Use placeholders on Windows. --- Source/Core/Common/MemArena.h | 19 +- Source/Core/Common/MemArenaWin.cpp | 368 +++++++++++++++++++++++++++-- 2 files changed, 368 insertions(+), 19 deletions(-) diff --git a/Source/Core/Common/MemArena.h b/Source/Core/Common/MemArena.h index 3f3b3adf89..bdde5579c5 100644 --- a/Source/Core/Common/MemArena.h +++ b/Source/Core/Common/MemArena.h @@ -4,15 +4,16 @@ #pragma once #include - -#ifdef _WIN32 -#include -#endif +#include #include "Common/CommonTypes.h" namespace Common { +#ifdef _WIN32 +struct WindowsMemoryRegion; +#endif + // This class lets you create a block of anonymous RAM, and then arbitrarily map views into it. // Multiple views can mirror the same section of the block, which makes it very convenient for // emulating memory mirrors. @@ -99,7 +100,15 @@ public: private: #ifdef _WIN32 - HANDLE hMemoryMapping; + WindowsMemoryRegion* EnsureSplitRegionForMapping(void* address, size_t size); + bool JoinRegionsAfterUnmap(void* address, size_t size); + + std::vector m_regions; + void* m_reserved_region = nullptr; + void* m_memory_handle = nullptr; + void* m_api_ms_win_core_memory_l1_1_6_handle = nullptr; + void* m_address_VirtualAlloc2 = nullptr; + void* m_address_MapViewOfFile3 = nullptr; #else int fd; #endif diff --git a/Source/Core/Common/MemArenaWin.cpp b/Source/Core/Common/MemArenaWin.cpp index 3229e417ab..6434627478 100644 --- a/Source/Core/Common/MemArenaWin.cpp +++ b/Source/Core/Common/MemArenaWin.cpp @@ -3,70 +3,410 @@ #include "Common/MemArena.h" +#include #include #include -#include #include #include +#include "Common/Assert.h" #include "Common/CommonFuncs.h" #include "Common/CommonTypes.h" +#include "Common/DynamicLibrary.h" #include "Common/Logging/Log.h" #include "Common/MsgHandler.h" #include "Common/StringUtil.h" +using PVirtualAlloc2 = PVOID(WINAPI*)(HANDLE Process, PVOID BaseAddress, SIZE_T Size, + ULONG AllocationType, ULONG PageProtection, + MEM_EXTENDED_PARAMETER* ExtendedParameters, + ULONG ParameterCount); + +using PMapViewOfFile3 = PVOID(WINAPI*)(HANDLE FileMapping, HANDLE Process, PVOID BaseAddress, + ULONG64 Offset, SIZE_T ViewSize, ULONG AllocationType, + ULONG PageProtection, + MEM_EXTENDED_PARAMETER* ExtendedParameters, + ULONG ParameterCount); + +using PIsApiSetImplemented = BOOL(APIENTRY*)(PCSTR Contract); + namespace Common { -MemArena::MemArena() = default; -MemArena::~MemArena() = default; +struct WindowsMemoryRegion +{ + u8* m_start; + size_t m_size; + bool m_is_mapped; + + WindowsMemoryRegion(u8* start, size_t size, bool is_mapped) + : m_start(start), m_size(size), m_is_mapped(is_mapped) + { + } +}; + +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; + + const HMODULE handle = LoadLibraryW(L"api-ms-win-core-memory-l1-1-6.dll"); + if (!handle) + return; + + void* const address_VirtualAlloc2 = GetProcAddress(handle, "VirtualAlloc2FromApp"); + void* const address_MapViewOfFile3 = GetProcAddress(handle, "MapViewOfFile3FromApp"); + if (address_VirtualAlloc2 && address_MapViewOfFile3) + { + m_api_ms_win_core_memory_l1_1_6_handle = handle; + m_address_VirtualAlloc2 = address_VirtualAlloc2; + m_address_MapViewOfFile3 = address_MapViewOfFile3; + } + else + { + // at least one function is not available, use legacy logic + FreeLibrary(handle); + } +} + +MemArena::~MemArena() +{ + ReleaseMemoryRegion(); + ReleaseSHMSegment(); + if (m_api_ms_win_core_memory_l1_1_6_handle) + FreeLibrary(static_cast(m_api_ms_win_core_memory_l1_1_6_handle)); +} void MemArena::GrabSHMSegment(size_t size) { const std::string name = "dolphin-emu." + std::to_string(GetCurrentProcessId()); - hMemoryMapping = CreateFileMapping(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, - static_cast(size), UTF8ToTStr(name).c_str()); + m_memory_handle = CreateFileMapping(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, + static_cast(size), UTF8ToTStr(name).c_str()); } void MemArena::ReleaseSHMSegment() { - CloseHandle(hMemoryMapping); - hMemoryMapping = 0; + if (!m_memory_handle) + return; + CloseHandle(m_memory_handle); + m_memory_handle = nullptr; } void* MemArena::CreateView(s64 offset, size_t size) { - return MapInMemoryRegion(offset, size, nullptr); + return MapViewOfFileEx(m_memory_handle, FILE_MAP_ALL_ACCESS, 0, (DWORD)((u64)offset), size, + nullptr); } void MemArena::ReleaseView(void* view, size_t size) { - UnmapFromMemoryRegion(view, size); + UnmapViewOfFile(view); } u8* MemArena::ReserveMemoryRegion(size_t memory_size) { - u8* base = static_cast(VirtualAlloc(nullptr, memory_size, MEM_RESERVE, PAGE_READWRITE)); - if (!base) + if (m_reserved_region) { - PanicAlertFmt("Failed to map enough memory space: {}", GetLastErrorString()); + PanicAlertFmt("Tried to reserve a second memory region from the same MemArena."); return nullptr; } - VirtualFree(base, 0, MEM_RELEASE); + + u8* base; + if (m_api_ms_win_core_memory_l1_1_6_handle) + { + base = static_cast(static_cast(m_address_VirtualAlloc2)( + nullptr, nullptr, memory_size, MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS, + nullptr, 0)); + if (base) + { + m_reserved_region = base; + m_regions.emplace_back(base, memory_size, false); + } + else + { + PanicAlertFmt("Failed to map enough memory space: {}", GetLastErrorString()); + } + } + else + { + NOTICE_LOG_FMT(MEMMAP, "VirtualAlloc2 and/or MapViewFromFile3 unavailable. " + "Falling back to legacy memory mapping."); + base = static_cast(VirtualAlloc(nullptr, memory_size, MEM_RESERVE, PAGE_READWRITE)); + if (base) + VirtualFree(base, 0, MEM_RELEASE); + else + PanicAlertFmt("Failed to find enough memory space: {}", GetLastErrorString()); + } + return base; } void MemArena::ReleaseMemoryRegion() { + if (m_api_ms_win_core_memory_l1_1_6_handle && 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) + size_t mapped_region_count = 0; + for (const auto& r : m_regions) + { + if (r.m_is_mapped) + ++mapped_region_count; + } + + if (mapped_region_count > 0) + { + PanicAlertFmt("Error while releasing fastmem region: {} regions are still mapped!", + mapped_region_count); + } + + // then free memory + VirtualFree(m_reserved_region, 0, MEM_RELEASE); + m_reserved_region = nullptr; + m_regions.clear(); + } +} + +WindowsMemoryRegion* MemArena::EnsureSplitRegionForMapping(void* start_address, size_t size) +{ + u8* const address = static_cast(start_address); + auto& regions = m_regions; + if (regions.empty()) + { + NOTICE_LOG_FMT(MEMMAP, "Tried to map a memory region without reserving a memory block first."); + return nullptr; + } + + // find closest region that is <= the given address by using upper bound and decrementing + auto it = std::upper_bound( + regions.begin(), regions.end(), address, + [](u8* addr, const WindowsMemoryRegion& region) { return addr < region.m_start; }); + if (it == regions.begin()) + { + // this should never happen, implies that the given address is before the start of the + // reserved memory block + NOTICE_LOG_FMT(MEMMAP, "Invalid address {} given to map.", fmt::ptr(address)); + return nullptr; + } + --it; + + if (it->m_is_mapped) + { + NOTICE_LOG_FMT(MEMMAP, + "Address to map {} with a size of 0x{:x} overlaps with existing mapping " + "at {}.", + fmt::ptr(address), size, fmt::ptr(it->m_start)); + return nullptr; + } + + const size_t mapping_index = it - regions.begin(); + u8* const mapping_address = it->m_start; + const size_t mapping_size = it->m_size; + if (mapping_address == address) + { + // if this region is already split up correctly we don't have to do anything + if (mapping_size == size) + return &*it; + + // if this region is smaller than the requested size we can't map + if (mapping_size < size) + { + NOTICE_LOG_FMT(MEMMAP, + "Not enough free space at address {} to map 0x{:x} bytes (0x{:x} available).", + fmt::ptr(mapping_address), size, mapping_size); + return nullptr; + } + + // split region + if (!VirtualFree(address, size, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) + { + NOTICE_LOG_FMT(MEMMAP, "Region splitting failed: {}", GetLastErrorString()); + return nullptr; + } + + // update tracked mappings and return the first of the two + it->m_size = size; + u8* const new_mapping_start = address + size; + const size_t new_mapping_size = mapping_size - size; + regions.insert(it + 1, WindowsMemoryRegion(new_mapping_start, new_mapping_size, false)); + return ®ions[mapping_index]; + } + + ASSERT(mapping_address < address); + + // is there enough space to map this? + const size_t size_before = static_cast(address - mapping_address); + const size_t minimum_size = size + size_before; + if (mapping_size < minimum_size) + { + NOTICE_LOG_FMT(MEMMAP, + "Not enough free space at address {} to map memory region (need 0x{:x} " + "bytes, but only 0x{:x} available).", + fmt::ptr(address), minimum_size, mapping_size); + return nullptr; + } + + // split region + if (!VirtualFree(address, size, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) + { + NOTICE_LOG_FMT(MEMMAP, "Region splitting failed: {}", GetLastErrorString()); + return nullptr; + } + + // do we now have two regions or three regions? + if (mapping_size == minimum_size) + { + // split into two; update tracked mappings and return the second one + it->m_size = size_before; + u8* const new_mapping_start = address; + const size_t new_mapping_size = size; + regions.insert(it + 1, WindowsMemoryRegion(new_mapping_start, new_mapping_size, false)); + return ®ions[mapping_index + 1]; + } + else + { + // split into three; update tracked mappings and return the middle one + it->m_size = size_before; + u8* const middle_mapping_start = address; + const size_t middle_mapping_size = size; + u8* const after_mapping_start = address + size; + const size_t after_mapping_size = mapping_size - minimum_size; + regions.insert(it + 1, WindowsMemoryRegion(after_mapping_start, after_mapping_size, false)); + regions.insert(regions.begin() + mapping_index + 1, + WindowsMemoryRegion(middle_mapping_start, middle_mapping_size, false)); + return ®ions[mapping_index + 1]; + } } void* MemArena::MapInMemoryRegion(s64 offset, size_t size, void* base) { - return MapViewOfFileEx(hMemoryMapping, FILE_MAP_ALL_ACCESS, 0, (DWORD)((u64)offset), size, base); + if (m_api_ms_win_core_memory_l1_1_6_handle) + { + WindowsMemoryRegion* const region = EnsureSplitRegionForMapping(base, size); + if (!region) + { + PanicAlertFmt("Splitting memory region failed."); + return nullptr; + } + + void* rv = static_cast(m_address_MapViewOfFile3)( + m_memory_handle, nullptr, base, offset, size, MEM_REPLACE_PLACEHOLDER, PAGE_READWRITE, + nullptr, 0); + if (rv) + { + region->m_is_mapped = true; + } + else + { + PanicAlertFmt("Mapping memory region failed: {}", GetLastErrorString()); + + // revert the split, if any + JoinRegionsAfterUnmap(base, size); + } + return rv; + } + + return MapViewOfFileEx(m_memory_handle, FILE_MAP_ALL_ACCESS, 0, (DWORD)((u64)offset), size, base); +} + +bool MemArena::JoinRegionsAfterUnmap(void* start_address, size_t size) +{ + u8* const address = static_cast(start_address); + auto& regions = m_regions; + if (regions.empty()) + { + NOTICE_LOG_FMT(MEMMAP, + "Tried to unmap a memory region without reserving a memory block first."); + return false; + } + + // there should be a mapping that matches the request exactly, find it + auto it = std::lower_bound( + regions.begin(), regions.end(), address, + [](const WindowsMemoryRegion& region, u8* addr) { return region.m_start < addr; }); + if (it == regions.end() || it->m_start != address || it->m_size != size) + { + // didn't find it, we were given bogus input + NOTICE_LOG_FMT(MEMMAP, "Invalid address/size given to unmap."); + return false; + } + it->m_is_mapped = false; + + const bool can_join_with_preceding = it != regions.begin() && !(it - 1)->m_is_mapped; + const bool can_join_with_succeeding = (it + 1) != regions.end() && !(it + 1)->m_is_mapped; + if (can_join_with_preceding && can_join_with_succeeding) + { + // join three mappings to one + auto it_preceding = it - 1; + auto it_succeeding = it + 1; + const size_t total_size = it_preceding->m_size + size + it_succeeding->m_size; + if (!VirtualFree(it_preceding->m_start, total_size, MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS)) + { + NOTICE_LOG_FMT(MEMMAP, "Region coalescing failed: {}", GetLastErrorString()); + return false; + } + + it_preceding->m_size = total_size; + regions.erase(it, it + 2); + } + else if (can_join_with_preceding && !can_join_with_succeeding) + { + // join two mappings to one + auto it_preceding = it - 1; + const size_t total_size = it_preceding->m_size + size; + if (!VirtualFree(it_preceding->m_start, total_size, MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS)) + { + NOTICE_LOG_FMT(MEMMAP, "Region coalescing failed: {}", GetLastErrorString()); + return false; + } + + it_preceding->m_size = total_size; + regions.erase(it); + } + else if (!can_join_with_preceding && can_join_with_succeeding) + { + // join two mappings to one + auto it_succeeding = it + 1; + const size_t total_size = size + it_succeeding->m_size; + if (!VirtualFree(it->m_start, total_size, MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS)) + { + NOTICE_LOG_FMT(MEMMAP, "Region coalescing failed: {}", GetLastErrorString()); + return false; + } + + it->m_size = total_size; + regions.erase(it_succeeding); + } + return true; } void MemArena::UnmapFromMemoryRegion(void* view, size_t size) { + if (m_api_ms_win_core_memory_l1_1_6_handle) + { + if (UnmapViewOfFileEx(view, MEM_PRESERVE_PLACEHOLDER)) + { + if (!JoinRegionsAfterUnmap(view, size)) + PanicAlertFmt("Joining memory region failed."); + } + else + { + PanicAlertFmt("Unmapping memory region failed: {}", GetLastErrorString()); + } + return; + } + UnmapViewOfFile(view); } } // namespace Common