From c430f10c86ad911af68a0629c925eca6457d6345 Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Tue, 17 Nov 2020 16:55:12 -0800 Subject: [PATCH] Implements a threaded compile service routine This is made to be completely synchronous to a thread --- External/FEXCore/Source/CMakeLists.txt | 1 + .../Source/Interface/Context/Context.h | 1 + .../Source/Interface/Core/CompileService.cpp | 178 ++++++++++++++++++ .../Source/Interface/Core/CompileService.h | 58 ++++++ .../FEXCore/Source/Interface/Core/Core.cpp | 64 +++++-- .../FEXCore/Debug/InternalThreadState.h | 5 +- 6 files changed, 293 insertions(+), 14 deletions(-) create mode 100644 External/FEXCore/Source/Interface/Core/CompileService.cpp create mode 100644 External/FEXCore/Source/Interface/Core/CompileService.h diff --git a/External/FEXCore/Source/CMakeLists.txt b/External/FEXCore/Source/CMakeLists.txt index 6de0d03b3..3cb81f2e3 100644 --- a/External/FEXCore/Source/CMakeLists.txt +++ b/External/FEXCore/Source/CMakeLists.txt @@ -120,6 +120,7 @@ set (SRCS Interface/Context/Context.cpp Interface/Core/BlockCache.cpp Interface/Core/BlockSamplingData.cpp + Interface/Core/CompileService.cpp Interface/Core/Core.cpp Interface/Core/CPUID.cpp Interface/Core/Frontend.cpp diff --git a/External/FEXCore/Source/Interface/Context/Context.h b/External/FEXCore/Source/Interface/Context/Context.h index 5e8c59f14..69dfcdef6 100644 --- a/External/FEXCore/Source/Interface/Context/Context.h +++ b/External/FEXCore/Source/Interface/Context/Context.h @@ -139,6 +139,7 @@ namespace FEXCore::Context { FEXCore::Core::ThreadState *GetThreadState(); void LoadEntryList(); + void *CompileCode(FEXCore::Core::InternalThreadState *Thread, uint64_t GuestRIP); uintptr_t CompileBlock(FEXCore::Core::InternalThreadState *Thread, uint64_t GuestRIP); uintptr_t CompileFallbackBlock(FEXCore::Core::InternalThreadState *Thread, uint64_t GuestRIP); diff --git a/External/FEXCore/Source/Interface/Core/CompileService.cpp b/External/FEXCore/Source/Interface/Core/CompileService.cpp new file mode 100644 index 000000000..d16c3deab --- /dev/null +++ b/External/FEXCore/Source/Interface/Core/CompileService.cpp @@ -0,0 +1,178 @@ +#include "Interface/Context/Context.h" +#include "Interface/Core/BlockCache.h" +#include "Interface/Core/CompileService.h" +#include "Interface/Core/InternalThreadState.h" +#include "Interface/Core/OpcodeDispatcher.h" +#include "Interface/Core/SignalDelegator.h" + +namespace FEXCore { + CompileService::CompileService(FEXCore::Context::Context *ctx, FEXCore::Core::InternalThreadState *Thread) + : CTX {ctx} + , ParentThread {Thread} { + + CompileThreadData = std::make_unique(); + CompileThreadData->IsCompileService = true; + + // We need a compiler for this work thread + CTX->InitializeCompiler(CompileThreadData.get(), true); + CompileThreadData->CPUBackend->CopyNecessaryDataForCompileThread(ParentThread->CPUBackend.get()); + + WorkerThread = std::thread([this]() { + ExecutionThread(); + }); + } + + void CompileService::Initialize() { + // Share CompileService which = this + CompileThreadData->CompileService = ParentThread->CompileService; + } + + void CompileService::Shutdown() { + ShuttingDown = true; + // Kick the working thread + StartWork.NotifyAll(); + WorkerThread.join(); + } + + void CompileService::ClearCache(FEXCore::Core::InternalThreadState *Thread, uint64_t GuestRIP) { + // On cache clear we need to spin down the execution thread to ensure it isn't trying to give us more work items + if (CompileMutex.try_lock()) { + // We can only clear these things if we pulled the compile mutex + + // Grab the work queue and clear it + // We don't need to grab the queue mutex since this thread will no longer receive any work events + // Threads are bounded 1:1 + while (WorkQueue.size()) { + WorkItem *Item = WorkQueue.front(); + WorkQueue.pop(); + delete Item; + } + + // Go through the garbage collection array and clear it + // It's safe to clear things that aren't marked safe since we are clearing cache + if (GCArray.size()) { + // Clean up our GC array + for (auto it = GCArray.begin(); it != GCArray.end();) { + delete *it; + it = GCArray.erase(it); + } + } + + if (GuestRIP == 0) { + CompileThreadData->IRLists.clear(); + } + else { + auto IR = CompileThreadData->IRLists.find(GuestRIP)->second.release(); + CompileThreadData->IRLists.clear(); + CompileThreadData->IRLists.try_emplace(GuestRIP, IR); + } + + CompileMutex.unlock(); + } + + // Clear the inverse cache of what is calling us from the Context ClearCache routine + auto SelectedThread = Thread->IsCompileService ? ParentThread : Thread; + SelectedThread->BlockCache->ClearCache(); + SelectedThread->CPUBackend->ClearCache(); + SelectedThread->IntBackend->ClearCache(); + } + + void CompileService::RemoveCodeEntry(uint64_t GuestRIP) { + CompileThreadData->IRLists.erase(GuestRIP); + CompileThreadData->DebugData.erase(GuestRIP); + CompileThreadData->BlockCache->Erase(GuestRIP); + } + + CompileService::WorkItem *CompileService::CompileCode(uint64_t RIP) { + // Tell the worker thread to compile code for us + WorkItem *Item = new WorkItem{}; + Item->RIP = RIP; + + { + // Fill the threads work queue + std::scoped_lock lk(QueueMutex); + WorkQueue.emplace(Item); + } + + // Notify the thread that it has more work + StartWork.NotifyAll(); + + return Item; + } + + void CompileService::ExecutionThread() { + // Ignore signals coming from the guest + CTX->SignalDelegation.MaskThreadSignals(); + + // Set our thread name so we can see its relation + char ThreadName[16]{}; + snprintf(ThreadName, 16, "%ld-CS", ParentThread->State.ThreadManager.TID.load()); + pthread_setname_np(pthread_self(), ThreadName); + + while (true) { + // Wait for work + StartWork.Wait(); + if (ShuttingDown.load()) { + break; + } + std::scoped_lock lk(CompileMutex); + + size_t WorkItems{}; + + do { + // Grab a work item + WorkItem *Item{}; + { + std::scoped_lock lk(QueueMutex); + WorkItems = WorkQueue.size(); + if (WorkItems) { + Item = WorkQueue.front(); + WorkQueue.pop(); + } + } + + // If we had a work item then work on it + if (Item) { + // Does the block cache already contain this RIP? + void *CompiledCode = reinterpret_cast(CompileThreadData->BlockCache->FindBlock(Item->RIP)); + if (!CompiledCode) { + // Code isn't in cache, compile now + // Set our thread state's RIP + CompileThreadData->State.State.rip = Item->RIP; + CompiledCode = CTX->CompileCode(CompileThreadData.get(), Item->RIP); + } + + if (!CompiledCode) { + // XXX: We currently have the expectation that compile service code will be significantly smaller than regular thread's code + ERROR_AND_DIE("Couldn't compile code for thread at RIP: 0x%lx", Item->RIP); + } + + auto BlockMapPtr = CompileThreadData->BlockCache->AddBlockMapping(Item->RIP, CompiledCode); + if (BlockMapPtr == 0) { + // XXX: We currently have the expectation that compiler service block cache will be significantly underutilized compared to regular thread + ERROR_AND_DIE("Couldn't add code to block cache for thread at RIP: 0x%lx", Item->RIP); + } + + Item->CodePtr = CompiledCode; + Item->IRList = CompileThreadData->IRLists.find(Item->RIP)->second.get(); + + GCArray.emplace_back(Item); + Item->ServiceWorkDone.NotifyAll(); + } + } while (WorkItems != 0); + + if (GCArray.size()) { + // Clean up our GC array + for (auto it = GCArray.begin(); it != GCArray.end();) { + if ((*it)->SafeToClear) { + delete *it; + it = GCArray.erase(it); + } + else { + ++it; + } + } + } + } + } +} diff --git a/External/FEXCore/Source/Interface/Core/CompileService.h b/External/FEXCore/Source/Interface/Core/CompileService.h new file mode 100644 index 000000000..a2ee7dbef --- /dev/null +++ b/External/FEXCore/Source/Interface/Core/CompileService.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace FEXCore { +namespace Context { + struct Context; +} +namespace Core { + struct InternalThreadState; +} + +class CompileService final { + public: + CompileService(FEXCore::Context::Context *ctx, FEXCore::Core::InternalThreadState *Thread); + void Initialize(); + void Shutdown(); + + struct WorkItem { + // Incoming + uint64_t RIP{}; + + // Outgoing + void *CodePtr{}; + FEXCore::IR::IRListView *IRList{}; + FEXCore::Core::DebugData *DebugData{}; + + // Communication + Event ServiceWorkDone{}; + std::atomic_bool SafeToClear{}; + }; + + WorkItem *CompileCode(uint64_t RIP); + void ClearCache(FEXCore::Core::InternalThreadState *Thread, uint64_t GuestRIP); + void RemoveCodeEntry(uint64_t GuestRIP); + + private: + FEXCore::Context::Context *CTX; + FEXCore::Core::InternalThreadState *ParentThread; + + void ExecutionThread(); + std::thread WorkerThread; + std::unique_ptr CompileThreadData; + + std::mutex QueueMutex{}; + std::mutex CompileMutex{}; + std::queue WorkQueue{}; + std::vector GCArray{}; + Event StartWork{}; + std::atomic_bool ShuttingDown{false}; +}; +} diff --git a/External/FEXCore/Source/Interface/Core/Core.cpp b/External/FEXCore/Source/Interface/Core/Core.cpp index a8f0b286f..6367cba88 100644 --- a/External/FEXCore/Source/Interface/Core/Core.cpp +++ b/External/FEXCore/Source/Interface/Core/Core.cpp @@ -4,6 +4,7 @@ #include "Interface/Context/Context.h" #include "Interface/Core/BlockCache.h" #include "Interface/Core/BlockSamplingData.h" +#include "Interface/Core/CompileService.h" #include "Interface/Core/Core.h" #include "Interface/Core/DebugData.h" #include "Interface/Core/OpcodeDispatcher.h" @@ -221,6 +222,10 @@ namespace FEXCore::Context { } for (auto &Thread : Threads) { + + if (Thread->CompileService) { + Thread->CompileService->Shutdown(); + } delete Thread; } Threads.clear(); @@ -587,9 +592,12 @@ namespace FEXCore::Context { // Pull out the current IR we added and store it back after we cleared the rest of the list // Needed in the case the the block mapping has aliased - auto IR = Thread->IRLists.find(Address)->second.release(); - Thread->IRLists.clear(); - Thread->IRLists.try_emplace(Address, IR); + auto iter = Thread->IRLists.find(Address); + if (iter != Thread->IRLists.end()) { + auto IR = iter->second.release(); + Thread->IRLists.clear(); + Thread->IRLists.try_emplace(Address, IR); + } BlockMapPtr = Thread->BlockCache->AddBlockMapping(Address, Ptr); LogMan::Throw::A(BlockMapPtr, "Couldn't add mapping after clearing mapping cache"); } @@ -601,7 +609,11 @@ namespace FEXCore::Context { Thread->BlockCache->ClearCache(); Thread->CPUBackend->ClearCache(); Thread->IntBackend->ClearCache(); - + + if (Thread->CompileService) { + Thread->CompileService->ClearCache(Thread, GuestRIP); + } + if (GuestRIP == 0) { Thread->IRLists.clear(); } @@ -612,10 +624,9 @@ namespace FEXCore::Context { } } - uintptr_t Context::CompileBlock(FEXCore::Core::InternalThreadState *Thread, uint64_t GuestRIP) { - void *CodePtr {nullptr}; + void *Context::CompileCode(FEXCore::Core::InternalThreadState *Thread, uint64_t GuestRIP) { uint8_t const *GuestCode{}; - if (Thread->CTX->Config.UnifiedMemory) { + if (Config.UnifiedMemory) { GuestCode = reinterpret_cast(GuestRIP); } else { @@ -638,7 +649,7 @@ namespace FEXCore::Context { LogMan::Msg::E("Had Frontend decoder error"); Stop(false /* Ignore Current Thread */); } - return 0; + return nullptr; } auto CodeBlocks = Thread->FrontendDecoder->GetDecodedBlocks(); @@ -671,7 +682,7 @@ namespace FEXCore::Context { else { ExistingCodePtr = MemoryMapper.GetPointer(Block.Entry + BlockInstructionsLength); } - + memcpy(&existing, (void*)(ExistingCodePtr), DecodedInfo->InstSize); auto CodeChanged = Thread->OpDispatcher->_ValidateCode(existing, ExistingCodePtr, DecodedInfo->InstSize); @@ -684,9 +695,9 @@ namespace FEXCore::Context { Thread->OpDispatcher->_RemoveCodeEntry(GuestRIP); Thread->OpDispatcher->_StoreContext(IR::GPRClass, 8, offsetof(FEXCore::Core::CPUState, rip), Thread->OpDispatcher->_Constant(Block.Entry + BlockInstructionsLength)); Thread->OpDispatcher->_ExitFunction(); - + auto NextOpBlock = Thread->OpDispatcher->CreateNewCodeBlock(); - + Thread->OpDispatcher->SetFalseJumpTarget(InvalidateCodeCond, NextOpBlock); Thread->OpDispatcher->SetCurrentCodeBlock(NextOpBlock); } @@ -717,7 +728,7 @@ namespace FEXCore::Context { if (TotalInstructions == 0) { // Couldn't handle any instruction in op dispatcher Thread->OpDispatcher->ResetWorkingList(); - return 0; + return nullptr; } else { uint8_t GPRSize = Config.Is64BitMode ? 8 : 4; @@ -808,7 +819,28 @@ namespace FEXCore::Context { } // Attempt to get the CPU backend to compile this code - CodePtr = Thread->CPUBackend->CompileCode(IRList, DebugData); + return Thread->CPUBackend->CompileCode(IRList, DebugData); + } + + uintptr_t Context::CompileBlock(FEXCore::Core::InternalThreadState *Thread, uint64_t GuestRIP) { + if (Thread->CompileBlockReentrantRefCount != 0) { + if (!Thread->CompileService) { + Thread->CompileService = std::make_shared(this, Thread); + Thread->CompileService->Initialize(); + } + auto WorkItem = Thread->CompileService->CompileCode(GuestRIP); + WorkItem->ServiceWorkDone.Wait(); + // Return here with the data in place + Thread->IRLists.try_emplace(GuestRIP, WorkItem->IRList->CreateCopy()); + uintptr_t Ptr = AddBlockMapping(Thread, GuestRIP, WorkItem->CodePtr); + LogMan::Throw::A(Ptr, "Couldn't add mapping from compile service!"); + WorkItem->SafeToClear = true; + return Ptr; + } + + ++Thread->CompileBlockReentrantRefCount; + + void *CodePtr = CompileCode(Thread, GuestRIP); if (CodePtr != nullptr) { // The core managed to compile the code. @@ -816,9 +848,11 @@ namespace FEXCore::Context { Symbols.Register(CodePtr, GuestRIP, DebugData->HostCodeSize); #endif + --Thread->CompileBlockReentrantRefCount; return AddBlockMapping(Thread, GuestRIP, CodePtr); } + --Thread->CompileBlockReentrantRefCount; return 0; } @@ -889,6 +923,10 @@ namespace FEXCore::Context { Thread->IRLists.erase(GuestRIP); Thread->DebugData.erase(GuestRIP); Thread->BlockCache->Erase(GuestRIP); + + if (Thread->CompileService) { + Thread->CompileService->RemoveCodeEntry(GuestRIP); + } } // Debug interface diff --git a/External/FEXCore/include/FEXCore/Debug/InternalThreadState.h b/External/FEXCore/include/FEXCore/Debug/InternalThreadState.h index 09fe09e8b..4441fa264 100644 --- a/External/FEXCore/include/FEXCore/Debug/InternalThreadState.h +++ b/External/FEXCore/include/FEXCore/Debug/InternalThreadState.h @@ -9,6 +9,7 @@ namespace FEXCore { class BlockCache; + class CompileService; } namespace FEXCore::Context { @@ -79,7 +80,9 @@ namespace FEXCore::Core { int StatusCode{}; FEXCore::Context::ExitReason ExitReason {FEXCore::Context::ExitReason::EXIT_WAITING}; - + uint32_t CompileBlockReentrantRefCount{}; + std::shared_ptr CompileService; + bool IsCompileService{false}; }; static_assert(offsetof(InternalThreadState, State) == 0, "InternalThreadState must have State be the first object"); static_assert(std::is_standard_layout::value, "This needs to be standard layout");