mirror of https://github.com/stella-emu/stella.git
Documentation.
This commit is contained in:
parent
8781889a7f
commit
0fbd875783
|
@ -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:
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue