diff --git a/External/FEXCore/Source/Utils/Threads.cpp b/External/FEXCore/Source/Utils/Threads.cpp index b7e3f22b5..2b507a770 100644 --- a/External/FEXCore/Source/Utils/Threads.cpp +++ b/External/FEXCore/Source/Utils/Threads.cpp @@ -17,196 +17,19 @@ #endif namespace FEXCore::Threads { - // Stack pool handling - struct StackPoolItem { - void *Ptr; - size_t Size; - }; - std::mutex DeadStackPoolMutex{}; - std::mutex LiveStackPoolMutex{}; - - static fextl::deque DeadStackPool{}; - static fextl::deque LiveStackPool{}; - - void *AllocateStackObject(size_t Size) { - std::lock_guard lk{DeadStackPoolMutex}; - if (DeadStackPool.size() == 0) { - // Nothing in the pool, just allocate - return FEXCore::Allocator::mmap(nullptr, Size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); - } - - // Keep the first item in the stack pool - auto Result = DeadStackPool.front().Ptr; - DeadStackPool.pop_front(); - - // Erase the rest as a garbage collection step - for (auto &Item : DeadStackPool) { - FEXCore::Allocator::munmap(Item.Ptr, Item.Size); - } - return Result; - } - - void AddStackToDeadPool(void *Ptr, size_t Size) { - std::lock_guard lk{DeadStackPoolMutex}; - DeadStackPool.emplace_back(StackPoolItem{Ptr, Size}); - } - - void AddStackToLivePool(void *Ptr, size_t Size) { - std::lock_guard lk{LiveStackPoolMutex}; - LiveStackPool.emplace_back(StackPoolItem{Ptr, Size}); - } - - void RemoveStackFromLivePool(void *Ptr) { - std::lock_guard lk{LiveStackPoolMutex}; - for (auto it = LiveStackPool.begin(); it != LiveStackPool.end(); ++it) { - if (it->Ptr == Ptr) { - LiveStackPool.erase(it); - return; - } - } - } - - void DeallocateStackObject(void *Ptr, size_t Size) { - RemoveStackFromLivePool(Ptr); - AddStackToDeadPool(Ptr, Size); - } - - void Shutdown() { - std::lock_guard lk{DeadStackPoolMutex}; - std::lock_guard lk2{LiveStackPoolMutex}; - // Erase all the dead stack pools - for (auto &Item : DeadStackPool) { - FEXCore::Allocator::munmap(Item.Ptr, Item.Size); - } - - // Now clean up any that are considered to still be live - // We are in shutdown phase, everything in the process is dead - for (auto &Item : LiveStackPool) { - FEXCore::Allocator::munmap(Item.Ptr, Item.Size); - } - - DeadStackPool.clear(); - LiveStackPool.clear(); - } - - void *InitializeThread(void *Ptr); - - class PThread final : public Thread { - public: - PThread(FEXCore::Threads::ThreadFunc Func, void *Arg) - : UserFunc {Func} - , UserArg {Arg} { - pthread_attr_t Attr{}; - Stack = AllocateStackObject(STACK_SIZE); - // pthreads allocates its dtv region behind our back and there is nothing we can do about it. - FEXCore::Allocator::YesIKnowImNotSupposedToUseTheGlibcAllocator glibc; - AddStackToLivePool(Stack, STACK_SIZE); - pthread_attr_init(&Attr); - pthread_attr_setstack(&Attr, Stack, STACK_SIZE); - // TODO: Thread creation should be using this instead. - // Causes Steam to crash early though. - // pthread_create(&Thread, &Attr, InitializeThread, this); - pthread_create(&Thread, &Attr, Func, Arg); - - pthread_attr_destroy(&Attr); - } - - bool joinable() override { - pthread_attr_t Attr{}; - if (pthread_getattr_np(Thread, &Attr) == 0) { - int AttachState{}; - if (pthread_attr_getdetachstate(&Attr, &AttachState) == 0) { - if (AttachState == PTHREAD_CREATE_JOINABLE) { - return true; - } - } - } - return false; - } - - bool join(void **ret) override { - return pthread_join(Thread, ret) == 0; - } - - bool detach() override { - return pthread_detach(Thread) == 0; - } - - bool IsSelf() override { - auto self = pthread_self(); - return self == Thread; - } - - void *Execute() { - return UserFunc(UserArg); - } - - void FreeStack() { - DeallocateStackObject(Stack, STACK_SIZE); - } - - private: - pthread_t Thread; - FEXCore::Threads::ThreadFunc UserFunc; - void *UserArg; - void *Stack{}; - constexpr static size_t STACK_SIZE = 8 * 1024 * 1024; - }; - - void *InitializeThread(void *Ptr) { - PThread *Thread{reinterpret_cast(Ptr)}; - - // Run the user function - void *Result = Thread->Execute(); - - // Put the stack back in to the stack pool - Thread->FreeStack(); - - // TLS/DTV teardown is something FEX can't control. Disable glibc checking when we leave a pthread. - FEXCore::Allocator::YesIKnowImNotSupposedToUseTheGlibcAllocator::HardDisable(); - - return Result; - } - - fextl::unique_ptr CreateThread_PThread( + fextl::unique_ptr CreateThread_Default( ThreadFunc Func, void* Arg) { - return fextl::make_unique(Func, Arg); + ERROR_AND_DIE_FMT("Frontend didn't setup thread creation!"); } - void CleanupAfterFork_PThread() { - // We don't need to pull the mutex here - // After a fork we are the only thread running - // Just need to make sure not to delete our own stack - uintptr_t StackLocation = reinterpret_cast(alloca(0)); - - auto ClearStackPool = [&](auto &StackPool) { - for (auto it = StackPool.begin(); it != StackPool.end(); ) { - StackPoolItem &Item = *it; - uintptr_t ItemStack = reinterpret_cast(Item.Ptr); - if (ItemStack <= StackLocation && (ItemStack + Item.Size) > StackLocation) { - // This is our stack item, skip it - ++it; - } - else { - // Untracked stack. Clean it up - FEXCore::Allocator::munmap(Item.Ptr, Item.Size); - it = StackPool.erase(it); - } - } - }; - - // Clear both dead stacks and live stacks - ClearStackPool(DeadStackPool); - ClearStackPool(LiveStackPool); - - LogMan::Throw::AFmt((DeadStackPool.size() + LiveStackPool.size()) <= 1, - "After fork we should only have zero or one tracked stacks!"); + void CleanupAfterFork_Default() { + ERROR_AND_DIE_FMT("Frontend didn't setup thread creation!"); } static FEXCore::Threads::Pointers Ptrs = { - .CreateThread = CreateThread_PThread, - .CleanupAfterFork = CleanupAfterFork_PThread, + .CreateThread = CreateThread_Default, + .CleanupAfterFork = CleanupAfterFork_Default, }; fextl::unique_ptr FEXCore::Threads::Thread::Create( diff --git a/External/FEXCore/include/FEXCore/Utils/Threads.h b/External/FEXCore/include/FEXCore/Utils/Threads.h index 656082506..88d4227b2 100644 --- a/External/FEXCore/include/FEXCore/Utils/Threads.h +++ b/External/FEXCore/include/FEXCore/Utils/Threads.h @@ -40,10 +40,6 @@ namespace FEXCore::Threads { static void SetInternalPointers(Pointers const &_Ptrs); }; - void *AllocateStackObject(size_t Size); - void DeallocateStackObject(void *Ptr, size_t Size); - void Shutdown(); - /** * @brief Sets the calling thread's signal mask to the one provided * diff --git a/Source/Tools/FEXLoader/FEXLoader.cpp b/Source/Tools/FEXLoader/FEXLoader.cpp index e92ae7c53..b7a1da36c 100644 --- a/Source/Tools/FEXLoader/FEXLoader.cpp +++ b/Source/Tools/FEXLoader/FEXLoader.cpp @@ -12,6 +12,7 @@ $end_info$ #include "VDSO_Emulation.h" #include "LinuxSyscalls/LinuxAllocator.h" #include "LinuxSyscalls/Syscalls.h" +#include "LinuxSyscalls/Utils/Threads.h" #include "LinuxSyscalls/x32/Syscalls.h" #include "LinuxSyscalls/x64/Syscalls.h" #include "LinuxSyscalls/SignalDelegator.h" @@ -387,6 +388,10 @@ int main(int argc, char **argv, char **const envp) { } FEXCore::Config::EraseSet(FEXCore::Config::CONFIG_APP_CONFIG_NAME, Program.ProgramName); } + + // Setup Thread handlers, so FEXCore can create threads. + FEX::LinuxEmulation::Threads::SetupThreadHandlers(); + FEXCore::Config::EraseSet(FEXCore::Config::CONFIG_IS64BIT_MODE, Loader.Is64BitMode() ? "1" : "0"); fextl::unique_ptr Allocator; @@ -546,7 +551,7 @@ int main(int argc, char **argv, char **const envp) { SyscallHandler.reset(); SignalDelegation.reset(); - FEXCore::Threads::Shutdown(); + FEX::LinuxEmulation::Threads::Shutdown(); Loader.FreeSections(); diff --git a/Source/Tools/FEXLoader/LinuxSyscalls/CMakeLists.txt b/Source/Tools/FEXLoader/LinuxSyscalls/CMakeLists.txt index a34b3600e..5667b29ee 100644 --- a/Source/Tools/FEXLoader/LinuxSyscalls/CMakeLists.txt +++ b/Source/Tools/FEXLoader/LinuxSyscalls/CMakeLists.txt @@ -8,6 +8,7 @@ add_library(LinuxEmulation STATIC Syscalls.cpp SyscallsSMCTracking.cpp SyscallsVMATracking.cpp + Utils/Threads.cpp x32/Syscalls.cpp x32/EPoll.cpp x32/FD.cpp diff --git a/Source/Tools/FEXLoader/LinuxSyscalls/Syscalls.cpp b/Source/Tools/FEXLoader/LinuxSyscalls/Syscalls.cpp index 821fdf21c..8ddbd2277 100644 --- a/Source/Tools/FEXLoader/LinuxSyscalls/Syscalls.cpp +++ b/Source/Tools/FEXLoader/LinuxSyscalls/Syscalls.cpp @@ -13,6 +13,7 @@ $end_info$ #include "LinuxSyscalls/SignalDelegator.h" #include "LinuxSyscalls/Syscalls.h" #include "LinuxSyscalls/Syscalls/Thread.h" +#include "LinuxSyscalls/Utils/Threads.h" #include "LinuxSyscalls/x32/Syscalls.h" #include "LinuxSyscalls/x64/Syscalls.h" #include "LinuxSyscalls/x32/Types.h" @@ -29,7 +30,6 @@ $end_info$ #include #include #include -#include #include #include #include @@ -426,7 +426,7 @@ struct StackFramePlusRet { static void Clone3HandlerRet() { StackFrameData *Data = (StackFrameData*)alloca(0); uint64_t Result = FEX::HLE::HandleNewClone(Data->Thread, Data->CTX, &Data->NewFrame, &Data->GuestArgs); - FEXCore::Threads::DeallocateStackObject(Data->NewStack, Data->StackSize); + FEX::LinuxEmulation::Threads::DeallocateStackObject(Data->NewStack); // To behave like a real clone, we now just need to call exit here exit(Result); FEX_UNREACHABLE; @@ -435,7 +435,7 @@ static void Clone3HandlerRet() { static int Clone2HandlerRet(void *arg) { StackFrameData *Data = (StackFrameData*)arg; uint64_t Result = FEX::HLE::HandleNewClone(Data->Thread, Data->CTX, &Data->NewFrame, &Data->GuestArgs); - FEXCore::Threads::DeallocateStackObject(Data->NewStack, Data->StackSize); + FEX::LinuxEmulation::Threads::DeallocateStackObject(Data->NewStack); FEXCore::Allocator::free(arg); return Result; } @@ -489,8 +489,8 @@ static uint64_t Clone2Handler(FEXCore::Core::CpuStateFrame *Frame, FEX::HLE::clo Data->GuestArgs = *args; // In the case of thread, we need a new stack - Data->StackSize = 8 * 1024 * 1024; - Data->NewStack = FEXCore::Threads::AllocateStackObject(Data->StackSize); + Data->StackSize = FEX::LinuxEmulation::Threads::STACK_SIZE; + Data->NewStack = FEX::LinuxEmulation::Threads::AllocateStackObject(); // Create a copy of the parent frame memcpy(&Data->NewFrame, Frame, sizeof(FEXCore::Core::CpuStateFrame)); @@ -514,8 +514,8 @@ static uint64_t Clone2Handler(FEXCore::Core::CpuStateFrame *Frame, FEX::HLE::clo static uint64_t Clone3Handler(FEXCore::Core::CpuStateFrame *Frame, FEX::HLE::clone3_args *args) { // In the case of thread, we need a new stack - uint64_t StackSize = 8 * 1024 * 1024; - void *NewStack = FEXCore::Threads::AllocateStackObject(StackSize); + const uint64_t StackSize = FEX::LinuxEmulation::Threads::STACK_SIZE; + void *NewStack = FEX::LinuxEmulation::Threads::AllocateStackObject(); constexpr size_t Offset = sizeof(StackFramePlusRet); StackFramePlusRet *Data = (StackFramePlusRet*)(reinterpret_cast(NewStack) + StackSize - Offset); diff --git a/Source/Tools/FEXLoader/LinuxSyscalls/Utils/Threads.cpp b/Source/Tools/FEXLoader/LinuxSyscalls/Utils/Threads.cpp new file mode 100644 index 000000000..c4761b0d7 --- /dev/null +++ b/Source/Tools/FEXLoader/LinuxSyscalls/Utils/Threads.cpp @@ -0,0 +1,205 @@ +#include "LinuxSyscalls/Utils/Threads.h" + +#include +#include + +#include + +namespace FEX::LinuxEmulation::Threads { + // Stack pool handling + struct StackPoolItem { + void *Ptr; + size_t Size; + }; + std::mutex DeadStackPoolMutex{}; + std::mutex LiveStackPoolMutex{}; + + static fextl::deque DeadStackPool{}; + static fextl::deque LiveStackPool{}; + + void *AllocateStackObject() { + std::lock_guard lk{DeadStackPoolMutex}; + if (DeadStackPool.size() == 0) { + // Nothing in the pool, just allocate + return FEXCore::Allocator::mmap(nullptr, FEX::LinuxEmulation::Threads::STACK_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + } + + // Keep the first item in the stack pool + auto Result = DeadStackPool.front().Ptr; + DeadStackPool.pop_front(); + + // Erase the rest as a garbage collection step + for (auto &Item : DeadStackPool) { + FEXCore::Allocator::munmap(Item.Ptr, Item.Size); + } + return Result; + } + + void AddStackToDeadPool(void *Ptr) { + std::lock_guard lk{DeadStackPoolMutex}; + DeadStackPool.emplace_back(StackPoolItem{Ptr, FEX::LinuxEmulation::Threads::STACK_SIZE}); + } + + void AddStackToLivePool(void *Ptr) { + std::lock_guard lk{LiveStackPoolMutex}; + LiveStackPool.emplace_back(StackPoolItem{Ptr, FEX::LinuxEmulation::Threads::STACK_SIZE}); + } + + void RemoveStackFromLivePool(void *Ptr) { + std::lock_guard lk{LiveStackPoolMutex}; + for (auto it = LiveStackPool.begin(); it != LiveStackPool.end(); ++it) { + if (it->Ptr == Ptr) { + LiveStackPool.erase(it); + return; + } + } + } + + void DeallocateStackObject(void *Ptr) { + RemoveStackFromLivePool(Ptr); + AddStackToDeadPool(Ptr); + } + + namespace PThreads { + void *InitializeThread(void *Ptr); + + class PThread final : public FEXCore::Threads::Thread { + public: + PThread(FEXCore::Threads::ThreadFunc Func, void *Arg) + : UserFunc {Func} + , UserArg {Arg} { + pthread_attr_t Attr{}; + Stack = AllocateStackObject(); + // pthreads allocates its dtv region behind our back and there is nothing we can do about it. + FEXCore::Allocator::YesIKnowImNotSupposedToUseTheGlibcAllocator glibc; + AddStackToLivePool(Stack); + pthread_attr_init(&Attr); + pthread_attr_setstack(&Attr, Stack, FEX::LinuxEmulation::Threads::STACK_SIZE); + // TODO: Thread creation should be using this instead. + // Causes Steam to crash early though. + // pthread_create(&Thread, &Attr, InitializeThread, this); + pthread_create(&Thread, &Attr, Func, Arg); + + pthread_attr_destroy(&Attr); + } + + bool joinable() override { + pthread_attr_t Attr{}; + if (pthread_getattr_np(Thread, &Attr) == 0) { + int AttachState{}; + if (pthread_attr_getdetachstate(&Attr, &AttachState) == 0) { + if (AttachState == PTHREAD_CREATE_JOINABLE) { + return true; + } + } + } + return false; + } + + bool join(void **ret) override { + return pthread_join(Thread, ret) == 0; + } + + bool detach() override { + return pthread_detach(Thread) == 0; + } + + bool IsSelf() override { + auto self = pthread_self(); + return self == Thread; + } + + void *Execute() { + return UserFunc(UserArg); + } + + void FreeStack() { + DeallocateStackObject(Stack); + } + + private: + pthread_t Thread; + FEXCore::Threads::ThreadFunc UserFunc; + void *UserArg; + void *Stack{}; + }; + + void *InitializeThread(void *Ptr) { + PThread *Thread{reinterpret_cast(Ptr)}; + + // Run the user function + void *Result = Thread->Execute(); + + // Put the stack back in to the stack pool + Thread->FreeStack(); + + // TLS/DTV teardown is something FEX can't control. Disable glibc checking when we leave a pthread. + FEXCore::Allocator::YesIKnowImNotSupposedToUseTheGlibcAllocator::HardDisable(); + + return Result; + } + + fextl::unique_ptr CreateThread_PThread( + FEXCore::Threads::ThreadFunc Func, + void* Arg) { + return fextl::make_unique(Func, Arg); + } + + void CleanupAfterFork_PThread() { + // We don't need to pull the mutex here + // After a fork we are the only thread running + // Just need to make sure not to delete our own stack + uintptr_t StackLocation = reinterpret_cast(alloca(0)); + + auto ClearStackPool = [&](auto &StackPool) { + for (auto it = StackPool.begin(); it != StackPool.end(); ) { + StackPoolItem &Item = *it; + uintptr_t ItemStack = reinterpret_cast(Item.Ptr); + if (ItemStack <= StackLocation && (ItemStack + Item.Size) > StackLocation) { + // This is our stack item, skip it + ++it; + } + else { + // Untracked stack. Clean it up + FEXCore::Allocator::munmap(Item.Ptr, Item.Size); + it = StackPool.erase(it); + } + } + }; + + // Clear both dead stacks and live stacks + ClearStackPool(DeadStackPool); + ClearStackPool(LiveStackPool); + + LogMan::Throw::AFmt((DeadStackPool.size() + LiveStackPool.size()) <= 1, + "After fork we should only have zero or one tracked stacks!"); + } + }; + + void SetupThreadHandlers() { + FEXCore::Threads::Pointers Ptrs = { + .CreateThread = PThreads::CreateThread_PThread, + .CleanupAfterFork = PThreads::CleanupAfterFork_PThread, + }; + + FEXCore::Threads::Thread::SetInternalPointers(Ptrs); + } + + void Shutdown() { + std::lock_guard lk{DeadStackPoolMutex}; + std::lock_guard lk2{LiveStackPoolMutex}; + // Erase all the dead stack pools + for (auto &Item : DeadStackPool) { + FEXCore::Allocator::munmap(Item.Ptr, Item.Size); + } + + // Now clean up any that are considered to still be live + // We are in shutdown phase, everything in the process is dead + for (auto &Item : LiveStackPool) { + FEXCore::Allocator::munmap(Item.Ptr, Item.Size); + } + + DeadStackPool.clear(); + LiveStackPool.clear(); + } +} diff --git a/Source/Tools/FEXLoader/LinuxSyscalls/Utils/Threads.h b/Source/Tools/FEXLoader/LinuxSyscalls/Utils/Threads.h new file mode 100644 index 000000000..c4ceeffe2 --- /dev/null +++ b/Source/Tools/FEXLoader/LinuxSyscalls/Utils/Threads.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +namespace FEX::LinuxEmulation::Threads { + /** + * @brief Size of the stack that this interface creates. + */ + constexpr size_t STACK_SIZE = 8 * 1024 * 1024; + + /** + * @brief Allocates a stack object from the internally managed stack pool. + */ + void *AllocateStackObject(); + + /** + * @brief Deallocates a stack from the internally managed stack pool. + * + * Will not free the memory immediately, instead saving for reuse temporarily to solve race conditions on stack usage while stack tears down. + * + * @param Ptr The stack base from `AllocateStackObject` + */ + void DeallocateStackObject(void *Ptr); + + /** + * @brief Registers thread creation handlers with FEXCore. + */ + void SetupThreadHandlers(); + + /** + * @brief Cleans up any remaining stack objects in the pools. + */ + void Shutdown(); +}