1//============================================================================
2//
3// SSSS tt lll lll
4// SS SS tt ll ll
5// SS tttttt eeee ll ll aaaa
6// SSSS tt ee ee ll ll aa
7// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator"
8// SS SS tt ee ll ll aa aa
9// SSSS ttt eeeee llll llll aaaaa
10//
11// Copyright (c) 1995-2019 by Bradford W. Mott, Stephen Anthony
12// and the Stella Team
13//
14// See the file "License.txt" for information on usage and redistribution of
15// this file, and for a DISCLAIMER OF ALL WARRANTIES.
16//============================================================================
17
18#include <exception>
19
20#include "EmulationWorker.hxx"
21#include "DispatchResult.hxx"
22#include "TIA.hxx"
23
24using namespace std::chrono;
25
26// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
27EmulationWorker::EmulationWorker()
28 : myPendingSignal(Signal::none),
29 myState(State::initializing),
30 myTia(nullptr),
31 myCyclesPerSecond(0),
32 myMaxCycles(0),
33 myMinCycles(0),
34 myDispatchResult(nullptr),
35 myTotalCycles(0)
36{
37 std::mutex mutex;
38 std::unique_lock<std::mutex> lock(mutex);
39 std::condition_variable threadInitialized;
40
41 myThread = std::thread(
42 &EmulationWorker::threadMain, this, &threadInitialized, &mutex
43 );
44
45 // Wait until the thread has acquired myThreadIsRunningMutex and moved on
46 while (myState == State::initializing) threadInitialized.wait(lock);
47}
48
49// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
50EmulationWorker::~EmulationWorker()
51{
52 // This has to run in a block in order to release the mutex before joining
53 {
54 std::unique_lock<std::mutex> lock(myThreadIsRunningMutex);
55
56 if (myState != State::exception) {
57 signalQuit();
58 myWakeupCondition.notify_one();
59 }
60 }
61
62 myThread.join();
63
64 handlePossibleException();
65}
66
67// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
68void EmulationWorker::handlePossibleException()
69{
70 if (myState == State::exception && myPendingException) {
71 std::exception_ptr ex = myPendingException;
72 // Make sure that the exception is not thrown a second time (destructor!!!)
73 myPendingException = nullptr;
74
75 std::rethrow_exception(ex);
76 }
77}
78
79// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
80void EmulationWorker::start(uInt32 cyclesPerSecond, uInt64 maxCycles, uInt64 minCycles, DispatchResult* dispatchResult, TIA* tia)
81{
82 // Wait until any pending signal has been processed
83 waitUntilPendingSignalHasProcessed();
84
85 // Run in a block to release the mutex before notifying; this avoids an unecessary
86 // block that will waste a timeslice
87 {
88 // Aquire the mutex -> wait until the thread is suspended
89 std::unique_lock<std::mutex> lock(myThreadIsRunningMutex);
90
91 // Pass on possible exceptions
92 handlePossibleException();
93
94 // Make sure that we don't overwrite the exit condition.
95 // This case is hypothetical and cannot happen, but handling it does not hurt, either
96 if (myPendingSignal == Signal::quit) return;
97
98 // NB: The thread does not suspend execution in State::initialized
99 if (myState != State::waitingForResume)
100 fatal("start called on running or dead worker");
101
102 // Store the parameters for emulation
103 myTia = tia;
104 myCyclesPerSecond = cyclesPerSecond;
105 myMaxCycles = maxCycles;
106 myMinCycles = minCycles;
107 myDispatchResult = dispatchResult;
108
109 // Raise the signal...
110 myPendingSignal = Signal::resume;
111 }
112
113 // ... and wakeup the thread
114 myWakeupCondition.notify_one();
115}
116
117// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
118uInt64 EmulationWorker::stop()
119{
120 // See EmulationWorker::start above for the gory details
121 waitUntilPendingSignalHasProcessed();
122
123 uInt64 totalCycles;
124 {
125 std::unique_lock<std::mutex> lock(myThreadIsRunningMutex);
126
127 // Paranoia: make sure that we don't doublecount an emulation timeslice
128 totalCycles = myTotalCycles;
129 myTotalCycles = 0;
130
131 handlePossibleException();
132
133 if (myPendingSignal == Signal::quit) return totalCycles;
134
135 // If the worker has stopped on its own, we return
136 if (myState == State::waitingForResume) return totalCycles;
137
138 // NB: The thread does not suspend execution in State::initialized or State::running
139 if (myState != State::waitingForStop)
140 fatal("stop called on a dead worker");
141
142 myPendingSignal = Signal::stop;
143 }
144
145 myWakeupCondition.notify_one();
146
147 return totalCycles;
148}
149
150// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
151void EmulationWorker::threadMain(std::condition_variable* initializedCondition, std::mutex* initializationMutex)
152{
153 std::unique_lock<std::mutex> lock(myThreadIsRunningMutex);
154
155 try {
156 {
157 // Wait until our parent releases the lock and sleeps
158 std::lock_guard<std::mutex> guard(*initializationMutex);
159
160 // Update the state...
161 myState = State::initialized;
162
163 // ... and wake up our parent to notifiy that we have initialized. From this point, the
164 // parent can safely assume that we are running while the mutex is locked.
165 initializedCondition->notify_one();
166 }
167
168 // Loop until we have an exit condition
169 while (myPendingSignal != Signal::quit) handleWakeup(lock);
170 }
171 catch (...) {
172 // Store away the exception and the state accordingly
173 myPendingException = std::current_exception();
174 myState = State::exception;
175
176 // Raising the exit condition is consistent and makes shure that the main thread
177 // will not deadlock if an exception is raised while it is waiting for a signal
178 // to be processed.
179 signalQuit();
180 }
181}
182
183// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
184void EmulationWorker::handleWakeup(std::unique_lock<std::mutex>& lock)
185{
186 switch (myState) {
187 case State::initialized:
188 // Enter waitingForResume and sleep after initialization
189 myState = State::waitingForResume;
190 myWakeupCondition.wait(lock);
191 break;
192
193 case State::waitingForResume:
194 handleWakeupFromWaitingForResume(lock);
195 break;
196
197 case State::waitingForStop:
198 handleWakeupFromWaitingForStop(lock);
199 break;
200
201 default:
202 fatal("wakeup in invalid worker state");
203 }
204}
205
206// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
207void EmulationWorker::handleWakeupFromWaitingForResume(std::unique_lock<std::mutex>& lock)
208{
209 switch (myPendingSignal) {
210 case Signal::resume:
211 // Clear the pending signal and notify the main thread
212 clearSignal();
213
214 // Reset virtual clock and cycle counter
215 myVirtualTime = high_resolution_clock::now();
216 myTotalCycles = 0;
217
218 // Enter emulation. This will emulate a timeslice and set the state upon completion.
219 dispatchEmulation(lock);
220 break;
221
222 case Signal::none:
223 // Reenter sleep on spurious wakeups
224 myWakeupCondition.wait(lock);
225 break;
226
227 case Signal::quit:
228 break;
229
230 default:
231 fatal("invalid signal while waiting for resume");
232 }
233}
234
235// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
236void EmulationWorker::handleWakeupFromWaitingForStop(std::unique_lock<std::mutex>& lock)
237{
238 switch (myPendingSignal) {
239 case Signal::stop:
240 // Clear the pending signal and notify the main thread
241 clearSignal();
242
243 // Enter waiting for resume and sleep
244 myState = State::waitingForResume;
245 myWakeupCondition.wait(lock);
246 break;
247
248 case Signal::none:
249 if (myVirtualTime <= high_resolution_clock::now())
250 // The time allotted to the emulation timeslice has passed and we haven't been stopped?
251 // -> go for another emulation timeslice
252 dispatchEmulation(lock);
253 else
254 // Wakeup was spurious, reenter sleep
255 myWakeupCondition.wait_until(lock, myVirtualTime);
256
257 break;
258
259 case Signal::quit:
260 break;
261
262 default:
263 fatal("invalid signal while waiting for stop");
264 }
265}
266
267// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
268void EmulationWorker::dispatchEmulation(std::unique_lock<std::mutex>& lock)
269{
270 // Technically, we could do without State::running, but it is cleaner and might be useful in the future
271 myState = State::running;
272
273 uInt64 totalCycles = 0;
274
275 do {
276 myTia->update(*myDispatchResult, totalCycles > 0 ? myMinCycles - totalCycles : myMaxCycles);
277 totalCycles += myDispatchResult->getCycles();
278 } while (totalCycles < myMinCycles && myDispatchResult->getStatus() == DispatchResult::Status::ok);
279
280 myTotalCycles += totalCycles;
281
282 bool continueEmulating = false;
283
284 if (myDispatchResult->getStatus() == DispatchResult::Status::ok) {
285 // If emulation finished successfully, we are free to go for another round
286 duration<double> timesliceSeconds(static_cast<double>(totalCycles) / static_cast<double>(myCyclesPerSecond));
287 myVirtualTime += duration_cast<high_resolution_clock::duration>(timesliceSeconds);
288
289 // If we aren't fast enough to keep up with the emulation, we stop immediatelly to avoid
290 // starving the system for processing time --- emulation will stutter anyway.
291 continueEmulating = myVirtualTime > high_resolution_clock::now();
292 }
293
294 if (continueEmulating) {
295 // If we are free to continue emulating, we sleep until either the timeslice has passed or we
296 // have been signalled from the main thread
297 myState = State::waitingForStop;
298 myWakeupCondition.wait_until(lock, myVirtualTime);
299 } else {
300 // If can't continue, we just stop and wait to be signalled
301 myState = State::waitingForResume;
302 myWakeupCondition.wait(lock);
303 }
304}
305
306// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
307void EmulationWorker::clearSignal()
308{
309 {
310 std::unique_lock<std::mutex> lock(mySignalChangeMutex);
311 myPendingSignal = Signal::none;
312 }
313
314 mySignalChangeCondition.notify_one();
315}
316
317// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
318void EmulationWorker::signalQuit()
319{
320 {
321 std::unique_lock<std::mutex> lock(mySignalChangeMutex);
322 myPendingSignal = Signal::quit;
323 }
324
325 mySignalChangeCondition.notify_one();
326}
327
328// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
329void EmulationWorker::waitUntilPendingSignalHasProcessed()
330{
331 std::unique_lock<std::mutex> lock(mySignalChangeMutex);
332
333 // White until there is no pending signal (or the exit condition has been raised)
334 while (myPendingSignal != Signal::none && myPendingSignal != Signal::quit)
335 mySignalChangeCondition.wait(lock);
336}
337
338// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
339void EmulationWorker::fatal(string message)
340{
341 (cerr << "FATAL in emulation worker: " << message << std::endl).flush();
342 throw runtime_error(message);
343}
344