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