| 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 | |