openMSX
ReverseManager.cc
Go to the documentation of this file.
1 #include "ReverseManager.hh"
2 #include "MSXMotherBoard.hh"
3 #include "EventDistributor.hh"
5 #include "Keyboard.hh"
6 #include "Debugger.hh"
7 #include "EventDelay.hh"
8 #include "MSXMixer.hh"
10 #include "XMLException.hh"
11 #include "TclObject.hh"
12 #include "FileOperations.hh"
13 #include "FileContext.hh"
14 #include "StateChange.hh"
15 #include "Reactor.hh"
16 #include "CommandException.hh"
17 #include "MemBuffer.hh"
18 #include "StringOp.hh"
19 #include "serialize.hh"
20 #include "serialize_stl.hh"
21 #include "xrange.hh"
22 #include <functional>
23 #include <cassert>
24 
25 using std::string;
26 using std::vector;
27 using std::shared_ptr;
28 using std::move;
29 
30 namespace openmsx {
31 
32 // Time between two snapshots (in seconds)
33 static const double SNAPSHOT_PERIOD = 1.0;
34 
35 // Max number of snapshots in a replay file
36 static const unsigned MAX_NOF_SNAPSHOTS = 10;
37 
38 // Min distance between snapshots in replay file (in seconds)
39 static const EmuDuration MIN_PARTITION_LENGTH = EmuDuration(60.0);
40 
41 // Max distance of one before last snapshot before the end time in replay file (in seconds)
42 static const EmuDuration MAX_DIST_1_BEFORE_LAST_SNAPSHOT = EmuDuration(30.0);
43 
44 static const char* const REPLAY_DIR = "replays";
45 
46 // A replay is a struct that contains a vector of motherboards and an MSX event
47 // log. Those combined are a replay, because you can replay the events from an
48 // existing motherboard state: the vector has to have at least one motherboard
49 // (the initial state), but can have optionally more motherboards, which are
50 // merely in-between snapshots, so it is quicker to jump to a later time in the
51 // event log.
52 
53 struct Replay
54 {
55  Replay(Reactor& reactor_)
56  : reactor(reactor_), currentTime(EmuTime::dummy()) {}
57 
59 
60  ReverseManager::Events* events;
61  std::vector<Reactor::Board> motherBoards;
63  // this is the amount of times the reverse goto command was used, which
64  // is interesting for the TAS community (see tasvideos.org). It's an
65  // indication of the effort it took to create the replay. Note that
66  // there is no way to verify this number.
67  unsigned reRecordCount;
68 
69  template<typename Archive>
70  void serialize(Archive& ar, unsigned version)
71  {
72  if (ar.versionAtLeast(version, 2)) {
73  ar.serializeWithID("snapshots", motherBoards, std::ref(reactor));
74  } else {
75  Reactor::Board newBoard = reactor.createEmptyMotherBoard();
76  ar.serialize("snapshot", *newBoard);
77  motherBoards.push_back(move(newBoard));
78  }
79 
80  ar.serialize("events", *events);
81 
82  if (ar.versionAtLeast(version, 3)) {
83  ar.serialize("currentTime", currentTime);
84  } else {
85  assert(ar.isLoader());
86  assert(!events->empty());
87  currentTime = events->back()->getTime();
88  }
89 
90  if (ar.versionAtLeast(version, 4)) {
91  ar.serialize("reRecordCount", reRecordCount);
92  }
93  }
94 };
95 SERIALIZE_CLASS_VERSION(Replay, 4);
96 
97 
98 // struct ReverseHistory
99 
100 void ReverseManager::ReverseHistory::swap(ReverseHistory& other)
101 {
102  std::swap(chunks, other.chunks);
103  std::swap(events, other.events);
104 }
105 
106 void ReverseManager::ReverseHistory::clear()
107 {
108  // clear() and free storage capacity
109  Chunks().swap(chunks);
110  Events().swap(events);
111 }
112 
113 
114 // struct ReverseChunk
115 
116 ReverseManager::ReverseChunk::ReverseChunk()
117  : time(EmuTime::zero)
118 {
119 }
120 
121 ReverseManager::ReverseChunk::ReverseChunk(ReverseChunk&& rhs)
122  : time (move(rhs.time))
123  , savestate (move(rhs.savestate))
124  , eventCount(move(rhs.eventCount))
125 {
126 }
127 
128 ReverseManager::ReverseChunk& ReverseManager::ReverseChunk::operator=(
129  ReverseChunk&& rhs)
130 {
131  time = move(rhs.time);
132  savestate = move(rhs.savestate);
133  eventCount = move(rhs.eventCount);
134  return *this;
135 }
136 
137 
138 class EndLogEvent final : public StateChange
139 {
140 public:
141  EndLogEvent() {} // for serialize
143  : StateChange(time)
144  {
145  }
146 
147  template<typename Archive> void serialize(Archive& ar, unsigned /*version*/)
148  {
149  ar.template serializeBase<StateChange>(*this);
150  }
151 };
152 REGISTER_POLYMORPHIC_CLASS(StateChange, EndLogEvent, "EndLog");
153 
154 // class ReverseManager
155 
157  : syncNewSnapshot(motherBoard_.getScheduler(), *this)
158  , syncInputEvent (motherBoard_.getScheduler(), *this)
159  , motherBoard(motherBoard_)
160  , eventDistributor(motherBoard.getReactor().getEventDistributor())
161  , reverseCmd(*this, motherBoard.getCommandController())
162  , keyboard(nullptr)
163  , eventDelay(nullptr)
164  , replayIndex(0)
165  , collecting(false)
166  , pendingTakeSnapshot(false)
167  , reRecordCount(0)
168 {
169  eventDistributor.registerEventListener(OPENMSX_TAKE_REVERSE_SNAPSHOT, *this);
170 
171  assert(!isCollecting());
172  assert(!isReplaying());
173 }
174 
176 {
177  stop();
179 }
180 
181 bool ReverseManager::isReplaying() const
182 {
183  return replayIndex != history.events.size();
184 }
185 
186 void ReverseManager::start()
187 {
188  if (!isCollecting()) {
189  // create first snapshot
190  collecting = true;
191  takeSnapshot(getCurrentTime());
192  // schedule creation of next snapshot
193  schedule(getCurrentTime());
194  // start recording events
195  motherBoard.getStateChangeDistributor().registerRecorder(*this);
196  }
197  assert(isCollecting());
198 }
199 
200 void ReverseManager::stop()
201 {
202  if (isCollecting()) {
203  motherBoard.getStateChangeDistributor().unregisterRecorder(*this);
204  syncNewSnapshot.removeSyncPoint(); // don't schedule new snapshot takings
205  syncInputEvent .removeSyncPoint(); // stop any pending replay actions
206  history.clear();
207  replayIndex = 0;
208  collecting = false;
209  pendingTakeSnapshot = false;
210  }
211  assert(!pendingTakeSnapshot);
212  assert(!isCollecting());
213  assert(!isReplaying());
214 }
215 
216 EmuTime::param ReverseManager::getEndTime(const ReverseHistory& history) const
217 {
218  if (!history.events.empty()) {
219  if (auto* ev = dynamic_cast<const EndLogEvent*>(
220  history.events.back().get())) {
221  // last log element is EndLogEvent, use that
222  return ev->getTime();
223  }
224  }
225  // otherwise use current time
226  assert(!isReplaying());
227  return getCurrentTime();
228 }
229 
230 void ReverseManager::status(TclObject& result) const
231 {
232  result.addListElement("status");
233  if (!isCollecting()) {
234  result.addListElement("disabled");
235  } else if (isReplaying()) {
236  result.addListElement("replaying");
237  } else {
238  result.addListElement("enabled");
239  }
240 
241  result.addListElement("begin");
242  EmuTime b(isCollecting() ? begin(history.chunks)->second.time
243  : EmuTime::zero);
244  result.addListElement((b - EmuTime::zero).toDouble());
245 
246  result.addListElement("end");
247  EmuTime end(isCollecting() ? getEndTime(history) : EmuTime::zero);
248  result.addListElement((end - EmuTime::zero).toDouble());
249 
250  result.addListElement("current");
251  EmuTime current(isCollecting() ? getCurrentTime() : EmuTime::zero);
252  result.addListElement((current - EmuTime::zero).toDouble());
253 
254  result.addListElement("snapshots");
255  TclObject snapshots;
256  for (auto& p : history.chunks) {
257  EmuTime time = p.second.time;
258  snapshots.addListElement((time - EmuTime::zero).toDouble());
259  }
260  result.addListElement(snapshots);
261 
262  result.addListElement("last_event");
263  auto lastEvent = history.events.rbegin();
264  if (lastEvent != history.events.rend() && dynamic_cast<const EndLogEvent*>(lastEvent->get())) {
265  ++lastEvent;
266  }
267  EmuTime le(isCollecting() && (lastEvent != history.events.rend()) ? (*lastEvent)->getTime() : EmuTime::zero);
268  result.addListElement((le - EmuTime::zero).toDouble());
269 }
270 
271 void ReverseManager::debugInfo(TclObject& result) const
272 {
273  // TODO this is useful during development, but for the end user this
274  // information means nothing. We should remove this later.
275  StringOp::Builder res;
276  size_t totalSize = 0;
277  for (auto& p : history.chunks) {
278  auto& chunk = p.second;
279  res << p.first << ' '
280  << (chunk.time - EmuTime::zero).toDouble() << ' '
281  << ((chunk.time - EmuTime::zero).toDouble() / (getCurrentTime() - EmuTime::zero).toDouble()) * 100 << '%'
282  << " (" << chunk.savestate.size() << ')'
283  << " (next event index: " << chunk.eventCount << ")\n";
284  totalSize += chunk.savestate.size();
285  }
286  res << "total size: " << totalSize << '\n';
287  result.setString(string(res));
288 }
289 
290 static void parseGoTo(Interpreter& interp, array_ref<TclObject> tokens,
291  bool& novideo, double& time)
292 {
293  novideo = false;
294  bool hasTime = false;
295  for (auto i : xrange(size_t(2), tokens.size())) {
296  if (tokens[i].getString() == "-novideo") {
297  novideo = true;
298  } else {
299  time = tokens[i].getDouble(interp);
300  hasTime = true;
301  }
302  }
303  if (!hasTime) {
304  throw SyntaxError();
305  }
306 }
307 
308 void ReverseManager::goBack(array_ref<TclObject> tokens)
309 {
310  bool novideo;
311  double t;
312  auto& interp = motherBoard.getReactor().getInterpreter();
313  parseGoTo(interp, tokens, novideo, t);
314 
315  EmuTime now = getCurrentTime();
316  EmuTime target(EmuTime::dummy());
317  if (t >= 0) {
318  EmuDuration d(t);
319  if (d < (now - EmuTime::zero)) {
320  target = now - d;
321  } else {
322  target = EmuTime::zero;
323  }
324  } else {
325  target = now + EmuDuration(-t);
326  }
327  goTo(target, novideo);
328 }
329 
330 void ReverseManager::goTo(array_ref<TclObject> tokens)
331 {
332  bool novideo;
333  double t;
334  auto& interp = motherBoard.getReactor().getInterpreter();
335  parseGoTo(interp, tokens, novideo, t);
336 
337  EmuTime target = EmuTime::zero + EmuDuration(t);
338  goTo(target, novideo);
339 }
340 
341 void ReverseManager::goTo(EmuTime::param target, bool novideo)
342 {
343  if (!isCollecting()) {
344  throw CommandException(
345  "Reverse was not enabled. First execute the 'reverse "
346  "start' command to start collecting data.");
347  }
348  goTo(target, novideo, history, true); // move in current time-line
349 }
350 
351 void ReverseManager::goTo(
352  EmuTime::param target, bool novideo, ReverseHistory& history,
353  bool sameTimeLine)
354 {
355  auto& mixer = motherBoard.getMSXMixer();
356  try {
357  // The call to MSXMotherBoard::fastForward() below may take
358  // some time to execute. The DirectX sound driver has a problem
359  // (not easily fixable) that it keeps on looping the sound
360  // buffer on buffer underruns (the SDL driver plays silence on
361  // underrun). At the end of this function we will switch to a
362  // different active MSXMotherBoard. So we can as well now
363  // already mute the current MSXMotherBoard.
364  mixer.mute();
365 
366  // -- Locate destination snapshot --
367  // We can't go back further in the past than the first snapshot.
368  assert(!history.chunks.empty());
369  auto it = begin(history.chunks);
370  EmuTime firstTime = it->second.time;
371  EmuTime targetTime = std::max(target, firstTime);
372  // Also don't go further into the future than 'end time'.
373  targetTime = std::min(targetTime, getEndTime(history));
374 
375  // Duration of 2 PAL frames. Possible improvement is to use the
376  // actual refresh rate (PAL/NTSC). But it should be the refresh
377  // rate of the active video chip (v99x8/v9990) at the target
378  // time. This is quite complex to get and the difference between
379  // 2 PAL and 2 NTSC frames isn't that big.
380  double dur2frames = 2.0 * (313.0 * 1368.0) / (3579545.0 * 6.0);
381  EmuDuration preDelta(novideo ? 0.0 : dur2frames);
382  EmuTime preTarget = ((targetTime - firstTime) > preDelta)
383  ? targetTime - preDelta
384  : firstTime;
385 
386  // find oldest snapshot that is not newer than requested time
387  // TODO ATM we do a linear search, could be improved to do a binary search.
388  assert(it->second.time <= preTarget); // first one is not newer
389  assert(it != end(history.chunks)); // there are snapshots
390  do {
391  ++it;
392  } while (it != end(history.chunks) &&
393  it->second.time <= preTarget);
394  // We found the first one that's newer, previous one is last
395  // one that's not newer (thus older or equal).
396  assert(it != begin(history.chunks));
397  --it;
398  EmuTime snapshotTime = it->second.time;
399  assert(snapshotTime <= preTarget);
400 
401  // IF current time is before the wanted time AND either
402  // - current time is closer than the closest (earlier) snapshot
403  // - OR current time is close enough (I arbitrarily choose 1s)
404  // THEN it's cheaper to start from the current position (and
405  // emulated forward) than to start from a snapshot
406  // THOUGH only when we're currently in the same time-line
407  // e.g. OK for a 'reverse goto' command, but not for a
408  // 'reverse loadreplay' command.
409  auto& reactor = motherBoard.getReactor();
410  EmuTime currentTime = getCurrentTime();
411  MSXMotherBoard* newBoard;
412  Reactor::Board newBoard_; // either nullptr or the same as newBoard
413  if (sameTimeLine &&
414  (currentTime <= preTarget) &&
415  ((snapshotTime <= currentTime) ||
416  ((preTarget - currentTime) < EmuDuration(1.0)))) {
417  newBoard = &motherBoard; // use current board
418  } else {
419  // Note: we don't (anymore) erase future snapshots
420  // -- restore old snapshot --
421  newBoard_ = reactor.createEmptyMotherBoard();
422  newBoard = newBoard_.get();
423  MemInputArchive in(it->second.savestate.data(),
424  it->second.savestate.size());
425  in.serialize("machine", *newBoard);
426 
427  if (eventDelay) {
428  // Handle all events that are scheduled, but not yet
429  // distributed. This makes sure no events get lost
430  // (important to keep host/msx keyboard in sync).
431  eventDelay->flush();
432  }
433 
434  // terminate replay log with EndLogEvent (if not there already)
435  if (history.events.empty() ||
436  !dynamic_cast<const EndLogEvent*>(history.events.back().get())) {
437  history.events.push_back(
438  std::make_shared<EndLogEvent>(currentTime));
439  }
440 
441  // Transfer history to the new ReverseManager.
442  // Also we should stop collecting in this ReverseManager,
443  // and start collecting in the new one.
444  auto& newManager = newBoard->getReverseManager();
445  newManager.transferHistory(history, it->second.eventCount);
446 
447  // transfer (or copy) state from old to new machine
448  transferState(*newBoard);
449 
450  // In case of load-replay it's possible we are not collecting,
451  // but calling stop() anyway is ok.
452  stop();
453  }
454 
455  // -- goto correct time within snapshot --
456  // Fast forward 2 frames before target time.
457  // If we're short on snapshots, create them at the usual interval.
458  while (true) {
459  EmuTime nextTarget = std::min(
460  preTarget,
461  newBoard->getCurrentTime() + EmuDuration(SNAPSHOT_PERIOD));
462  newBoard->fastForward(nextTarget, true);
463  if (nextTarget >= preTarget) break;
464  newBoard->getReverseManager().takeSnapshot(
465  newBoard->getCurrentTime());
466  }
467 
468  // switch to the new MSXMotherBoard
469  // Note: this deletes the current MSXMotherBoard and
470  // ReverseManager. So we can't access those objects anymore.
471  bool unmute = true;
472  if (newBoard_) {
473  unmute = false;
474  reactor.replaceBoard(motherBoard, move(newBoard_));
475  }
476 
477  // Fast forward to actual target time with board activated.
478  // This makes sure the video output gets rendered.
479  newBoard->fastForward(targetTime, false);
480 
481  // In case we didn't actually create a new board, don't leave
482  // the (old) board muted.
483  if (unmute) {
484  mixer.unmute();
485  }
486 
487  //assert(!isCollecting()); // can't access 'this->' members anymore!
488  assert(newBoard->getReverseManager().isCollecting());
489  } catch (MSXException&) {
490  // Make sure mixer doesn't stay muted in case of error.
491  mixer.unmute();
492  throw;
493  }
494 }
495 
496 void ReverseManager::transferState(MSXMotherBoard& newBoard)
497 {
498  // Transfer viewonly mode
499  const auto& oldDistributor = motherBoard.getStateChangeDistributor();
500  auto& newDistributor = newBoard .getStateChangeDistributor();
501  newDistributor.setViewOnlyMode(oldDistributor.isViewOnlyMode());
502 
503  // transfer keyboard state
504  auto& newManager = newBoard.getReverseManager();
505  if (newManager.keyboard && keyboard) {
506  newManager.keyboard->transferHostKeyMatrix(*keyboard);
507  }
508 
509  // transfer watchpoints
510  newBoard.getDebugger().transfer(motherBoard.getDebugger());
511 
512  // copy rerecord count
513  newManager.reRecordCount = reRecordCount;
514 
515  // transfer settings
516  const auto& oldController = motherBoard.getMSXCommandController();
517  newBoard.getMSXCommandController().transferSettings(oldController);
518 }
519 
520 void ReverseManager::saveReplay(array_ref<TclObject> tokens, TclObject& result)
521 {
522  const auto& chunks = history.chunks;
523  if (chunks.empty()) {
524  throw CommandException("No recording...");
525  }
526 
527  string filename;
528  switch (tokens.size()) {
529  case 2:
530  // nothing
531  break;
532  case 3:
533  filename = tokens[2].getString().str();
534  break;
535  default:
536  throw SyntaxError();
537  }
539  filename, REPLAY_DIR, "openmsx", ".omr");
540 
541  auto& reactor = motherBoard.getReactor();
542  Replay replay(reactor);
543  replay.reRecordCount = reRecordCount;
544 
545  // store current time (possibly somewhere in the middle of the timeline)
546  // so that on load we can go back there
547  replay.currentTime = getCurrentTime();
548 
549  // restore first snapshot to be able to serialize it to a file
550  auto initialBoard = reactor.createEmptyMotherBoard();
551  MemInputArchive in(begin(chunks)->second.savestate.data(),
552  begin(chunks)->second.savestate.size());
553  in.serialize("machine", *initialBoard);
554  replay.motherBoards.push_back(move(initialBoard));
555 
556  // determine which extra snapshots to put in the replay
557  const auto& startTime = begin(chunks)->second.time;
558  // for the end time, try to take MAX_DIST_1_BEFORE_LAST_SNAPSHOT
559  // seconds before the normal end time so that we get an extra snapshot
560  // at that point, which is comfortable if you want to reverse from the
561  // last snapshot after loading the replay.
562  const auto& lastChunkTime = chunks.rbegin()->second.time;
563  const auto& endTime = ((startTime + MAX_DIST_1_BEFORE_LAST_SNAPSHOT) < lastChunkTime) ? lastChunkTime - MAX_DIST_1_BEFORE_LAST_SNAPSHOT : lastChunkTime;
564  EmuDuration totalLength = endTime - startTime;
565  EmuDuration partitionLength = totalLength.divRoundUp(MAX_NOF_SNAPSHOTS);
566  partitionLength = std::max(MIN_PARTITION_LENGTH, partitionLength);
567  EmuTime nextPartitionEnd = startTime + partitionLength;
568  auto it = begin(chunks);
569  auto lastAddedIt = begin(chunks); // already added
570  while (it != end(chunks)) {
571  ++it;
572  if (it == end(chunks) || (it->second.time > nextPartitionEnd)) {
573  --it;
574  assert(it->second.time <= nextPartitionEnd);
575  if (it != lastAddedIt) {
576  // this is a new one, add it to the list of snapshots
577  Reactor::Board board = reactor.createEmptyMotherBoard();
578  MemInputArchive in(it->second.savestate.data(),
579  it->second.savestate.size());
580  in.serialize("machine", *board);
581  replay.motherBoards.push_back(move(board));
582  lastAddedIt = it;
583  }
584  ++it;
585  while (it != end(chunks) && it->second.time > nextPartitionEnd) {
586  nextPartitionEnd += partitionLength;
587  }
588  }
589  }
590  assert(lastAddedIt == --end(chunks)); // last snapshot must be included
591 
592  // add sentinel when there isn't one yet
593  bool addSentinel = history.events.empty() ||
594  !dynamic_cast<EndLogEvent*>(history.events.back().get());
595  if (addSentinel) {
597  history.events.push_back(std::make_shared<EndLogEvent>(
598  getCurrentTime()));
599  }
600  try {
601  XmlOutputArchive out(filename);
602  replay.events = &history.events;
603  out.serialize("replay", replay);
604  } catch (MSXException&) {
605  if (addSentinel) {
606  history.events.pop_back();
607  }
608  throw;
609  }
610 
611  if (addSentinel) {
612  // Is there a cleaner way to only add the sentinel in the log?
613  // I mean avoid changing/restoring the current log. We could
614  // make a copy and work on that, but that seems much less
615  // efficient.
616  history.events.pop_back();
617  }
618 
619  result.setString("Saved replay to " + filename);
620 }
621 
622 void ReverseManager::loadReplay(
623  Interpreter& interp, array_ref<TclObject> tokens, TclObject& result)
624 {
625  if (tokens.size() < 3) throw SyntaxError();
626 
627  vector<string> arguments;
628  const TclObject* whereArg = nullptr;
629  bool enableViewOnly = false;
630 
631  for (size_t i = 2; i < tokens.size(); ++i) {
632  string_ref token = tokens[i].getString();
633  if (token == "-viewonly") {
634  enableViewOnly = true;
635  } else if (token == "-goto") {
636  if (++i == tokens.size()) {
637  throw CommandException("Missing argument");
638  }
639  whereArg = &tokens[i];
640  } else {
641  arguments.push_back(token.str());
642  }
643  }
644 
645  if (arguments.size() != 1) throw SyntaxError();
646 
647  // resolve the filename
648  UserDataFileContext context(REPLAY_DIR);
649  string fileNameArg = arguments[0];
650  string filename;
651  try {
652  // Try filename as typed by user.
653  filename = context.resolve(fileNameArg);
654  } catch (MSXException& /*e1*/) { try {
655  // Not found, try adding '.omr'.
656  filename = context.resolve(fileNameArg + ".omr");
657  } catch (MSXException& e2) { try {
658  // Again not found, try adding '.gz'.
659  // (this is for backwards compatibility).
660  filename = context.resolve(fileNameArg + ".gz");
661  } catch (MSXException& /*e3*/) {
662  // Show error message that includes the default extension.
663  throw e2;
664  }}}
665 
666  // restore replay
667  auto& reactor = motherBoard.getReactor();
668  Replay replay(reactor);
669  Events events;
670  replay.events = &events;
671  try {
672  XmlInputArchive in(filename);
673  in.serialize("replay", replay);
674  } catch (XMLException& e) {
675  throw CommandException("Cannot load replay, bad file format: " + e.getMessage());
676  } catch (MSXException& e) {
677  throw CommandException("Cannot load replay: " + e.getMessage());
678  }
679 
680  // get destination time index
681  auto destination = EmuTime::zero;
682  string_ref where = whereArg ? whereArg->getString() : "begin";
683  if (where == "begin") {
684  destination = EmuTime::zero;
685  } else if (where == "end") {
686  destination = EmuTime::infinity;
687  } else if (where == "savetime") {
688  destination = replay.currentTime;
689  } else {
690  destination += EmuDuration(whereArg->getDouble(interp));
691  }
692 
693  // OK, we are going to be actually changing states now
694 
695  // now we can change the view only mode
696  motherBoard.getStateChangeDistributor().setViewOnlyMode(enableViewOnly);
697 
698  assert(!replay.motherBoards.empty());
699  auto& newReverseManager = replay.motherBoards[0]->getReverseManager();
700  auto& newHistory = newReverseManager.history;
701 
702  if (newReverseManager.reRecordCount == 0) {
703  // serialize Replay version >= 4
704  newReverseManager.reRecordCount = replay.reRecordCount;
705  } else {
706  // newReverseManager.reRecordCount is initialized via
707  // call from MSXMotherBoard to setReRecordCount()
708  }
709 
710  // Restore event log
711  swap(newHistory.events, events);
712  auto& newEvents = newHistory.events;
713 
714  // Restore snapshots
715  unsigned replayIndex = 0;
716  for (auto& m : replay.motherBoards) {
717  ReverseChunk newChunk;
718  newChunk.time = m->getCurrentTime();
719 
720  MemOutputArchive out;
721  out.serialize("machine", *m);
722  newChunk.savestate = out.releaseBuffer();
723 
724  // update replayIndex
725  // TODO: should we use <= instead??
726  while (replayIndex < newEvents.size() &&
727  (newEvents[replayIndex]->getTime() < newChunk.time)) {
728  replayIndex++;
729  }
730  newChunk.eventCount = replayIndex;
731 
732  newHistory.chunks[newHistory.getNextSeqNum(newChunk.time)] =
733  move(newChunk);
734  }
735 
736  // Note: untill this point we didn't make any changes to the current
737  // ReverseManager/MSXMotherBoard yet
738  reRecordCount = newReverseManager.reRecordCount;
739  bool novideo = false;
740  goTo(destination, novideo, newHistory, false); // move to different time-line
741 
742  result.setString("Loaded replay from " + filename);
743 }
744 
745 void ReverseManager::transferHistory(ReverseHistory& oldHistory,
746  unsigned oldEventCount)
747 {
748  assert(!isCollecting());
749  assert(history.chunks.empty());
750 
751  // actual history transfer
752  history.swap(oldHistory);
753 
754  // resume collecting (and event recording)
755  collecting = true;
756  schedule(getCurrentTime());
757  motherBoard.getStateChangeDistributor().registerRecorder(*this);
758 
759  // start replaying events
760  replayIndex = oldEventCount;
761  // replay log contains at least the EndLogEvent
762  assert(replayIndex < history.events.size());
763  replayNextEvent();
764 }
765 
766 void ReverseManager::execNewSnapshot()
767 {
768  // During record we should take regular snapshots, and 'now'
769  // it's been a while since the last snapshot. But 'now' can be
770  // in the middle of a CPU instruction (1). However the CPU
771  // emulation code cannot handle taking snapshots at arbitrary
772  // moments in EmuTime (2)(3)(4). So instead we send out an
773  // event that indicates we want to take a snapshot (5).
774  // (1) Schedulables are executed at the exact requested
775  // EmuTime, even in the middle of a Z80 instruction.
776  // (2) The CPU code serializes all registers, current time and
777  // various other status info, but not enough info to be
778  // able to resume in the middle of an instruction.
779  // (3) Only the CPU has this limitation of not being able to
780  // take a snapshot at any EmuTime, all other devices can.
781  // This is because in our emulation model the CPU 'drives
782  // time forward'. It's the only device code that can be
783  // interrupted by other emulation code (via Schedulables).
784  // (4) In the past we had a CPU core that could execute/resume
785  // partial instructions (search SVN history). Though it was
786  // much more complex and it also ran slower than the
787  // current code.
788  // (5) Events are delivered from the Reactor code. That code
789  // only runs when the CPU code has exited (meaning no
790  // longer active in any stackframe). So it's executed right
791  // after the CPU has finished the current instruction. And
792  // that's OK, we only require regular snapshots here, they
793  // should not be *exactly* equally far apart in time.
794  pendingTakeSnapshot = true;
795  eventDistributor.distributeEvent(
796  std::make_shared<SimpleEvent>(OPENMSX_TAKE_REVERSE_SNAPSHOT));
797 }
798 
799 void ReverseManager::execInputEvent()
800 {
801  auto event = history.events[replayIndex];
802  try {
803  // deliver current event at current time
804  motherBoard.getStateChangeDistributor().distributeReplay(event);
805  } catch (MSXException&) {
806  // can throw in case we replay a command that fails
807  // ignore
808  }
809  if (!dynamic_cast<const EndLogEvent*>(event.get())) {
810  ++replayIndex;
811  replayNextEvent();
812  } else {
813  assert(!isReplaying()); // stopped by replay of EndLogEvent
814  }
815 }
816 
817 int ReverseManager::signalEvent(const shared_ptr<const Event>& event)
818 {
819  (void)event;
820  assert(event->getType() == OPENMSX_TAKE_REVERSE_SNAPSHOT);
821 
822  // This event is send to all MSX machines, make sure it's actually this
823  // machine that requested the snapshot.
824  if (pendingTakeSnapshot) {
825  pendingTakeSnapshot = false;
826  takeSnapshot(getCurrentTime());
827  // schedule creation of next snapshot
828  schedule(getCurrentTime());
829  }
830  return 0;
831 }
832 
833 unsigned ReverseManager::ReverseHistory::getNextSeqNum(EmuTime::param time) const
834 {
835  if (chunks.empty()) {
836  return 1;
837  }
838  const auto& startTime = begin(chunks)->second.time;
839  double duration = (time - startTime).toDouble();
840  return unsigned(duration / SNAPSHOT_PERIOD + 0.5) + 1;
841 }
842 
843 void ReverseManager::takeSnapshot(EmuTime::param time)
844 {
845  // (possibly) drop old snapshots
846  // TODO does snapshot pruning still happen correctly (often enough)
847  // when going back/forward in time?
848  unsigned seqNum = history.getNextSeqNum(time);
849  dropOldSnapshots<25>(seqNum);
850 
851  // During replay we might already have a snapshot with the current
852  // sequence number, though this snapshot does not necessarily have the
853  // exact same EmuTime (because we don't (re)start taking snapshots at
854  // the same moment in time).
855 
856  // actually create new snapshot
857  MemOutputArchive out;
858  out.serialize("machine", motherBoard);
859  ReverseChunk& newChunk = history.chunks[seqNum];
860  newChunk.time = time;
861  newChunk.savestate = out.releaseBuffer();
862  newChunk.eventCount = replayIndex;
863 }
864 
865 void ReverseManager::replayNextEvent()
866 {
867  // schedule next event at its own time
868  assert(replayIndex < history.events.size());
869  syncInputEvent.setSyncPoint(history.events[replayIndex]->getTime());
870 }
871 
872 void ReverseManager::signalStateChange(const shared_ptr<StateChange>& event)
873 {
874  if (isReplaying()) {
875  // this is an event we just replayed
876  assert(event == history.events[replayIndex]);
877  if (dynamic_cast<EndLogEvent*>(event.get())) {
878  signalStopReplay(event->getTime());
879  } else {
880  // ignore all other events
881  }
882  } else {
883  // record event
884  history.events.push_back(event);
885  ++replayIndex;
886  assert(!isReplaying());
887  }
888 }
889 
890 void ReverseManager::signalStopReplay(EmuTime::param time)
891 {
892  motherBoard.getStateChangeDistributor().stopReplay(time);
893  // this is needed to prevent a reRecordCount increase
894  // due to this action ending the replay
895  reRecordCount--;
896 }
897 
898 void ReverseManager::stopReplay(EmuTime::param time)
899 {
900  if (isReplaying()) {
901  // if we're replaying, stop it and erase remainder of event log
902  syncInputEvent.removeSyncPoint();
903  Events& events = history.events;
904  events.erase(begin(events) + replayIndex, end(events));
905  // search snapshots that are newer than 'time' and erase them
906  auto it = find_if(begin(history.chunks), end(history.chunks),
907  [&](Chunks::value_type& p) { return p.second.time > time; });
908  history.chunks.erase(it, end(history.chunks));
909  // this also means someone is changing history, record that
910  reRecordCount++;
911  }
912  assert(!isReplaying());
913 }
914 
915 /* Should be called each time a new snapshot is added.
916  * This function will erase zero or more earlier snapshots so that there are
917  * more snapshots of recent history and less of distant history. It has the
918  * following properties:
919  * - the very oldest snapshot is never deleted
920  * - it keeps the N or N+1 most recent snapshots (snapshot distance = 1)
921  * - then it keeps N or N+1 with snapshot distance 2
922  * - then N or N+1 with snapshot distance 4
923  * - ... and so on
924  * @param count The index of the just added (or about to be added) element.
925  * First element should have index 1.
926  */
927 template<unsigned N>
928 void ReverseManager::dropOldSnapshots(unsigned count)
929 {
930  unsigned y = (count + N - 1) ^ (count + N);
931  unsigned d = N;
932  unsigned d2 = 2 * N + 1;
933  while (true) {
934  y >>= 1;
935  if ((y == 0) || (count <= d)) return;
936  history.chunks.erase(count - d);
937  d += d2;
938  d2 *= 2;
939  }
940 }
941 
942 void ReverseManager::schedule(EmuTime::param time)
943 {
944  syncNewSnapshot.setSyncPoint(time + EmuDuration(SNAPSHOT_PERIOD));
945 }
946 
947 
948 // class ReverseCmd
949 
950 ReverseManager::ReverseCmd::ReverseCmd(
951  ReverseManager& manager_, CommandController& controller)
952  : Command(controller, "reverse")
953  , manager(manager_)
954 {
955 }
956 
957 void ReverseManager::ReverseCmd::execute(array_ref<TclObject> tokens, TclObject& result)
958 {
959  if (tokens.size() < 2) {
960  throw CommandException("Missing subcommand");
961  }
962  auto& interp = getInterpreter();
963  string_ref subcommand = tokens[1].getString();
964  if (subcommand == "start") {
965  manager.start();
966  } else if (subcommand == "stop") {
967  manager.stop();
968  } else if (subcommand == "status") {
969  manager.status(result);
970  } else if (subcommand == "debug") {
971  manager.debugInfo(result);
972  } else if (subcommand == "goback") {
973  manager.goBack(tokens);
974  } else if (subcommand == "goto") {
975  manager.goTo(tokens);
976  } else if (subcommand == "savereplay") {
977  return manager.saveReplay(tokens, result);
978  } else if (subcommand == "loadreplay") {
979  return manager.loadReplay(interp, tokens, result);
980  } else if (subcommand == "viewonlymode") {
981  auto& distributor = manager.motherBoard.getStateChangeDistributor();
982  switch (tokens.size()) {
983  case 2:
984  result.setString(distributor.isViewOnlyMode() ? "true" : "false");
985  break;
986  case 3:
987  distributor.setViewOnlyMode(tokens[2].getBoolean(interp));
988  break;
989  default:
990  throw SyntaxError();
991  }
992  } else if (subcommand == "truncatereplay") {
993  if (manager.isReplaying()) {
994  manager.signalStopReplay(manager.getCurrentTime());
995  }
996  } else {
997  throw CommandException("Invalid subcommand: " + subcommand);
998  }
999 }
1000 
1001 string ReverseManager::ReverseCmd::help(const vector<string>& /*tokens*/) const
1002 {
1003  return "start start collecting reverse data\n"
1004  "stop stop collecting\n"
1005  "status show various status info on reverse\n"
1006  "goback <n> go back <n> seconds in time\n"
1007  "goto <time> go to an absolute moment in time\n"
1008  "viewonlymode <bool> switch viewonly mode on or off\n"
1009  "truncatereplay stop replaying and remove all 'future' data\n"
1010  "savereplay [<name>] save the first snapshot and all replay data as a 'replay' (with optional name)\n"
1011  "loadreplay [-goto <begin|end|savetime|<n>>] [-viewonly] <name> load a replay (snapshot and replay data) with given name and start replaying\n";
1012 }
1013 
1014 void ReverseManager::ReverseCmd::tabCompletion(vector<string>& tokens) const
1015 {
1016  if (tokens.size() == 2) {
1017  static const char* const subCommands[] = {
1018  "start", "stop", "status", "goback", "goto",
1019  "savereplay", "loadreplay", "viewonlymode",
1020  "truncatereplay",
1021  };
1022  completeString(tokens, subCommands);
1023  } else if ((tokens.size() == 3) || (tokens[1] == "loadreplay")) {
1024  if (tokens[1] == "loadreplay" || tokens[1] == "savereplay") {
1025  std::vector<const char*> cmds;
1026  if (tokens[1] == "loadreplay") {
1027  cmds = { "-goto", "-viewonly" };
1028  }
1029  UserDataFileContext context(REPLAY_DIR);
1030  completeFileName(tokens, context, cmds);
1031  } else if (tokens[1] == "viewonlymode") {
1032  static const char* const options[] = { "true", "false" };
1033  completeString(tokens, options);
1034  }
1035  }
1036 }
1037 
1038 } // namespace openmsx
MSXCommandController & getMSXCommandController()
void transferSettings(const MSXCommandController &from)
Transfer setting values from one machine to another, used for during 'reverse'.
Contains the main loop of openMSX.
Definition: Reactor.hh:62
void mute()
TODO This methods (un)mute the sound.
Definition: MSXMixer.cc:652
string_ref::const_iterator end(const string_ref &x)
Definition: string_ref.hh:150
size_type size() const
Definition: array_ref.hh:61
void serialize(Archive &ar, unsigned version)
Definition: EmuTime.cc:18
std::string str() const
Definition: string_ref.cc:12
static param dummy()
Definition: EmuTime.hh:21
void registerEventListener(EventType type, EventListener &listener, Priority priority=OTHER)
Registers a given object to receive certain events.
void unregisterEventListener(EventType type, EventListener &listener)
Unregisters a previously registered event listener.
std::unique_ptr< MSXMotherBoard > Board
Definition: Reactor.hh:106
void distributeEvent(const EventPtr &event)
Schedule the given event for delivery.
EndLogEvent(EmuTime::param time)
This class implements a subset of the proposal for std::string_ref (proposed for the next c++ standar...
Definition: string_ref.hh:18
const EmuTime & param
Definition: EmuTime.hh:20
Used to schedule 'taking reverse snapshots' between Z80 instructions.
Definition: Event.hh:43
unsigned reRecordCount
void stopReplay(EmuTime::param time)
Explicitly stop replay.
void unregisterRecorder(StateChangeRecorder &recorder)
void registerRecorder(StateChangeRecorder &recorder)
(Un)registers the given object to receive state change events.
REGISTER_POLYMORPHIC_CLASS(DiskContainer, NowindRomDisk,"NowindRomDisk")
This class implements a subset of the proposal for std::array_ref (proposed for the next c++ standard...
Definition: array_ref.hh:19
std::unique_ptr< Context > context
Definition: GLContext.cc:9
SERIALIZE_CLASS_VERSION(CassettePlayer, 2)
void distributeReplay(const EventPtr &event)
ReverseManager::Events * events
ReverseManager(MSXMotherBoard &motherBoard)
string parseCommandFileArgument(string_ref argument, string_ref directory, string_ref prefix, string_ref extension)
Helper function for parsing filename arguments in Tcl commands.
static const EmuTime infinity
Definition: EmuTime.hh:58
Thanks to enen for testing this on a real cartridge:
Definition: Autofire.cc:5
void serialize(Archive &ar, unsigned version)
Replay(Reactor &reactor_)
static const EmuTime zero
Definition: EmuTime.hh:57
Base class for all external MSX state changing events.
Definition: StateChange.hh:14
void serialize(Archive &ar, unsigned version)
std::vector< Reactor::Board > motherBoards
Board createEmptyMotherBoard()
Definition: Reactor.cc:361
void serialize(Archive &ar, unsigned)
string_ref::const_iterator begin(const string_ref &x)
Definition: string_ref.hh:149
XRange< T > xrange(T e)
Definition: xrange.hh:98
void setViewOnlyMode(bool value)
Set viewOnlyMode.
StateChangeDistributor & getStateChangeDistributor()
Interpreter & getInterpreter()
Definition: Reactor.cc:273