Documentation.

This commit is contained in:
Christian Speckner 2018-06-09 23:16:59 +02:00
parent 8781889a7f
commit 0fbd875783
2 changed files with 107 additions and 17 deletions

View File

@ -16,8 +16,8 @@
//============================================================================ //============================================================================
/* /*
* This class is the core of stella's real time scheduling. Scheduling is a two step * This class is the core of stella's scheduling. Scheduling is a two step process
* process that is shared between the main loop in OSystem and this class. * that is shared between the main loop in OSystem and this class.
* *
* In emulation mode (as opposed to debugger, menu, etc.), each iteration of the main loop * In emulation mode (as opposed to debugger, menu, etc.), each iteration of the main loop
* instructs the emulation worker to start emulation on a separate thread and then proceeds * instructs the emulation worker to start emulation on a separate thread and then proceeds
@ -26,9 +26,9 @@
* timeslice (as calculated from the 6507 cycles that have passed) has been reached. After * timeslice (as calculated from the 6507 cycles that have passed) has been reached. After
* that, it iterates. * that, it iterates.
* *
* The emulation worker contains its own microscheduling. After emulating a timeslice, it sleeps * The emulation worker does its own microscheduling. After emulating a timeslice, it sleeps
* until either the allotted time is up or it has been signalled to stop. If the time is up * until either the allotted time is up or it has been signalled to stop. If the time is up
* without the signal, the worker will emulate another timeslice, etc. * without being signalled, the worker will emulate another timeslice, etc.
* *
* In combination, the scheduling in the main loop and the microscheduling in the worker * In combination, the scheduling in the main loop and the microscheduling in the worker
* ensure that the emulation continues to run even if rendering blocks, ensuring the real * ensure that the emulation continues to run even if rendering blocks, ensuring the real
@ -52,19 +52,16 @@ class DispatchResult;
class EmulationWorker class EmulationWorker
{ {
public:
enum class State {
initializing, initialized, waitingForResume, running, waitingForStop, exception
};
enum class Signal {
resume, stop, quit, none
};
public: public:
/**
The constructor starts the worker thread and waits until it has initialized.
*/
EmulationWorker(); EmulationWorker();
/**
The destructor signals quit to the worker and joins.
*/
~EmulationWorker(); ~EmulationWorker();
/** /**
@ -79,52 +76,127 @@ class EmulationWorker
private: private:
/**
Check whether an exception occurred on the thread and rethrow if appicable.
*/
void handlePossibleException(); void handlePossibleException();
// Passing references into a thread is awkward and requires std::ref -> use pointers here /**
The main thread entry point.
Passing references into a thread is awkward and requires std::ref -> use pointers here
*/
void threadMain(std::condition_variable* initializedCondition, std::mutex* initializationMutex); void threadMain(std::condition_variable* initializedCondition, std::mutex* initializationMutex);
/**
Handle thread wakeup after sleep depending on the thread state.
*/
void handleWakeup(std::unique_lock<std::mutex>& lock); void handleWakeup(std::unique_lock<std::mutex>& lock);
/**
Handle wakeup while sleeping and waiting to be resumed.
*/
void handleWakeupFromWaitingForResume(std::unique_lock<std::mutex>& lock); void handleWakeupFromWaitingForResume(std::unique_lock<std::mutex>& lock);
/**
Handle wakeup while sleeping and waiting to be stopped (or for the timeslice
to expire).
*/
void handleWakeupFromWaitingForStop(std::unique_lock<std::mutex>& lock); void handleWakeupFromWaitingForStop(std::unique_lock<std::mutex>& lock);
/**
Run the emulation, adjust the thread state according to the result and sleep.
*/
void dispatchEmulation(std::unique_lock<std::mutex>& lock); void dispatchEmulation(std::unique_lock<std::mutex>& lock);
/**
Clear any pending signal and wake up the main thread (if it is waiting for the signal
to be cleared).
*/
void clearSignal(); void clearSignal();
/**
* Signal quit and wake up the main thread if applicable.
*/
void signalQuit(); void signalQuit();
/**
Wait and sleep until a pending signal has been processed (or quit sigmalled).
This is called from the main thread.
*/
void waitUntilPendingSignalHasProcessed(); void waitUntilPendingSignalHasProcessed();
/**
Log a fatal error to cerr and throw a runtime exception.
*/
void fatal(string message); void fatal(string message);
private: private:
/**
Thread state.
*/
enum class State {
// Initial state
initializing,
// Thread has initialized. From the point, myThreadIsRunningMutex is locked if and only if
// the thread is running.
initialized,
// Sleeping and waiting for emulation to be resumed
waitingForResume,
// Running and emulating
running,
// Sleeping and waiting for emulation to be stopped
waitingForStop,
// An exception occurred and the thread has terminated (or is terminating)
exception
};
TIA* myTia; /**
Thread behavior is controlled by signals that are raised prior to waking up the thread.
*/
enum class Signal {
// Resume emulation
resume,
// Stop emulation
stop,
// Quit (either during destruction or after an exception)
quit,
// No pending signal
none
};
private:
// Worker thread
std::thread myThread; std::thread myThread;
// Condition variable for waking up the thread // Condition variable for waking up the thread
std::condition_variable myWakeupCondition; std::condition_variable myWakeupCondition;
// THe thread is running while this mutex is locked // The thread is running if and only if while this mutex is locked
std::mutex myThreadIsRunningMutex; std::mutex myThreadIsRunningMutex;
// Condition variable to signal changes to the pending signal
std::condition_variable mySignalChangeCondition; std::condition_variable mySignalChangeCondition;
// This mutex guards reading and writing the pending signal.
std::mutex mySignalChangeMutex; std::mutex mySignalChangeMutex;
// Any exception on the worker thread is saved here to be rethrown on the main thread.
std::exception_ptr myPendingException; std::exception_ptr myPendingException;
// Any pending signal (or Signal::none)
Signal myPendingSignal; Signal myPendingSignal;
// The initial access to myState is not synchronized -> make this atomic // The initial access to myState is not synchronized -> make this atomic
std::atomic<State> myState; std::atomic<State> myState;
// Emulation parameters
TIA* myTia;
uInt32 myCyclesPerSecond; uInt32 myCyclesPerSecond;
uInt32 myMaxCycles; uInt32 myMaxCycles;
uInt32 myMinCycles; uInt32 myMinCycles;
DispatchResult* myDispatchResult; DispatchResult* myDispatchResult;
// Total number of cycles during this emulation run
uInt64 myTotalCycles; uInt64 myTotalCycles;
// 6507 time
std::chrono::time_point<std::chrono::high_resolution_clock> myVirtualTime; std::chrono::time_point<std::chrono::high_resolution_clock> myVirtualTime;
private: private:

