diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 8e004a720..e71eceecc 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -54,6 +54,10 @@ add_library(common minizip_helpers.h null_audio_stream.cpp null_audio_stream.h + memory_arena.cpp + memory_arena.h + page_fault_handler.cpp + page_fault_handler.h rectangle.h progress_callback.cpp progress_callback.h @@ -183,3 +187,8 @@ if(APPLE AND NOT BUILD_LIBRETRO_CORE) gl/context_agl.h ) endif() + +if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + # We need -lrt for shm_unlink + target_link_libraries(common PRIVATE rt) +endif() diff --git a/src/common/common.vcxproj b/src/common/common.vcxproj index ea797a34e..be057f88e 100644 --- a/src/common/common.vcxproj +++ b/src/common/common.vcxproj @@ -90,6 +90,8 @@ + + @@ -154,6 +156,8 @@ + + @@ -743,4 +747,4 @@ - \ No newline at end of file + diff --git a/src/common/common.vcxproj.filters b/src/common/common.vcxproj.filters index 6d3229cc3..048011244 100644 --- a/src/common/common.vcxproj.filters +++ b/src/common/common.vcxproj.filters @@ -102,6 +102,8 @@ + + @@ -196,6 +198,8 @@ + + @@ -211,4 +215,4 @@ {642ff5eb-af39-4aab-a42f-6eb8188a11d7} - \ No newline at end of file + diff --git a/src/common/memory_arena.cpp b/src/common/memory_arena.cpp new file mode 100644 index 000000000..96cbc0e51 --- /dev/null +++ b/src/common/memory_arena.cpp @@ -0,0 +1,294 @@ +#include "memory_arena.h" +#include "common/assert.h" +#include "common/log.h" +#include "common/string_util.h" +Log_SetChannel(Common::MemoryArena); + +#if defined(WIN32) +#include "common/windows_headers.h" +#elif defined(ANDROID) +#include +#include +#include +#include +#include +#include +#elif defined(__linux__) || defined(__APPLE__) +#include +#include +#include +#include +#endif + +namespace Common { + +// Borrowed from Dolphin +#ifdef ANDROID +#define ASHMEM_DEVICE "/dev/ashmem" + +static int AshmemCreateFileMapping(const char* name, size_t size) +{ + // ASharedMemory path - works on API >= 26 and falls through on API < 26: + + // We can't call ASharedMemory_create the normal way without increasing the + // minimum version requirement to API 26, so we use dlopen/dlsym instead + static void* libandroid = dlopen("libandroid.so", RTLD_LAZY | RTLD_LOCAL); + static auto shared_memory_create = + reinterpret_cast(dlsym(libandroid, "ASharedMemory_create")); + if (shared_memory_create) + return shared_memory_create(name, size); + + // /dev/ashmem path - works on API < 29: + + int fd, ret; + fd = open(ASHMEM_DEVICE, O_RDWR); + if (fd < 0) + return fd; + + // We don't really care if we can't set the name, it is optional + ioctl(fd, ASHMEM_SET_NAME, name); + + ret = ioctl(fd, ASHMEM_SET_SIZE, size); + if (ret < 0) + { + close(fd); + Log_ErrorPrintf("Ashmem returned error: 0x%08x", ret); + return ret; + } + return fd; +} +#endif + +MemoryArena::MemoryArena() = default; + +MemoryArena::~MemoryArena() +{ +#if defined(WIN32) + if (m_file_handle) + CloseHandle(m_file_handle); +#elif defined(__linux__) + if (m_shmem_fd > 0) + close(m_shmem_fd); +#endif +} + +void* MemoryArena::FindBaseAddressForMapping(size_t size) +{ + void* base_address; +#if defined(WIN32) + base_address = VirtualAlloc(nullptr, size, MEM_RESERVE, PAGE_READWRITE); + if (base_address) + VirtualFree(base_address, 0, MEM_RELEASE); +#elif defined(__linux__) || defined(__APPLE__) + base_address = mmap(nullptr, size, PROT_NONE, MAP_ANON | MAP_PRIVATE, -1, 0); + if (base_address) + munmap(base_address, size); +#elif defined(__ANDROID__) + base_address = mmap(nullptr, size, PROT_NONE, MAP_ANON | MAP_SHARED, -1, 0); + if (base_address) + munmap(base_address, size); +#else + base_address = nullptr; +#endif + + if (!base_address) + { + Log_ErrorPrintf("Failed to get base address for memory mapping of size %zu", size); + return nullptr; + } + + return base_address; +} + +bool MemoryArena::Create(size_t size, bool writable, bool executable) +{ +#if defined(WIN32) + const std::string file_mapping_name = StringUtil::StdStringFromFormat("duckstation_%u", GetCurrentProcessId()); + + const DWORD protect = (writable ? (executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE) : PAGE_READONLY); + m_file_handle = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr, protect, Truncate32(size >> 32), Truncate32(size), + file_mapping_name.c_str()); + if (!m_file_handle) + { + Log_ErrorPrintf("CreateFileMapping failed: %u", GetLastError()); + return false; + } + + return true; +#elif defined(ANDROID) + const std::string file_mapping_name = + StringUtil::StdStringFromFormat("duckstation_%u", static_cast(getpid())); + m_shmem_fd = AshmemCreateFileMapping(file_mapping_name.c_str(), size); + if (m_shmem_fd < 0) + { + Log_ErrorPrintf("AshmemCreateFileMapping failed: %d %d", m_shmem_fd, errno); + return false; + } + + return true; +#elif defined(__linux__) + const std::string file_mapping_name = + StringUtil::StdStringFromFormat("duckstation_%u", static_cast(getpid())); + m_shmem_fd = shm_open(file_mapping_name.c_str(), O_CREAT | O_EXCL | (writable ? O_RDWR : O_RDONLY), 0600); + if (m_shmem_fd < 0) + { + Log_ErrorPrintf("shm_open failed: %d", errno); + return false; + } + + // we're not going to be opening this mapping in other processes, so remove the file + shm_unlink(file_mapping_name.c_str()); + + // ensure it's the correct size + if (ftruncate64(m_shmem_fd, static_cast(size)) < 0) + { + Log_ErrorPrintf("ftruncate64(%zu) failed: %d", size, errno); + return false; + } + + return true; +#elif defined(__APPLE__) + const std::string file_mapping_name = + StringUtil::StdStringFromFormat("duckstation_%u", static_cast(getpid())); + m_shmem_fd = shm_open(file_mapping_name.c_str(), O_CREAT | O_EXCL | (writable ? O_RDWR : O_RDONLY), 0600); + if (m_shmem_fd < 0) + { + Log_ErrorPrintf("shm_open failed: %d", errno); + return false; + } + + // we're not going to be opening this mapping in other processes, so remove the file + shm_unlink(file_mapping_name.c_str()); + + // ensure it's the correct size + if (ftruncate(m_shmem_fd, static_cast(size)) < 0) + { + Log_ErrorPrintf("ftruncate(%zu) failed: %d", size, errno); + return false; + } + + return true; +#else + return false; +#endif +} + +std::optional MemoryArena::CreateView(size_t offset, size_t size, bool writable, bool executable, + void* fixed_address) +{ + void* base_pointer = CreateViewPtr(offset, size, writable, executable, fixed_address); + if (!base_pointer) + return std::nullopt; + + return View(this, base_pointer, offset, size, writable); +} + +void* MemoryArena::CreateViewPtr(size_t offset, size_t size, bool writable, bool executable, + void* fixed_address /*= nullptr*/) +{ + void* base_pointer; +#if defined(WIN32) + const DWORD desired_access = FILE_MAP_READ | (writable ? FILE_MAP_WRITE : 0) | (executable ? FILE_MAP_EXECUTE : 0); + base_pointer = + MapViewOfFileEx(m_file_handle, desired_access, Truncate32(offset >> 32), Truncate32(offset), size, fixed_address); + if (!base_pointer) + return nullptr; +#elif defined(__linux__) + const int flags = (fixed_address != nullptr) ? (MAP_SHARED | MAP_FIXED) : MAP_SHARED; + const int prot = PROT_READ | (writable ? PROT_WRITE : 0) | (executable ? PROT_EXEC : 0); + base_pointer = mmap64(fixed_address, size, prot, flags, m_shmem_fd, static_cast(offset)); + if (base_pointer == reinterpret_cast(-1)) + return nullptr; +#elif defined(__APPLE__) + const int flags = (fixed_address != nullptr) ? (MAP_SHARED | MAP_FIXED) : MAP_SHARED; + const int prot = PROT_READ | (writable ? PROT_WRITE : 0) | (executable ? PROT_EXEC : 0); + base_pointer = mmap(fixed_address, size, prot, flags, m_shmem_fd, static_cast(offset)); + if (base_pointer == reinterpret_cast(-1)) + return nullptr; +#else + return nullptr; +#endif + + m_num_views.fetch_add(1); + return base_pointer; +} + +bool MemoryArena::FlushViewPtr(void* address, size_t size) +{ +#if defined(WIN32) + return FlushViewOfFile(address, size); +#elif defined(__linux__) || defined(__APPLE__) + return (msync(address, size, 0) >= 0); +#else + return false; +#endif +} + +bool MemoryArena::ReleaseViewPtr(void* address, size_t size) +{ + bool result; +#if defined(WIN32) + result = static_cast(UnmapViewOfFile(address)); +#elif defined(__linux__) || defined(__APPLE__) + result = (munmap(address, size) >= 0); +#else + result = false; +#endif + + if (!result) + { + Log_ErrorPrintf("Failed to unmap previously-created view at %p", address); + return false; + } + + const size_t prev_count = m_num_views.fetch_sub(1); + Assert(prev_count > 0); + return true; +} + +bool MemoryArena::SetPageProtection(void* address, size_t length, bool readable, bool writable, bool executable) +{ +#if defined(WIN32) + static constexpr DWORD protection_table[2][2][2] = { + {{PAGE_NOACCESS, PAGE_EXECUTE}, {PAGE_WRITECOPY, PAGE_EXECUTE_WRITECOPY}}, + {{PAGE_READONLY, PAGE_EXECUTE_READ}, {PAGE_READWRITE, PAGE_EXECUTE_READWRITE}}}; + + DWORD old_protect; + return static_cast( + VirtualProtect(address, length, protection_table[readable][writable][executable], &old_protect)); +#elif defined(__linux__) || defined(__ANDROID__) || defined(__APPLE__) + const int prot = (readable ? PROT_READ : 0) | (writable ? PROT_WRITE : 0) | (executable ? PROT_EXEC : 0); + return (mprotect(address, length, prot) >= 0); +#else + return false; +#endif +} + +MemoryArena::View::View(MemoryArena* parent, void* base_pointer, size_t arena_offset, size_t mapping_size, + bool writable) + : m_parent(parent), m_base_pointer(base_pointer), m_arena_offset(arena_offset), m_mapping_size(mapping_size), + m_writable(writable) +{ +} + +MemoryArena::View::View(View&& view) + : m_parent(view.m_parent), m_base_pointer(view.m_base_pointer), m_arena_offset(view.m_arena_offset), + m_mapping_size(view.m_mapping_size) +{ + view.m_parent = nullptr; + view.m_base_pointer = nullptr; + view.m_arena_offset = 0; + view.m_mapping_size = 0; +} + +MemoryArena::View::~View() +{ + if (m_parent) + { + if (m_writable && !m_parent->FlushViewPtr(m_base_pointer, m_mapping_size)) + Panic("Failed to flush previously-created view"); + if (!m_parent->ReleaseViewPtr(m_base_pointer, m_mapping_size)) + Panic("Failed to unmap previously-created view"); + } +} +} // namespace Common diff --git a/src/common/memory_arena.h b/src/common/memory_arena.h new file mode 100644 index 000000000..7e2d51bb1 --- /dev/null +++ b/src/common/memory_arena.h @@ -0,0 +1,58 @@ +#pragma once +#include "types.h" +#include +#include + +namespace Common { +class MemoryArena +{ +public: + class View + { + public: + View(MemoryArena* parent, void* base_pointer, size_t arena_offset, size_t mapping_size, bool writable); + View(View&& view); + ~View(); + + void* GetBasePointer() const { return m_base_pointer; } + size_t GetArenaOffset() const { return m_arena_offset; } + size_t GetMappingSize() const { return m_mapping_size; } + bool IsWritable() const { return m_writable; } + + private: + MemoryArena* m_parent; + void* m_base_pointer; + size_t m_arena_offset; + size_t m_mapping_size; + bool m_writable; + }; + + MemoryArena(); + ~MemoryArena(); + + static void* FindBaseAddressForMapping(size_t size); + + bool Create(size_t size, bool writable, bool executable); + + std::optional CreateView(size_t offset, size_t size, bool writable, bool executable, + void* fixed_address = nullptr); + + void* CreateViewPtr(size_t offset, size_t size, bool writable, bool executable, void* fixed_address = nullptr); + bool FlushViewPtr(void* address, size_t size); + bool ReleaseViewPtr(void* address, size_t size); + + static bool SetPageProtection(void* address, size_t length, bool readable, bool writable, bool executable); + +private: +#if defined(WIN32) + void* m_file_handle = nullptr; +#elif defined(__linux__) || defined(ANDROID) || defined(__APPLE__) + int m_shmem_fd = -1; +#endif + + std::atomic_size_t m_num_views{0}; + size_t m_size = 0; + bool m_writable = false; + bool m_executable = false; +}; +} // namespace Common diff --git a/src/common/page_fault_handler.cpp b/src/common/page_fault_handler.cpp new file mode 100644 index 000000000..f9fd1686c --- /dev/null +++ b/src/common/page_fault_handler.cpp @@ -0,0 +1,275 @@ +#include "page_fault_handler.h" +#include "common/log.h" +#include +#include +#include +Log_SetChannel(Common::PageFaultHandler); + +#if defined(WIN32) +#include "common/windows_headers.h" +#elif defined(__linux__) || defined(__ANDROID__) +#include +#include +#include +#define USE_SIGSEGV 1 +#elif defined(__APPLE__) +#include +#include +#define USE_SIGSEGV 1 +#endif + +namespace Common::PageFaultHandler { + +struct RegisteredHandler +{ + void* owner; + Callback callback; +}; +static std::vector m_handlers; +static std::mutex m_handler_lock; +static thread_local bool s_in_handler; + +#ifdef __aarch64__ +static bool IsStoreInstruction(const void* ptr) +{ + u32 bits; + std::memcpy(&bits, ptr, sizeof(bits)); + + // Based on vixl's disassembler Instruction::IsStore(). + // if (Mask(LoadStoreAnyFMask) != LoadStoreAnyFixed) + if ((bits & 0x0a000000) != 0x08000000) + return false; + + // if (Mask(LoadStorePairAnyFMask) == LoadStorePairAnyFixed) + if ((bits & 0x3a000000) == 0x28000000) + { + // return Mask(LoadStorePairLBit) == 0 + return (bits & (1 << 22)) == 0; + } + + switch (bits & 0xC4C00000) + { + case 0x00000000: // STRB_w + case 0x40000000: // STRH_w + case 0x80000000: // STR_w + case 0xC0000000: // STR_x + case 0x04000000: // STR_b + case 0x44000000: // STR_h + case 0x84000000: // STR_s + case 0xC4000000: // STR_d + case 0x04800000: // STR_q + return true; + + default: + return false; + } +} +#endif + +#if defined(WIN32) +static PVOID s_veh_handle; + +static LONG ExceptionHandler(PEXCEPTION_POINTERS exi) +{ + if (exi->ExceptionRecord->ExceptionCode != EXCEPTION_ACCESS_VIOLATION || s_in_handler) + return EXCEPTION_CONTINUE_SEARCH; + + s_in_handler = true; + +#if defined(_M_AMD64) + void* const exception_pc = reinterpret_cast(exi->ContextRecord->Rip); +#elif defined(_M_ARM64) + void* const exception_pc = reinterpret_cast(exi->ContextRecord->Pc); +#else + void* const exception_pc = nullptr; +#endif + + void* const exception_address = reinterpret_cast(exi->ExceptionRecord->ExceptionInformation[1]); + bool const is_write = exi->ExceptionRecord->ExceptionInformation[0] == 1; + + std::lock_guard guard(m_handler_lock); + for (const RegisteredHandler& rh : m_handlers) + { + if (rh.callback(exception_pc, exception_address, is_write) == HandlerResult::ContinueExecution) + { + s_in_handler = false; + return EXCEPTION_CONTINUE_EXECUTION; + } + } + + s_in_handler = false; + return EXCEPTION_CONTINUE_SEARCH; +} + +#elif defined(USE_SIGSEGV) + +static struct sigaction s_old_sigsegv_action; +#ifdef __APPLE__ +static struct sigaction s_old_sigbus_action; +#endif + +static void SIGSEGVHandler(int sig, siginfo_t* info, void* ctx) +{ + if ((info->si_code != SEGV_MAPERR && info->si_code != SEGV_ACCERR) || s_in_handler) + return; + +#ifndef __APPLE__ + void* const exception_address = reinterpret_cast(info->si_addr); + +#if defined(__x86_64__) + void* const exception_pc = reinterpret_cast(static_cast(ctx)->uc_mcontext.gregs[REG_RIP]); + const bool is_write = (static_cast(ctx)->uc_mcontext.gregs[REG_ERR] & 2) != 0; +#elif defined(__aarch64__) + void* const exception_pc = reinterpret_cast(static_cast(ctx)->uc_mcontext.pc); + const bool is_write = IsStoreInstruction(exception_pc); +#else + void* const exception_pc = nullptr; + const bool is_write = false; +#endif +#else // __APPLE__ +#if defined(__x86_64__) + void* const exception_address = + reinterpret_cast(static_cast(ctx)->uc_mcontext->__es.__faultvaddr); + void* const exception_pc = reinterpret_cast(static_cast(ctx)->uc_mcontext->__ss.__rip); + const bool is_write = (static_cast(ctx)->uc_mcontext->__es.__err & 2) != 0; +#elif defined(__aarch64__) + void* const exception_address = reinterpret_cast(static_cast(ctx)->uc_mcontext->__es.__far); + void* const exception_pc = reinterpret_cast(static_cast(ctx)->uc_mcontext->__ss.__pc); + const bool is_write = IsStoreInstruction(exception_pc); +#else + void* const exception_address = reinterpret_cast(info->si_addr); + void* const exception_pc = nullptr; + const bool is_write = false; +#endif +#endif // __APPLE__ + + std::lock_guard guard(m_handler_lock); + for (const RegisteredHandler& rh : m_handlers) + { + if (rh.callback(exception_pc, exception_address, is_write) == HandlerResult::ContinueExecution) + { + s_in_handler = false; + return; + } + } + + // call old signal handler +#ifndef __APPLE__ + const struct sigaction& sa = s_old_sigsegv_action; +#else + const struct sigaction& sa = (sig == SIGBUS) ? s_old_sigbus_action : s_old_sigsegv_action; +#endif + if (sa.sa_flags & SA_SIGINFO) + sa.sa_sigaction(sig, info, ctx); + else if (sa.sa_handler == SIG_DFL) + signal(sig, SIG_DFL); + else if (sa.sa_handler == SIG_IGN) + return; + else + sa.sa_handler(sig); +} + +#endif + +bool InstallHandler(void* owner, Callback callback) +{ + bool was_empty; + { + std::lock_guard guard(m_handler_lock); + if (std::find_if(m_handlers.begin(), m_handlers.end(), + [owner](const RegisteredHandler& rh) { return rh.owner == owner; }) != m_handlers.end()) + { + return false; + } + + was_empty = m_handlers.empty(); + m_handlers.push_back(RegisteredHandler{owner, std::move(callback)}); + } + + if (was_empty) + { +#if defined(WIN32) + s_veh_handle = AddVectoredExceptionHandler(1, ExceptionHandler); + if (!s_veh_handle) + { + Log_ErrorPrint("Failed to add vectored exception handler"); + return false; + } +#elif defined(USE_SIGSEGV) +#if 0 + // Alternative stack - we'll need this is we ever use the host stack for branches. + stack_t signal_stack = {}; + signal_stack.ss_sp = malloc(SIGSTKSZ); + signal_stack.ss_size = SIGSTKSZ; + if (sigaltstack(&signal_stack, nullptr)) + { + Log_ErrorPrintf("signaltstack() failed: %d", errno); + return false; + } +#endif + + struct sigaction sa = {}; + sa.sa_sigaction = SIGSEGVHandler; + sa.sa_flags = SA_SIGINFO; + sigemptyset(&sa.sa_mask); + if (sigaction(SIGSEGV, &sa, &s_old_sigsegv_action) < 0) + { + Log_ErrorPrintf("sigaction(SIGSEGV) failed: %d", errno); + return false; + } +#ifdef __APPLE__ + if (sigaction(SIGBUS, &sa, &s_old_sigbus_action) < 0) + { + Log_ErrorPrintf("sigaction(SIGBUS) failed: %d", errno); + return false; + } +#endif + +#else + return false; +#endif + } + + return true; +} + +bool RemoveHandler(void* owner) +{ + std::lock_guard guard(m_handler_lock); + auto it = std::find_if(m_handlers.begin(), m_handlers.end(), + [owner](const RegisteredHandler& rh) { return rh.owner == owner; }); + if (it == m_handlers.end()) + return false; + + m_handlers.erase(it); + + if (m_handlers.empty()) + { +#if defined(WIN32) + RemoveVectoredExceptionHandler(s_veh_handle); + s_veh_handle = nullptr; +#else + // restore old signal handler +#ifdef __APPLE__ + if (sigaction(SIGBUS, &s_old_sigbus_action, nullptr) < 0) + { + Log_ErrorPrintf("sigaction(SIGBUS) failed: %d", errno); + return false; + } + s_old_sigbus_action = {}; +#endif + + if (sigaction(SIGSEGV, &s_old_sigsegv_action, nullptr) < 0) + { + Log_ErrorPrintf("sigaction(SIGSEGV) failed: %d", errno); + return false; + } + + s_old_sigsegv_action = {}; +#endif + } + + return true; +} + +} // namespace Common::PageFaultHandler diff --git a/src/common/page_fault_handler.h b/src/common/page_fault_handler.h new file mode 100644 index 000000000..b2c4f9040 --- /dev/null +++ b/src/common/page_fault_handler.h @@ -0,0 +1,18 @@ +#pragma once +#include "types.h" +#include + +namespace Common::PageFaultHandler { +enum class HandlerResult +{ + ContinueExecution, + ExecuteNextHandler, +}; + +using Callback = std::function; +using Handle = void*; + +bool InstallHandler(void* owner, Callback callback); +bool RemoveHandler(void* owner); + +} // namespace Common::PageFaultHandler