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