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 intervals that are
458  // at least the usual interval, but the later, the more: each
459  // time divide the remaining time in half and make a snapshot
460  // there.
461  while (true) {
462  EmuTime nextTarget = std::min(
463  preTarget,
464  newBoard->getCurrentTime() + std::max(
465  EmuDuration(SNAPSHOT_PERIOD),
466  (preTarget - newBoard->getCurrentTime()) / 2
467  ));
468  newBoard->fastForward(nextTarget, true);
469  if (nextTarget >= preTarget) break;
470  newBoard->getReverseManager().takeSnapshot(
471  newBoard->getCurrentTime());
472  }
473 
474  // switch to the new MSXMotherBoard
475  // Note: this deletes the current MSXMotherBoard and
476  // ReverseManager. So we can't access those objects anymore.
477  bool unmute = true;
478  if (newBoard_) {
479  unmute = false;
480  reactor.replaceBoard(motherBoard, move(newBoard_));
481  }
482 
483  // Fast forward to actual target time with board activated.
484  // This makes sure the video output gets rendered.
485  newBoard->fastForward(targetTime, false);
486 
487  // In case we didn't actually create a new board, don't leave
488  // the (old) board muted.
489  if (unmute) {
490  mixer.unmute();
491  }
492 
493  //assert(!isCollecting()); // can't access 'this->' members anymore!
494  assert(newBoard->getReverseManager().isCollecting());
495  } catch (MSXException&) {
496  // Make sure mixer doesn't stay muted in case of error.
497  mixer.unmute();
498  throw;
499  }
500 }
501 
502 void ReverseManager::transferState(MSXMotherBoard& newBoard)
503 {
504  // Transfer viewonly mode
505  const auto& oldDistributor = motherBoard.getStateChangeDistributor();
506  auto& newDistributor = newBoard .getStateChangeDistributor();
507  newDistributor.setViewOnlyMode(oldDistributor.isViewOnlyMode());
508 
509  // transfer keyboard state
510  auto& newManager = newBoard.getReverseManager();
511  if (newManager.keyboard && keyboard) {
512  newManager.keyboard->transferHostKeyMatrix(*keyboard);
513  }
514 
515  // transfer watchpoints
516  newBoard.getDebugger().transfer(motherBoard.getDebugger());
517 
518  // copy rerecord count
519  newManager.reRecordCount = reRecordCount;
520 
521  // transfer settings
522  const auto& oldController = motherBoard.getMSXCommandController();
523  newBoard.getMSXCommandController().transferSettings(oldController);
524 }
525 
526 void ReverseManager::saveReplay(array_ref<TclObject> tokens, TclObject& result)
527 {
528  const auto& chunks = history.chunks;
529  if (chunks.empty()) {
530  throw CommandException("No recording...");
531  }
532 
533  string filename;
534  switch (tokens.size()) {
535  case 2:
536  // nothing
537  break;
538  case 3:
539  filename = tokens[2].getString().str();
540  break;
541  default:
542  throw SyntaxError();
543  }
545  filename, REPLAY_DIR, "openmsx", ".omr");
546 
547  auto& reactor = motherBoard.getReactor();
548  Replay replay(reactor);
549  replay.reRecordCount = reRecordCount;
550 
551  // store current time (possibly somewhere in the middle of the timeline)
552  // so that on load we can go back there
553  replay.currentTime = getCurrentTime();
554 
555  // restore first snapshot to be able to serialize it to a file
556  auto initialBoard = reactor.createEmptyMotherBoard();
557  MemInputArchive in(begin(chunks)->second.savestate.data(),
558  begin(chunks)->second.savestate.size());
559  in.serialize("machine", *initialBoard);
560  replay.motherBoards.push_back(move(initialBoard));
561 
562  // determine which extra snapshots to put in the replay
563  const auto& startTime = begin(chunks)->second.time;
564  // for the end time, try to take MAX_DIST_1_BEFORE_LAST_SNAPSHOT
565  // seconds before the normal end time so that we get an extra snapshot
566  // at that point, which is comfortable if you want to reverse from the
567  // last snapshot after loading the replay.
568  const auto& lastChunkTime = chunks.rbegin()->second.time;
569  const auto& endTime = ((startTime + MAX_DIST_1_BEFORE_LAST_SNAPSHOT) < lastChunkTime) ? lastChunkTime - MAX_DIST_1_BEFORE_LAST_SNAPSHOT : lastChunkTime;
570  EmuDuration totalLength = endTime - startTime;
571  EmuDuration partitionLength = totalLength.divRoundUp(MAX_NOF_SNAPSHOTS);
572  partitionLength = std::max(MIN_PARTITION_LENGTH, partitionLength);
573  EmuTime nextPartitionEnd = startTime + partitionLength;
574  auto it = begin(chunks);
575  auto lastAddedIt = begin(chunks); // already added
576  while (it != end(chunks)) {
577  ++it;
578  if (it == end(chunks) || (it->second.time > nextPartitionEnd)) {
579  --it;
580  assert(it->second.time <= nextPartitionEnd);
581  if (it != lastAddedIt) {
582  // this is a new one, add it to the list of snapshots
583  Reactor::Board board = reactor.createEmptyMotherBoard();
584  MemInputArchive in(it->second.savestate.data(),
585  it->second.savestate.size());
586  in.serialize("machine", *board);
587  replay.motherBoards.push_back(move(board));
588  lastAddedIt = it;
589  }
590  ++it;
591  while (it != end(chunks) && it->second.time > nextPartitionEnd) {
592  nextPartitionEnd += partitionLength;
593  }
594  }
595  }
596  assert(lastAddedIt == --end(chunks)); // last snapshot must be included
597 
598  // add sentinel when there isn't one yet
599  bool addSentinel = history.events.empty() ||
600  !dynamic_cast<EndLogEvent*>(history.events.back().get());
601  if (addSentinel) {
603  history.events.push_back(std::make_shared<EndLogEvent>(
604  getCurrentTime()));
605  }
606  try {
607  XmlOutputArchive out(filename);
608  replay.events = &history.events;
609  out.serialize("replay", replay);
610  } catch (MSXException&) {
611  if (addSentinel) {
612  history.events.pop_back();
613  }
614  throw;
615  }
616 
617  if (addSentinel) {
618  // Is there a cleaner way to only add the sentinel in the log?
619  // I mean avoid changing/restoring the current log. We could
620  // make a copy and work on that, but that seems much less
621  // efficient.
622  history.events.pop_back();
623  }
624 
625  result.setString("Saved replay to " + filename);
626 }
627 
628 void ReverseManager::loadReplay(
629  Interpreter& interp, array_ref<TclObject> tokens, TclObject& result)
630 {
631  if (tokens.size() < 3) throw SyntaxError();
632 
633  vector<string> arguments;
634  const TclObject* whereArg = nullptr;
635  bool enableViewOnly = false;
636 
637  for (size_t i = 2; i < tokens.size(); ++i) {
638  string_ref token = tokens[i].getString();
639  if (token == "-viewonly") {
640  enableViewOnly = true;
641  } else if (token == "-goto") {
642  if (++i == tokens.size()) {
643  throw CommandException("Missing argument");
644  }
645  whereArg = &tokens[i];
646  } else {
647  arguments.push_back(token.str());
648  }
649  }
650 
651  if (arguments.size() != 1) throw SyntaxError();
652 
653  // resolve the filename
654  UserDataFileContext context(REPLAY_DIR);
655  string fileNameArg = arguments[0];
656  string filename;
657  try {
658  // Try filename as typed by user.
659  filename = context.resolve(fileNameArg);
660  } catch (MSXException& /*e1*/) { try {
661  // Not found, try adding '.omr'.
662  filename = context.resolve(fileNameArg + ".omr");
663  } catch (MSXException& e2) { try {
664  // Again not found, try adding '.gz'.
665  // (this is for backwards compatibility).
666  filename = context.resolve(fileNameArg + ".gz");
667  } catch (MSXException& /*e3*/) {
668  // Show error message that includes the default extension.
669  throw e2;
670  }}}
671 
672  // restore replay
673  auto& reactor = motherBoard.getReactor();
674  Replay replay(reactor);
675  Events events;
676  replay.events = &events;
677  try {
678  XmlInputArchive in(filename);
679  in.serialize("replay", replay);
680  } catch (XMLException& e) {
681  throw CommandException("Cannot load replay, bad file format: " + e.getMessage());
682  } catch (MSXException& e) {
683  throw CommandException("Cannot load replay: " + e.getMessage());
684  }
685 
686  // get destination time index
687  auto destination = EmuTime::zero;
688  string_ref where = whereArg ? whereArg->getString() : "begin";
689  if (where == "begin") {
690  destination = EmuTime::zero;
691  } else if (where == "end") {
692  destination = EmuTime::infinity;
693  } else if (where == "savetime") {
694  destination = replay.currentTime;
695  } else {
696  destination += EmuDuration(whereArg->getDouble(interp));
697  }
698 
699  // OK, we are going to be actually changing states now
700 
701  // now we can change the view only mode
702  motherBoard.getStateChangeDistributor().setViewOnlyMode(enableViewOnly);
703 
704  assert(!replay.motherBoards.empty());
705  auto& newReverseManager = replay.motherBoards[0]->getReverseManager();
706  auto& newHistory = newReverseManager.history;
707 
708  if (newReverseManager.reRecordCount == 0) {
709  // serialize Replay version >= 4
710  newReverseManager.reRecordCount = replay.reRecordCount;
711  } else {
712  // newReverseManager.reRecordCount is initialized via
713  // call from MSXMotherBoard to setReRecordCount()
714  }
715 
716  // Restore event log
717  swap(newHistory.events, events);
718  auto& newEvents = newHistory.events;
719 
720  // Restore snapshots
721  unsigned replayIndex = 0;
722  for (auto& m : replay.motherBoards) {
723  ReverseChunk newChunk;
724  newChunk.time = m->getCurrentTime();
725 
726  MemOutputArchive out;
727  out.serialize("machine", *m);
728  newChunk.savestate = out.releaseBuffer();
729 
730  // update replayIndex
731  // TODO: should we use <= instead??
732  while (replayIndex < newEvents.size() &&
733  (newEvents[replayIndex]->getTime() < newChunk.time)) {
734  replayIndex++;
735  }
736  newChunk.eventCount = replayIndex;
737 
738  newHistory.chunks[newHistory.getNextSeqNum(newChunk.time)] =
739  move(newChunk);
740  }
741 
742  // Note: untill this point we didn't make any changes to the current
743  // ReverseManager/MSXMotherBoard yet
744  reRecordCount = newReverseManager.reRecordCount;
745  bool novideo = false;
746  goTo(destination, novideo, newHistory, false); // move to different time-line
747 
748  result.setString("Loaded replay from " + filename);
749 }
750 
751 void ReverseManager::transferHistory(ReverseHistory& oldHistory,
752  unsigned oldEventCount)
753 {
754  assert(!isCollecting());
755  assert(history.chunks.empty());
756 
757  // actual history transfer
758  history.swap(oldHistory);
759 
760  // resume collecting (and event recording)
761  collecting = true;
762  schedule(getCurrentTime());
763  motherBoard.getStateChangeDistributor().registerRecorder(*this);
764 
765  // start replaying events
766  replayIndex = oldEventCount;
767  // replay log contains at least the EndLogEvent
768  assert(replayIndex < history.events.size());
769  replayNextEvent();
770 }
771 
772 void ReverseManager::execNewSnapshot()
773 {
774  // During record we should take regular snapshots, and 'now'
775  // it's been a while since the last snapshot. But 'now' can be
776  // in the middle of a CPU instruction (1). However the CPU
777  // emulation code cannot handle taking snapshots at arbitrary
778  // moments in EmuTime (2)(3)(4). So instead we send out an
779  // event that indicates we want to take a snapshot (5).
780  // (1) Schedulables are executed at the exact requested
781  // EmuTime, even in the middle of a Z80 instruction.
782  // (2) The CPU code serializes all registers, current time and
783  // various other status info, but not enough info to be
784  // able to resume in the middle of an instruction.
785  // (3) Only the CPU has this limitation of not being able to
786  // take a snapshot at any EmuTime, all other devices can.
787  // This is because in our emulation model the CPU 'drives
788  // time forward'. It's the only device code that can be
789  // interrupted by other emulation code (via Schedulables).
790  // (4) In the past we had a CPU core that could execute/resume
791  // partial instructions (search SVN history). Though it was
792  // much more complex and it also ran slower than the
793  // current code.
794  // (5) Events are delivered from the Reactor code. That code
795  // only runs when the CPU code has exited (meaning no
796  // longer active in any stackframe). So it's executed right
797  // after the CPU has finished the current instruction. And
798  // that's OK, we only require regular snapshots here, they
799  // should not be *exactly* equally far apart in time.
800  pendingTakeSnapshot = true;
801  eventDistributor.distributeEvent(
802  std::make_shared<SimpleEvent>(OPENMSX_TAKE_REVERSE_SNAPSHOT));
803 }
804 
805 void ReverseManager::execInputEvent()
806 {
807  auto event = history.events[replayIndex];
808  try {
809  // deliver current event at current time
810  motherBoard.getStateChangeDistributor().distributeReplay(event);
811  } catch (MSXException&) {
812  // can throw in case we replay a command that fails
813  // ignore
814  }
815  if (!dynamic_cast<const EndLogEvent*>(event.get())) {
816  ++replayIndex;
817  replayNextEvent();
818  } else {
819  assert(!isReplaying()); // stopped by replay of EndLogEvent
820  }
821 }
822 
823 int ReverseManager::signalEvent(const shared_ptr<const Event>& event)
824 {
825  (void)event;
826  assert(event->getType() == OPENMSX_TAKE_REVERSE_SNAPSHOT);
827 
828  // This event is send to all MSX machines, make sure it's actually this
829  // machine that requested the snapshot.
830  if (pendingTakeSnapshot) {
831  pendingTakeSnapshot = false;
832  takeSnapshot(getCurrentTime());
833  // schedule creation of next snapshot
834  schedule(getCurrentTime());
835  }
836  return 0;
837 }
838 
839 unsigned ReverseManager::ReverseHistory::getNextSeqNum(EmuTime::param time) const
840 {
841  if (chunks.empty()) {
842  return 0;
843  }
844  const auto& startTime = begin(chunks)->second.time;
845  double duration = (time - startTime).toDouble();
846  return unsigned(duration / SNAPSHOT_PERIOD + 0.5);
847 }
848 
849 void ReverseManager::takeSnapshot(EmuTime::param time)
850 {
851  // (possibly) drop old snapshots
852  // TODO does snapshot pruning still happen correctly (often enough)
853  // when going back/forward in time?
854  unsigned seqNum = history.getNextSeqNum(time);
855  dropOldSnapshots<25>(seqNum);
856 
857  // During replay we might already have a snapshot with the current
858  // sequence number, though this snapshot does not necessarily have the
859  // exact same EmuTime (because we don't (re)start taking snapshots at
860  // the same moment in time).
861 
862  // actually create new snapshot
863  MemOutputArchive out;
864  out.serialize("machine", motherBoard);
865  ReverseChunk& newChunk = history.chunks[seqNum];
866  newChunk.time = time;
867  newChunk.savestate = out.releaseBuffer();
868  newChunk.eventCount = replayIndex;
869 }
870 
871 void ReverseManager::replayNextEvent()
872 {
873  // schedule next event at its own time
874  assert(replayIndex < history.events.size());
875  syncInputEvent.setSyncPoint(history.events[replayIndex]->getTime());
876 }
877 
878 void ReverseManager::signalStateChange(const shared_ptr<StateChange>& event)
879 {
880  if (isReplaying()) {
881  // this is an event we just replayed
882  assert(event == history.events[replayIndex]);
883  if (dynamic_cast<EndLogEvent*>(event.get())) {
884  signalStopReplay(event->getTime());
885  } else {
886  // ignore all other events
887  }
888  } else {
889  // record event
890  history.events.push_back(event);
891  ++replayIndex;
892  assert(!isReplaying());
893  }
894 }
895 
896 void ReverseManager::signalStopReplay(EmuTime::param time)
897 {
898  motherBoard.getStateChangeDistributor().stopReplay(time);
899  // this is needed to prevent a reRecordCount increase
900  // due to this action ending the replay
901  reRecordCount--;
902 }
903 
904 void ReverseManager::stopReplay(EmuTime::param time)
905 {
906  if (isReplaying()) {
907  // if we're replaying, stop it and erase remainder of event log
908  syncInputEvent.removeSyncPoint();
909  Events& events = history.events;
910  events.erase(begin(events) + replayIndex, end(events));
911  // search snapshots that are newer than 'time' and erase them
912  auto it = find_if(begin(history.chunks), end(history.chunks),
913  [&](Chunks::value_type& p) { return p.second.time > time; });
914  history.chunks.erase(it, end(history.chunks));
915  // this also means someone is changing history, record that
916  reRecordCount++;
917  }
918  assert(!isReplaying());
919 }
920 
921 /* Should be called each time a new snapshot is added.
922  * This function will erase zero or more earlier snapshots so that there are
923  * more snapshots of recent history and less of distant history. It has the
924  * following properties:
925  * - the very oldest snapshot is never deleted
926  * - it keeps the N or N+1 most recent snapshots (snapshot distance = 1)
927  * - then it keeps N or N+1 with snapshot distance 2
928  * - then N or N+1 with snapshot distance 4
929  * - ... and so on
930  * @param count The index of the just added (or about to be added) element.
931  * First element should have index 1.
932  */
933 template<unsigned N>
934 void ReverseManager::dropOldSnapshots(unsigned count)
935 {
936  unsigned y = (count + N) ^ (count + N + 1);
937  unsigned d = N;
938  unsigned d2 = 2 * N + 1;
939  while (true) {
940  y >>= 1;
941  if ((y == 0) || (count < d)) return;
942  history.chunks.erase(count - d);
943  d += d2;
944  d2 *= 2;
945  }
946 }
947 
948 void ReverseManager::schedule(EmuTime::param time)
949 {
950  syncNewSnapshot.setSyncPoint(time + EmuDuration(SNAPSHOT_PERIOD));
951 }
952 
953 
954 // class ReverseCmd
955 
956 ReverseManager::ReverseCmd::ReverseCmd(
957  ReverseManager& manager_, CommandController& controller)
958  : Command(controller, "reverse")
959  , manager(manager_)
960 {
961 }
962 
963 void ReverseManager::ReverseCmd::execute(array_ref<TclObject> tokens, TclObject& result)
964 {
965  if (tokens.size() < 2) {
966  throw CommandException("Missing subcommand");
967  }
968  auto& interp = getInterpreter();
969  string_ref subcommand = tokens[1].getString();
970  if (subcommand == "start") {
971  manager.start();
972  } else if (subcommand == "stop") {
973  manager.stop();
974  } else if (subcommand == "status") {
975  manager.status(result);
976  } else if (subcommand == "debug") {
977  manager.debugInfo(result);
978  } else if (subcommand == "goback") {
979  manager.goBack(tokens);
980  } else if (subcommand == "goto") {
981  manager.goTo(tokens);
982  } else if (subcommand == "savereplay") {
983  return manager.saveReplay(tokens, result);
984  } else if (subcommand == "loadreplay") {
985  return manager.loadReplay(interp, tokens, result);
986  } else if (subcommand == "viewonlymode") {
987  auto& distributor = manager.motherBoard.getStateChangeDistributor();
988  switch (tokens.size()) {
989  case 2:
990  result.setString(distributor.isViewOnlyMode() ? "true" : "false");
991  break;
992  case 3:
993  distributor.setViewOnlyMode(tokens[2].getBoolean(interp));
994  break;
995  default:
996  throw SyntaxError();
997  }
998  } else if (subcommand == "truncatereplay") {
999  if (manager.isReplaying()) {
1000  manager.signalStopReplay(manager.getCurrentTime());
1001  }
1002  } else {
1003  throw CommandException("Invalid subcommand: " + subcommand);
1004  }
1005 }
1006 
1007 string ReverseManager::ReverseCmd::help(const vector<string>& /*tokens*/) const
1008 {
1009  return "start start collecting reverse data\n"
1010  "stop stop collecting\n"
1011  "status show various status info on reverse\n"
1012  "goback <n> go back <n> seconds in time\n"
1013  "goto <time> go to an absolute moment in time\n"
1014  "viewonlymode <bool> switch viewonly mode on or off\n"
1015  "truncatereplay stop replaying and remove all 'future' data\n"
1016  "savereplay [<name>] save the first snapshot and all replay data as a 'replay' (with optional name)\n"
1017  "loadreplay [-goto <begin|end|savetime|<n>>] [-viewonly] <name> load a replay (snapshot and replay data) with given name and start replaying\n";
1018 }
1019 
1020 void ReverseManager::ReverseCmd::tabCompletion(vector<string>& tokens) const
1021 {
1022  if (tokens.size() == 2) {
1023  static const char* const subCommands[] = {
1024  "start", "stop", "status", "goback", "goto",
1025  "savereplay", "loadreplay", "viewonlymode",
1026  "truncatereplay",
1027  };
1028  completeString(tokens, subCommands);
1029  } else if ((tokens.size() == 3) || (tokens[1] == "loadreplay")) {
1030  if (tokens[1] == "loadreplay" || tokens[1] == "savereplay") {
1031  std::vector<const char*> cmds;
1032  if (tokens[1] == "loadreplay") {
1033  cmds = { "-goto", "-viewonly" };
1034  }
1035  UserDataFileContext context(REPLAY_DIR);
1036  completeFileName(tokens, context, cmds);
1037  } else if (tokens[1] == "viewonlymode") {
1038  static const char* const options[] = { "true", "false" };
1039  completeString(tokens, options);
1040  }
1041  }
1042 }
1043 
1044 } // 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