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