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
* process that is shared between the main loop in OSystem and this class.
* This class is the core of stella's scheduling. Scheduling is a two step process
* 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
* 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
* 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
* 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
* ensure that the emulation continues to run even if rendering blocks, ensuring the real
@ -52,19 +52,16 @@ class DispatchResult;
class EmulationWorker
{
public:
enum class State {
initializing, initialized, waitingForResume, running, waitingForStop, exception
};
enum class Signal {
resume, stop, quit, none
};
public:
/**
The constructor starts the worker thread and waits until it has initialized.
*/
EmulationWorker();
/**
The destructor signals quit to the worker and joins.
*/
~EmulationWorker();
/**
@ -79,52 +76,127 @@ class EmulationWorker
private:
/**
Check whether an exception occurred on the thread and rethrow if appicable.
*/
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);
/**
Handle thread wakeup after sleep depending on the thread state.
*/
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);
/**
Handle wakeup while sleeping and waiting to be stopped (or for the timeslice
to expire).
*/
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);
/**
Clear any pending signal and wake up the main thread (if it is waiting for the signal
to be cleared).
*/
void clearSignal();
/**
* Signal quit and wake up the main thread if applicable.
*/
void signalQuit();
/**
Wait and sleep until a pending signal has been processed (or quit sigmalled).
This is called from the main thread.
*/
void waitUntilPendingSignalHasProcessed();
/**
Log a fatal error to cerr and throw a runtime exception.
*/
void fatal(string message);
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;
// Condition variable for waking up the thread
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;
// Condition variable to signal changes to the pending signal
std::condition_variable mySignalChangeCondition;
// This mutex guards reading and writing the pending signal.
std::mutex mySignalChangeMutex;
// Any exception on the worker thread is saved here to be rethrown on the main thread.
std::exception_ptr myPendingException;
// Any pending signal (or Signal::none)
Signal myPendingSignal;
// The initial access to myState is not synchronized -> make this atomic
std::atomic<State> myState;
// Emulation parameters
TIA* myTia;
uInt32 myCyclesPerSecond;
uInt32 myMaxCycles;
uInt32 myMinCycles;
DispatchResult* myDispatchResult;
// Total number of cycles during this emulation run
uInt64 myTotalCycles;
// 6507 time
std::chrono::time_point<std::chrono::high_resolution_clock> myVirtualTime;
private:

View File

@ -646,9 +646,14 @@ double OSystem::dispatchEmulation(EmulationWorker& emulationWorker)
EmulationTiming& timing(myConsole->emulationTiming());
DispatchResult dispatchResult;
// Check whether we have a frame pending for rendering...
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();
// 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(
timing.cyclesPerSecond(),
timing.maxCyclesPerTimeslice(),
@ -657,15 +662,21 @@ double OSystem::dispatchEmulation(EmulationWorker& emulationWorker)
&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();
// Stop the worker and wait until it has finished
uInt64 totalCycles = emulationWorker.stop();
// Break or trap? -> start debugger
if (dispatchResult.getStatus() == DispatchResult::Status::debugger) myDebugger->start();
// Handle frying
if (dispatchResult.getStatus() == DispatchResult::Status::ok && myEventHandler->frying())
myConsole->fry();
// Return the 6507 time used in seconds
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
bool busyWait = mySettings->getString("timing") != "sleep";
// 6507 time
time_point<high_resolution_clock> virtualTime = high_resolution_clock::now();
// The emulation worker
EmulationWorker emulationWorker;
for(;;)
@ -685,16 +698,19 @@ void OSystem::mainLoop()
double timesliceSeconds;
if (myEventHandler->state() == EventHandlerState::EMULATION)
// Dispatch emulation and render frame (if applicable)
timesliceSeconds = dispatchEmulation(emulationWorker);
else {
// Render the GUI with 30 Hz in all other modes
timesliceSeconds = 1. / 30.;
myFrameBuffer->update();
}
duration<double> timeslice(timesliceSeconds);
virtualTime += duration_cast<high_resolution_clock::duration>(timeslice);
time_point<high_resolution_clock> now = high_resolution_clock::now();
// We allow 6507 time to lag behind by one frame max
double maxLag = myConsole
? (
static_cast<double>(myConsole->emulationTiming().cyclesPerFrame()) /
@ -703,8 +719,10 @@ void OSystem::mainLoop()
: 0;
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;
else if (virtualTime > now) {
// Wait until we have caught up with 6507 time
if (busyWait && myEventHandler->state() == EventHandlerState::EMULATION) {
while (high_resolution_clock::now() < virtualTime);
}