View File

@ -646,9 +646,14 @@ double OSystem::dispatchEmulation(EmulationWorker& emulationWorker)
EmulationTiming& timing(myConsole->emulationTiming()); EmulationTiming& timing(myConsole->emulationTiming());
DispatchResult dispatchResult; DispatchResult dispatchResult;
// Check whether we have a frame pending for rendering...
bool framePending = tia.newFramePending(); bool framePending = tia.newFramePending();
// ... and copy it to the frame buffer. It is important to do this before
// the worker is started to avoid racing.
if (framePending) tia.renderToFrameBuffer(); if (framePending) tia.renderToFrameBuffer();
// Start emulation on a dedicated thread. It will do its own scheduling to sync 6507 and real time
// and will run until we stop the worker.
emulationWorker.start( emulationWorker.start(
timing.cyclesPerSecond(), timing.cyclesPerSecond(),
timing.maxCyclesPerTimeslice(), timing.maxCyclesPerTimeslice(),
@ -657,15 +662,21 @@ double OSystem::dispatchEmulation(EmulationWorker& emulationWorker)
&tia &tia
); );
// Render the frame. This may block, but emulation will continue to run on the worker, so the
// audio pipeline is kept fed :)
if (framePending) myFrameBuffer->updateInEmulationMode(); if (framePending) myFrameBuffer->updateInEmulationMode();
// Stop the worker and wait until it has finished
uInt64 totalCycles = emulationWorker.stop(); uInt64 totalCycles = emulationWorker.stop();
// Break or trap? -> start debugger
if (dispatchResult.getStatus() == DispatchResult::Status::debugger) myDebugger->start(); if (dispatchResult.getStatus() == DispatchResult::Status::debugger) myDebugger->start();
// Handle frying
if (dispatchResult.getStatus() == DispatchResult::Status::ok && myEventHandler->frying()) if (dispatchResult.getStatus() == DispatchResult::Status::ok && myEventHandler->frying())
myConsole->fry(); myConsole->fry();
// Return the 6507 time used in seconds
return static_cast<double>(totalCycles) / static_cast<double>(timing.cyclesPerSecond()); return static_cast<double>(totalCycles) / static_cast<double>(timing.cyclesPerSecond());
} }
@ -674,7 +685,9 @@ void OSystem::mainLoop()
{ {
// Sleep-based wait: good for CPU, bad for graphical sync // Sleep-based wait: good for CPU, bad for graphical sync
bool busyWait = mySettings->getString("timing") != "sleep"; bool busyWait = mySettings->getString("timing") != "sleep";
// 6507 time
time_point<high_resolution_clock> virtualTime = high_resolution_clock::now(); time_point<high_resolution_clock> virtualTime = high_resolution_clock::now();
// The emulation worker
EmulationWorker emulationWorker; EmulationWorker emulationWorker;
for(;;) for(;;)
@ -685,16 +698,19 @@ void OSystem::mainLoop()
double timesliceSeconds; double timesliceSeconds;
if (myEventHandler->state() == EventHandlerState::EMULATION) if (myEventHandler->state() == EventHandlerState::EMULATION)
// Dispatch emulation and render frame (if applicable)
timesliceSeconds = dispatchEmulation(emulationWorker); timesliceSeconds = dispatchEmulation(emulationWorker);
else { else {
// Render the GUI with 30 Hz in all other modes
timesliceSeconds = 1. / 30.; timesliceSeconds = 1. / 30.;
myFrameBuffer->update(); myFrameBuffer->update();
} }
duration<double> timeslice(timesliceSeconds); duration<double> timeslice(timesliceSeconds);
virtualTime += duration_cast<high_resolution_clock::duration>(timeslice); virtualTime += duration_cast<high_resolution_clock::duration>(timeslice);
time_point<high_resolution_clock> now = high_resolution_clock::now(); time_point<high_resolution_clock> now = high_resolution_clock::now();
// We allow 6507 time to lag behind by one frame max
double maxLag = myConsole double maxLag = myConsole
? ( ? (
static_cast<double>(myConsole->emulationTiming().cyclesPerFrame()) / static_cast<double>(myConsole->emulationTiming().cyclesPerFrame()) /
@ -703,8 +719,10 @@ void OSystem::mainLoop()
: 0; : 0;
if (duration_cast<duration<double>>(now - virtualTime).count() > maxLag) if (duration_cast<duration<double>>(now - virtualTime).count() > maxLag)
// If 6507 time is lagging behind more than one frame we reset it to real time
virtualTime = now; virtualTime = now;
else if (virtualTime > now) { else if (virtualTime > now) {
// Wait until we have caught up with 6507 time
if (busyWait && myEventHandler->state() == EventHandlerState::EMULATION) { if (busyWait && myEventHandler->state() == EventHandlerState::EMULATION) {
while (high_resolution_clock::now() < virtualTime); while (high_resolution_clock::now() < virtualTime);
} }