From afb1e1d1e1bc5afdb79860d2e33af3abc761303c Mon Sep 17 00:00:00 2001 From: Christian Speckner Date: Thu, 7 Jun 2018 20:43:56 +0200 Subject: [PATCH] Emulation worker. Currently untested and disconnected. --- .vscode/settings.json | 11 +- src/emucore/EmulationWorker.cxx | 256 ++++++++++++++++++++++++++++++++ src/emucore/EmulationWorker.hxx | 92 ++++++++++++ src/emucore/module.mk | 1 + 4 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 src/emucore/EmulationWorker.cxx create mode 100644 src/emucore/EmulationWorker.hxx diff --git a/.vscode/settings.json b/.vscode/settings.json index 790c9dc4d..a067b748d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -43,6 +43,15 @@ "stdexcept": "cpp", "fstream": "cpp", "__locale": "cpp", - "__string": "cpp" + "__string": "cpp", + "__config": "cpp", + "__nullptr": "cpp", + "cstddef": "cpp", + "exception": "cpp", + "initializer_list": "cpp", + "new": "cpp", + "typeinfo": "cpp", + "__mutex_base": "cpp", + "mutex": "cpp" } } diff --git a/src/emucore/EmulationWorker.cxx b/src/emucore/EmulationWorker.cxx new file mode 100644 index 000000000..504877ee1 --- /dev/null +++ b/src/emucore/EmulationWorker.cxx @@ -0,0 +1,256 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2018 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#include + +#include "EmulationWorker.hxx" +#include "DispatchResult.hxx" +#include "TIA.hxx" + +using namespace std::chrono; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +EmulationWorker::EmulationWorker(TIA& tia) : myTia(tia) +{ + std::mutex mutex; + std::unique_lock lock(mutex); + std::condition_variable threadInitialized; + + myThread = std::thread( + &EmulationWorker::threadMain, this, &threadInitialized, &mutex + ); + + // Wait until the thread has acquired myMutex and moved on + while (myState == State::initializing) threadInitialized.wait(lock); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +EmulationWorker::~EmulationWorker() +{ + // This has to run in a block in order to release the mutex before joining + { + std::unique_lock lock(myMutex); + + if (myState != State::exception) { + myPendingSignal = Signal::quit; + mySignalCondition.notify_one(); + } + } + + myThread.join(); + + handlePossibleException(); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void EmulationWorker::handlePossibleException() +{ + if (myState == State::exception && myPendingException) { + std::exception_ptr ex = myPendingException; + // Make sure that the exception is not thrown a second time (destructor!!!) + myPendingException = nullptr; + + std::rethrow_exception(ex); + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void EmulationWorker::start(uInt32 cyclesPerSecond, uInt32 maxCycles, uInt32 minCycles, DispatchResult* dispatchResult) +{ + // Optimization: run this in a block to unlock the mutex before notifying the thread; + // otherwise, the thread would immediatelly block again until we have released the mutex, + // sacrificing a timeslice + { + // Aquire the mutex -> wait until the thread is suspended + std::unique_lock lock(myMutex); + + // Pass on possible exceptions + handlePossibleException(); + + // NB: The thread does not suspend execution in State::initialized + if (myState != State::waitingForResume) + throw runtime_error("start called on running or dead worker"); + + // Store the parameters for emulation + myCyclesPerSecond = cyclesPerSecond; + myMaxCycles = maxCycles; + myMinCycles = minCycles; + myDispatchResult = dispatchResult; + + // Set the signal... + myPendingSignal = Signal::resume; + } + + // ... and wakeup the thread + mySignalCondition.notify_one(); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void EmulationWorker::stop() +{ + // See EmulationWorker::start for the gory details + { + std::unique_lock lock(myMutex); + handlePossibleException(); + + // If the worker has stopped on its own, we return + if (myState == State::waitingForResume) return; + + // NB: The thread does not suspend execution in State::initialized or State::running + if (myState != State::waitingForStop) + throw runtime_error("stop called on a dead worker"); + + myPendingSignal = Signal::stop; + } + + mySignalCondition.notify_one(); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void EmulationWorker::threadMain(std::condition_variable* initializedCondition, std::mutex* initializationMutex) +{ + std::unique_lock lock(myMutex); + + try { + // Optimization: release the lock before notifying our parent -> saves a timeslice + { + // Wait until our parent releases the lock and sleeps + std::lock_guard guard(*initializationMutex); + + // Update the state... + myState = State::initialized; + } + + // ... and wake up our parent to notifiy that we have initialized. From this point, the + // parent can safely assume that we are running while the mutex is locked. + initializedCondition->notify_one(); + + while (myPendingSignal != Signal::quit) handleWakeup(lock); + } + catch (...) { + myPendingException = std::current_exception(); + myState = State::exception; + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void EmulationWorker::handleWakeup(std::unique_lock& lock) +{ + switch (myState) { + case State::initialized: + myState = State::waitingForResume; + mySignalCondition.wait(lock); + break; + + case State::waitingForResume: + handleWakeupFromWaitingForResume(lock); + break; + + case State::waitingForStop: + handleWakeupFromWaitingForStop(lock); + break; + + default: + throw runtime_error("wakeup in invalid worker state"); + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void EmulationWorker::handleWakeupFromWaitingForResume(std::unique_lock& lock) +{ + switch (myPendingSignal) { + case Signal::resume: + myPendingSignal = Signal::none; + dispatchEmulation(lock); + break; + + case Signal::none: + mySignalCondition.wait(lock); + break; + + case Signal::quit: + break; + + default: + throw runtime_error("invalid signal while waiting for resume"); + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void EmulationWorker::handleWakeupFromWaitingForStop(std::unique_lock& lock) +{ + switch (myPendingSignal) { + case Signal::stop: + myState = State::waitingForResume; + myPendingSignal = Signal::none; + + mySignalCondition.wait(lock); + break; + + case Signal::none: + if (myWakeupPoint <= high_resolution_clock::now()) + mySignalCondition.wait_until(lock, myWakeupPoint); + else { + myState = State::waitingForResume; + mySignalCondition.wait(lock); + } + + break; + + case Signal::quit: + break; + + default: + throw runtime_error("invalid signal while waiting for stop"); + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void EmulationWorker::dispatchEmulation(std::unique_lock& lock) +{ + myState = State::running; + + time_point now = high_resolution_clock::now(); + uInt64 totalCycles = 0; + + do { + myTia.update(*myDispatchResult, totalCycles > 0 ? myMinCycles - totalCycles : myMaxCycles); + } while (totalCycles < myMinCycles && myDispatchResult->getStatus() == DispatchResult::Status::ok); + + if (myDispatchResult->getStatus() == DispatchResult::Status::ok) { + // If emulation finished successfully, we can go for another round + duration timesliceSeconds(static_cast(totalCycles) / static_cast(myCyclesPerSecond)); + myWakeupPoint = now + duration_cast(timesliceSeconds); + + myState = State::waitingForStop; + + if (myWakeupPoint > high_resolution_clock::now()) + // If we can keep up with the emulation, we sleep + mySignalCondition.wait_until(lock, myWakeupPoint); + else { + // If we are already lagging behind, we briefly relinquish control over the mutex + // and yield to scheduler, to make sure that the main thread has a chance to stop us + lock.release(); + std::this_thread::yield(); + lock.lock(); + } + } else { + // If execution trapped, we stop immediatelly. + myState = State::waitingForResume; + mySignalCondition.wait(lock); + } +} diff --git a/src/emucore/EmulationWorker.hxx b/src/emucore/EmulationWorker.hxx new file mode 100644 index 000000000..227081973 --- /dev/null +++ b/src/emucore/EmulationWorker.hxx @@ -0,0 +1,92 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2018 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#ifndef EMULATION_WORKER_HXX +#define EMULATION_WORKER_HXX + +#include +#include +#include +#include +#include +#include + +#include "bspf.hxx" + +class TIA; +class DispatchResult; + +class EmulationWorker +{ + public: + enum class State { + initializing, initialized, waitingForResume, running, waitingForStop, exception + }; + + enum class Signal { + resume, stop, quit, none + }; + + public: + + EmulationWorker(TIA& tia); + + ~EmulationWorker(); + + void handlePossibleException(); + + void start(uInt32 cyclesPerSecond, uInt32 maxCycles, uInt32 minCycles, DispatchResult* dispatchResult); + + void stop(); + + private: + + void handleException(); + + // Passing references into a thread is awkward and requires std::ref -> use pointers here + void threadMain(std::condition_variable* initializedCondition, std::mutex* initializationMutex); + + void handleWakeup(std::unique_lock& lock); + + void handleWakeupFromWaitingForResume(std::unique_lock& lock); + + void handleWakeupFromWaitingForStop(std::unique_lock& lock); + + void dispatchEmulation(std::unique_lock& lock); + + private: + + TIA& myTia; + + std::thread myThread; + + std::condition_variable mySignalCondition; + std::mutex myMutex; + std::exception_ptr myPendingException; + Signal myPendingSignal; + // The initial access to myState is not synchronized -> make this atomic + std::atomic myState; + + uInt32 myCyclesPerSecond; + uInt32 myMaxCycles; + uInt32 myMinCycles; + DispatchResult* myDispatchResult; + + std::chrono::time_point myWakeupPoint; +}; + +#endif // EMULATION_WORKER_HXX diff --git a/src/emucore/module.mk b/src/emucore/module.mk index 36dfa1345..57b8fb133 100644 --- a/src/emucore/module.mk +++ b/src/emucore/module.mk @@ -55,6 +55,7 @@ MODULE_OBJS := \ src/emucore/Driving.o \ src/emucore/EventHandler.o \ src/emucore/EmulationTiming.o \ + src/emucore/EmulationWorker.o \ src/emucore/FrameBuffer.o \ src/emucore/FBSurface.o \ src/emucore/FSNode.o \