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 <cmath> |
19 | |
20 | #include "OSystem.hxx" |
21 | #include "Serializer.hxx" |
22 | #include "StateManager.hxx" |
23 | #include "TIA.hxx" |
24 | #include "EventHandler.hxx" |
25 | |
26 | #include "RewindManager.hxx" |
27 | |
28 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
29 | RewindManager::RewindManager(OSystem& system, StateManager& statemgr) |
30 | : myOSystem(system), |
31 | myStateManager(statemgr) |
32 | { |
33 | setup(); |
34 | } |
35 | |
36 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
37 | void RewindManager::setup() |
38 | { |
39 | myStateSize = 0; |
40 | myLastTimeMachineAdd = false; |
41 | |
42 | const string& prefix = myOSystem.settings().getBool("dev.settings" ) ? "dev." : "plr." ; |
43 | |
44 | mySize = myOSystem.settings().getInt(prefix + "tm.size" ); |
45 | if(mySize != myStateList.capacity()) |
46 | resize(mySize); |
47 | |
48 | myUncompressed = myOSystem.settings().getInt(prefix + "tm.uncompressed" ); |
49 | |
50 | myInterval = INTERVAL_CYCLES[0]; |
51 | for(int i = 0; i < NUM_INTERVALS; ++i) |
52 | if(INT_SETTINGS[i] == myOSystem.settings().getString(prefix + "tm.interval" )) |
53 | myInterval = INTERVAL_CYCLES[i]; |
54 | |
55 | myHorizon = HORIZON_CYCLES[NUM_HORIZONS-1]; |
56 | for(int i = 0; i < NUM_HORIZONS; ++i) |
57 | if(HOR_SETTINGS[i] == myOSystem.settings().getString(prefix + "tm.horizon" )) |
58 | myHorizon = HORIZON_CYCLES[i]; |
59 | |
60 | // calc interval growth factor for compression |
61 | // this factor defines the backward horizon |
62 | const double MAX_FACTOR = 1E8; |
63 | double minFactor = 0, maxFactor = MAX_FACTOR; |
64 | myFactor = 1; |
65 | |
66 | while(myUncompressed < mySize) |
67 | { |
68 | double interval = myInterval; |
69 | double cycleSum = interval * (myUncompressed + 1); |
70 | // calculate nextCycles factor |
71 | myFactor = (minFactor + maxFactor) / 2; |
72 | // horizon not reachable? |
73 | if(myFactor == MAX_FACTOR) |
74 | break; |
75 | // sum up interval cycles (first state is not compressed) |
76 | for(uInt32 i = myUncompressed + 1; i < mySize; ++i) |
77 | { |
78 | interval *= myFactor; |
79 | cycleSum += interval; |
80 | } |
81 | double diff = cycleSum - myHorizon; |
82 | |
83 | // exit loop if result is close enough |
84 | if(std::abs(diff) < myHorizon * 1E-5) |
85 | break; |
86 | // define new boundary |
87 | if(cycleSum < myHorizon) |
88 | minFactor = myFactor; |
89 | else |
90 | maxFactor = myFactor; |
91 | } |
92 | } |
93 | |
94 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
95 | bool RewindManager::addState(const string& message, bool timeMachine) |
96 | { |
97 | // only check for Time Machine states, ignore for debugger |
98 | if(timeMachine && myStateList.currentIsValid()) |
99 | { |
100 | // check if the current state has the right interval from the last state |
101 | RewindState& lastState = myStateList.current(); |
102 | uInt32 interval = myInterval; |
103 | |
104 | // adjust frame timed intervals to actual scanlines (vs 262) |
105 | if(interval >= 76 * 262 && interval <= 76 * 262 * 30) |
106 | { |
107 | const uInt32 scanlines = std::max(myOSystem.console().tia().scanlinesLastFrame(), 240u); |
108 | |
109 | interval = interval * scanlines / 262; |
110 | } |
111 | |
112 | if(myOSystem.console().tia().cycles() - lastState.cycles < interval) |
113 | return false; |
114 | } |
115 | |
116 | // Remove all future states |
117 | myStateList.removeToLast(); |
118 | |
119 | // Make sure we never run out of space |
120 | if(myStateList.full()) |
121 | compressStates(); |
122 | |
123 | // Add new state at the end of the list (queue adds at end) |
124 | // This updates the 'current' iterator inside the list |
125 | myStateList.addLast(); |
126 | RewindState& state = myStateList.current(); |
127 | Serializer& s = state.data; |
128 | |
129 | s.rewind(); // rewind Serializer internal buffers |
130 | if(myStateManager.saveState(s) && myOSystem.console().tia().saveDisplay(s)) |
131 | { |
132 | myStateSize = std::max(myStateSize, uInt32(s.size())); |
133 | state.message = message; |
134 | state.cycles = myOSystem.console().tia().cycles(); |
135 | myLastTimeMachineAdd = timeMachine; |
136 | return true; |
137 | } |
138 | return false; |
139 | } |
140 | |
141 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
142 | uInt32 RewindManager::rewindStates(uInt32 numStates) |
143 | { |
144 | uInt64 startCycles = myOSystem.console().tia().cycles(); |
145 | uInt32 i; |
146 | string message; |
147 | |
148 | for(i = 0; i < numStates; ++i) |
149 | { |
150 | if(!atFirst()) |
151 | { |
152 | if(!myLastTimeMachineAdd) |
153 | // Set internal current iterator to previous state (back in time), |
154 | // since we will now process this state... |
155 | myStateList.moveToPrevious(); |
156 | else |
157 | // ...except when the last state was added automatically, |
158 | // because that already happened one interval before |
159 | myLastTimeMachineAdd = false; |
160 | |
161 | RewindState& state = myStateList.current(); |
162 | Serializer& s = state.data; |
163 | s.rewind(); // rewind Serializer internal buffers |
164 | } |
165 | else |
166 | break; |
167 | } |
168 | |
169 | if(i) |
170 | // Load the current state and get the message string for the rewind |
171 | message = loadState(startCycles, i); |
172 | else |
173 | message = "Rewind not possible" ; |
174 | |
175 | if(myOSystem.eventHandler().state() != EventHandlerState::TIMEMACHINE) |
176 | myOSystem.frameBuffer().showMessage(message); |
177 | return i; |
178 | } |
179 | |
180 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
181 | uInt32 RewindManager::unwindStates(uInt32 numStates) |
182 | { |
183 | uInt64 startCycles = myOSystem.console().tia().cycles(); |
184 | uInt32 i; |
185 | string message; |
186 | |
187 | for(i = 0; i < numStates; ++i) |
188 | { |
189 | if(!atLast()) |
190 | { |
191 | // Set internal current iterator to nextCycles state (forward in time), |
192 | // since we will now process this state |
193 | myStateList.moveToNext(); |
194 | |
195 | RewindState& state = myStateList.current(); |
196 | Serializer& s = state.data; |
197 | s.rewind(); // rewind Serializer internal buffers |
198 | } |
199 | else |
200 | break; |
201 | } |
202 | |
203 | if(i) |
204 | // Load the current state and get the message string for the unwind |
205 | message = loadState(startCycles, i); |
206 | else |
207 | message = "Unwind not possible" ; |
208 | |
209 | if(myOSystem.eventHandler().state() != EventHandlerState::TIMEMACHINE) |
210 | myOSystem.frameBuffer().showMessage(message); |
211 | return i; |
212 | } |
213 | |
214 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
215 | uInt32 RewindManager::windStates(uInt32 numStates, bool unwind) |
216 | { |
217 | if(unwind) |
218 | return unwindStates(numStates); |
219 | else |
220 | return rewindStates(numStates); |
221 | } |
222 | |
223 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
224 | string RewindManager::saveAllStates() |
225 | { |
226 | if (getLastIdx() == 0) |
227 | return "Nothing to save" ; |
228 | |
229 | try |
230 | { |
231 | ostringstream buf; |
232 | buf << myOSystem.stateDir() |
233 | << myOSystem.console().properties().get(PropType::Cart_Name) |
234 | << ".sta" ; |
235 | |
236 | Serializer out(buf.str(), Serializer::Mode::ReadWriteTrunc); |
237 | if (!out) |
238 | return "Can't save to all states file" ; |
239 | |
240 | uInt32 curIdx = getCurrentIdx(); |
241 | rewindStates(1000); |
242 | uInt32 numStates = uInt32(cyclesList().size()); |
243 | |
244 | // Save header |
245 | buf.str("" ); |
246 | out.putString(STATE_HEADER); |
247 | out.putShort(numStates); |
248 | out.putInt(myStateSize); |
249 | |
250 | unique_ptr<uInt8[]> buffer = make_unique<uInt8[]>(myStateSize); |
251 | for (uInt32 i = 0; i < numStates; i++) |
252 | { |
253 | RewindState& state = myStateList.current(); |
254 | Serializer& s = state.data; |
255 | // Rewind Serializer internal buffers |
256 | s.rewind(); |
257 | // Save state |
258 | s.getByteArray(buffer.get(), myStateSize); |
259 | out.putByteArray(buffer.get(), myStateSize); |
260 | out.putString(state.message); |
261 | out.putLong(state.cycles); |
262 | |
263 | if (i < numStates) |
264 | unwindStates(1); |
265 | } |
266 | // restore old state position |
267 | rewindStates(numStates - curIdx); |
268 | |
269 | buf.str("" ); |
270 | buf << "Saved " << numStates << " states" ; |
271 | return buf.str(); |
272 | } |
273 | catch (...) |
274 | { |
275 | return "Error saving all states" ; |
276 | } |
277 | } |
278 | |
279 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
280 | string RewindManager::loadAllStates() |
281 | { |
282 | try |
283 | { |
284 | ostringstream buf; |
285 | buf << myOSystem.stateDir() |
286 | << myOSystem.console().properties().get(PropType::Cart_Name) |
287 | << ".sta" ; |
288 | |
289 | // Make sure the file can be opened for reading |
290 | Serializer in(buf.str(), Serializer::Mode::ReadOnly); |
291 | if (!in) |
292 | return "Can't load from all states file" ; |
293 | |
294 | clear(); |
295 | uInt32 numStates; |
296 | |
297 | // Load header |
298 | buf.str("" ); |
299 | // Check compatibility |
300 | if (in.getString() != STATE_HEADER) |
301 | return "Incompatible all states file" ; |
302 | numStates = in.getShort(); |
303 | myStateSize = in.getInt(); |
304 | |
305 | unique_ptr<uInt8[]> buffer = make_unique<uInt8[]>(myStateSize); |
306 | for (uInt32 i = 0; i < numStates; i++) |
307 | { |
308 | if (myStateList.full()) |
309 | compressStates(); |
310 | |
311 | // Add new state at the end of the list (queue adds at end) |
312 | // This updates the 'current' iterator inside the list |
313 | myStateList.addLast(); |
314 | RewindState& state = myStateList.current(); |
315 | Serializer& s = state.data; |
316 | // Rewind Serializer internal buffers |
317 | s.rewind(); |
318 | |
319 | // Fill new state with saved values |
320 | in.getByteArray(buffer.get(), myStateSize); |
321 | s.putByteArray(buffer.get(), myStateSize); |
322 | state.message = in.getString(); |
323 | state.cycles = in.getLong(); |
324 | } |
325 | |
326 | // initialize current state (parameters ignored) |
327 | loadState(0, 0); |
328 | |
329 | buf.str("" ); |
330 | buf << "Loaded " << numStates << " states" ; |
331 | return buf.str(); |
332 | } |
333 | catch (...) |
334 | { |
335 | return "Error loading all states" ; |
336 | } |
337 | } |
338 | |
339 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
340 | void RewindManager::compressStates() |
341 | { |
342 | double expectedCycles = myInterval * myFactor * (1 + myFactor); |
343 | double maxError = 1.5; |
344 | uInt32 idx = myStateList.size() - 2; |
345 | // in case maxError is <= 1.5 remove first state by default: |
346 | Common::LinkedObjectPool<RewindState>::const_iter removeIter = myStateList.first(); |
347 | /*if(myUncompressed < mySize) |
348 | // if compression is enabled, the first but one state is removed by default: |
349 | removeIter++;*/ |
350 | |
351 | // iterate from last but one to first but one |
352 | for(auto it = myStateList.previous(myStateList.last()); it != myStateList.first(); --it) |
353 | { |
354 | if(idx < mySize - myUncompressed) |
355 | { |
356 | expectedCycles *= myFactor; |
357 | |
358 | uInt64 prevCycles = myStateList.previous(it)->cycles; |
359 | uInt64 nextCycles = myStateList.next(it)->cycles; |
360 | double error = expectedCycles / (nextCycles - prevCycles); |
361 | |
362 | if(error > maxError) |
363 | { |
364 | maxError = error; |
365 | removeIter = it; |
366 | } |
367 | } |
368 | --idx; |
369 | } |
370 | myStateList.remove(removeIter); // remove |
371 | } |
372 | |
373 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
374 | string RewindManager::loadState(Int64 startCycles, uInt32 numStates) |
375 | { |
376 | RewindState& state = myStateList.current(); |
377 | Serializer& s = state.data; |
378 | |
379 | myStateManager.loadState(s); |
380 | myOSystem.console().tia().loadDisplay(s); |
381 | |
382 | Int64 diff = startCycles - state.cycles; |
383 | stringstream message; |
384 | |
385 | message << (diff >= 0 ? "Rewind" : "Unwind" ) << " " << getUnitString(diff); |
386 | message << " [" << myStateList.currentIdx() << "/" << myStateList.size() << "]" ; |
387 | |
388 | // add optional message |
389 | if(numStates == 1 && !state.message.empty()) |
390 | message << " (" << state.message << ")" ; |
391 | |
392 | return message.str(); |
393 | } |
394 | |
395 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
396 | string RewindManager::getUnitString(Int64 cycles) |
397 | { |
398 | const Int32 scanlines = std::max(myOSystem.console().tia().scanlinesLastFrame(), 240u); |
399 | const bool isNTSC = scanlines <= 287; |
400 | const Int32 NTSC_FREQ = 1193182; // ~76*262*60 |
401 | const Int32 PAL_FREQ = 1182298; // ~76*312*50 |
402 | const Int32 freq = isNTSC ? NTSC_FREQ : PAL_FREQ; // = cycles/second |
403 | |
404 | const Int32 NUM_UNITS = 5; |
405 | const string UNIT_NAMES[NUM_UNITS] = { "cycle" , "scanline" , "frame" , "second" , "minute" }; |
406 | const Int64 UNIT_CYCLES[NUM_UNITS + 1] = { 1, 76, 76 * scanlines, freq, freq * 60, Int64(1) << 62 }; |
407 | |
408 | stringstream result; |
409 | Int32 i; |
410 | |
411 | cycles = std::abs(cycles); |
412 | |
413 | for(i = 0; i < NUM_UNITS - 1; ++i) |
414 | { |
415 | // use the lower unit up to twice the nextCycles unit, except for an exact match of the nextCycles unit |
416 | // TODO: does the latter make sense, e.g. for ROMs with changing scanlines? |
417 | if(cycles == 0 || (cycles < UNIT_CYCLES[i + 1] * 2 && cycles % UNIT_CYCLES[i + 1] != 0)) |
418 | break; |
419 | } |
420 | result << cycles / UNIT_CYCLES[i] << " " << UNIT_NAMES[i]; |
421 | if(cycles / UNIT_CYCLES[i] != 1) |
422 | result << "s" ; |
423 | |
424 | return result.str(); |
425 | } |
426 | |
427 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
428 | uInt64 RewindManager::getFirstCycles() const |
429 | { |
430 | return !myStateList.empty() ? myStateList.first()->cycles : 0; |
431 | } |
432 | |
433 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
434 | uInt64 RewindManager::getCurrentCycles() const |
435 | { |
436 | if(myStateList.currentIsValid()) |
437 | return myStateList.current().cycles; |
438 | else |
439 | return 0; |
440 | } |
441 | |
442 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
443 | uInt64 RewindManager::getLastCycles() const |
444 | { |
445 | return !myStateList.empty() ? myStateList.last()->cycles : 0; |
446 | } |
447 | |
448 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
449 | IntArray RewindManager::cyclesList() const |
450 | { |
451 | IntArray arr; |
452 | |
453 | uInt64 firstCycle = getFirstCycles(); |
454 | for(auto it = myStateList.cbegin(); it != myStateList.cend(); ++it) |
455 | arr.push_back(uInt32(it->cycles - firstCycle)); |
456 | |
457 | return arr; |
458 | } |
459 | |