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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
29RewindManager::RewindManager(OSystem& system, StateManager& statemgr)
30 : myOSystem(system),
31 myStateManager(statemgr)
32{
33 setup();
34}
35
36// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
37void 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
95bool 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
142uInt32 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
181uInt32 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
215uInt32 RewindManager::windStates(uInt32 numStates, bool unwind)
216{
217 if(unwind)
218 return unwindStates(numStates);
219 else
220 return rewindStates(numStates);
221}
222
223// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
224string 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
280string 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
340void 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
374string 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
396string 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
428uInt64 RewindManager::getFirstCycles() const
429{
430 return !myStateList.empty() ? myStateList.first()->cycles : 0;
431}
432
433// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
434uInt64 RewindManager::getCurrentCycles() const
435{
436 if(myStateList.currentIsValid())
437 return myStateList.current().cycles;
438 else
439 return 0;
440}
441
442// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
443uInt64 RewindManager::getLastCycles() const
444{
445 return !myStateList.empty() ? myStateList.last()->cycles : 0;
446}
447
448// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
449IntArray 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