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