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