31 using std::shared_ptr;
37 static const double SNAPSHOT_PERIOD = 1.0;
40 static const unsigned MAX_NOF_SNAPSHOTS = 10;
45 static const char*
const REPLAY_DIR =
"replays";
70 template<
typename Archive>
73 if (ar.versionAtLeast(version, 2)) {
77 ar.serialize(
"snapshot", *newBoard);
81 ar.serialize(
"events", *
events);
83 if (ar.versionAtLeast(version, 3)) {
86 assert(ar.isLoader());
91 if (ar.versionAtLeast(version, 4)) {
103 virtual string help(
const vector<string>& tokens)
const;
112 void ReverseManager::ReverseHistory::swap(ReverseHistory& other)
114 std::swap(chunks, other.chunks);
115 std::swap(events, other.events);
118 void ReverseManager::ReverseHistory::clear()
121 Chunks().swap(chunks);
122 Events().swap(events);
128 ReverseManager::ReverseChunk::ReverseChunk()
133 ReverseManager::ReverseChunk::ReverseChunk(ReverseChunk&& rhs)
134 : time (move(rhs.time))
135 , savestate (move(rhs.savestate))
136 , eventCount(move(rhs.eventCount))
140 ReverseManager::ReverseChunk& ReverseManager::ReverseChunk::operator=(
143 time = move(rhs.time);
144 savestate = move(rhs.savestate);
145 eventCount = move(rhs.eventCount);
159 template<
typename Archive>
void serialize(Archive& ar,
unsigned )
161 ar.template serializeBase<StateChange>(*this);
175 , motherBoard(motherBoard_)
176 , eventDistributor(motherBoard.getReactor().getEventDistributor())
178 *this, motherBoard.getCommandController()))
180 , eventDelay(nullptr)
183 , pendingTakeSnapshot(false)
188 assert(!isCollecting());
189 assert(!isReplaying());
201 keyboard = &keyboard_;
205 eventDelay = &eventDelay_;
210 reRecordCount = reRecordCount_;
213 bool ReverseManager::isCollecting()
const
218 bool ReverseManager::isReplaying()
const
220 return replayIndex != history.events.size();
223 void ReverseManager::start()
225 if (!isCollecting()) {
232 assert(isCollecting());
235 void ReverseManager::stop()
237 if (isCollecting()) {
244 pendingTakeSnapshot =
false;
246 assert(!pendingTakeSnapshot);
247 assert(!isCollecting());
248 assert(!isReplaying());
251 EmuTime::param ReverseManager::getEndTime(
const ReverseHistory& history)
const
253 if (!history.events.empty()) {
254 if (
auto ev = dynamic_cast<const EndLogEvent*>(
255 history.events.back().get())) {
257 return ev->getTime();
261 assert(!isReplaying());
265 void ReverseManager::status(TclObject& result)
const
267 result.addListElement(
"status");
268 if (!isCollecting()) {
269 result.addListElement(
"disabled");
270 }
else if (isReplaying()) {
271 result.addListElement(
"replaying");
273 result.addListElement(
"enabled");
276 result.addListElement(
"begin");
277 EmuTime begin(isCollecting() ? history.chunks.begin()->second.time
281 result.addListElement(
"end");
285 result.addListElement(
"current");
289 result.addListElement(
"snapshots");
291 for (
auto& p : history.chunks) {
295 result.addListElement(snapshots);
298 void ReverseManager::debugInfo(TclObject& result)
const
303 size_t totalSize = 0;
304 for (
auto& p : history.chunks) {
305 auto& chunk = p.second;
306 res << p.first <<
' '
309 <<
" (" << chunk.savestate.size() <<
')'
310 <<
" (next event index: " << chunk.eventCount <<
")\n";
311 totalSize += chunk.savestate.size();
313 res <<
"total size: " << totalSize <<
'\n';
314 result.setString(
string(res));
317 static void parseGoTo(
const vector<TclObject>& tokens,
bool& novideo,
double& time)
320 bool hasTime =
false;
321 for (
auto i :
xrange(
size_t(2), tokens.size())) {
322 if (tokens[i].getString() ==
"-novideo") {
325 time = tokens[i].getDouble();
334 void ReverseManager::goBack(
const vector<TclObject>& tokens)
338 parseGoTo(tokens, novideo, t);
352 goTo(target, novideo);
355 void ReverseManager::goTo(
const std::vector<TclObject>& tokens)
359 parseGoTo(tokens, novideo, t);
362 goTo(target, novideo);
367 if (!isCollecting()) {
368 throw CommandException(
369 "Reverse was not enabled. First execute the 'reverse "
370 "start' command to start collecting data.");
372 goTo(target, novideo, history,
true);
375 void ReverseManager::goTo(
392 assert(!history.chunks.empty());
393 auto it = history.chunks.begin();
394 EmuTime firstTime = it->second.time;
395 EmuTime targetTime = std::max(target, firstTime);
397 targetTime = std::min(targetTime, getEndTime(history));
404 double dur2frames = 2.0 * (313.0 * 1368.0) / (3579545.0 * 6.0);
406 EmuTime preTarget = ((targetTime - firstTime) > preDelta)
407 ? targetTime - preDelta
412 assert(it->second.time <= preTarget);
413 assert(it != history.chunks.end());
416 }
while (it != history.chunks.end() &&
417 it->second.time <= preTarget);
420 assert(it != history.chunks.begin());
422 EmuTime snapshotTime = it->second.time;
423 assert(snapshotTime <= preTarget);
435 MSXMotherBoard* newBoard;
438 (currentTime <= preTarget) &&
439 ((snapshotTime <= currentTime) ||
441 newBoard = &motherBoard;
445 newBoard_ = reactor.createEmptyMotherBoard();
446 newBoard = newBoard_.get();
447 MemInputArchive in(it->second.savestate.data(),
448 it->second.savestate.size());
459 if (history.events.empty() ||
460 !
dynamic_cast<const EndLogEvent*
>(history.events.back().get())) {
461 history.events.push_back(
462 std::make_shared<EndLogEvent>(currentTime));
468 auto& newManager = newBoard->getReverseManager();
469 newManager.transferHistory(history, it->second.eventCount);
472 transferState(*newBoard);
481 newBoard->fastForward(preTarget,
true);
487 if (newBoard_.get()) {
489 reactor.replaceBoard(motherBoard, move(newBoard_));
494 newBoard->fastForward(targetTime,
false);
503 assert(newBoard->getReverseManager().isCollecting());
504 }
catch (MSXException&) {
511 void ReverseManager::transferState(MSXMotherBoard& newBoard)
515 auto& newDistributor = newBoard .getStateChangeDistributor();
519 auto& newManager = newBoard.getReverseManager();
520 if (newManager.keyboard && keyboard) {
521 newManager.keyboard->transferHostKeyMatrix(*keyboard);
525 newBoard.getDebugger().transfer(motherBoard.
getDebugger());
528 newManager.reRecordCount = reRecordCount;
535 void ReverseManager::saveReplay(
const vector<TclObject>& tokens, TclObject& result)
537 const auto& chunks = history.chunks;
538 if (chunks.empty()) {
539 throw CommandException(
"No recording...");
543 switch (tokens.size()) {
548 filename = tokens[2].getString().str();
554 filename, REPLAY_DIR,
"openmsx",
".omr");
558 replay.reRecordCount = reRecordCount;
565 auto initialBoard = reactor.createEmptyMotherBoard();
566 MemInputArchive in(chunks.begin()->second.savestate.data(),
567 chunks.begin()->second.savestate.size());
568 in.serialize(
"machine", *initialBoard);
569 replay.motherBoards.push_back(move(initialBoard));
572 const auto& startTime = chunks.begin()->second.time;
573 const auto& endTime = chunks.rbegin()->second.time;
575 EmuDuration partitionLength = totalLength.divRoundUp(MAX_NOF_SNAPSHOTS);
576 partitionLength = std::max(MIN_PARTITION_LENGTH, partitionLength);
577 EmuTime nextPartitionEnd = startTime + partitionLength;
578 auto it = chunks.begin();
579 auto lastAddedIt = chunks.begin();
580 while (it != chunks.end()) {
582 if (it == chunks.end() || (it->second.time > nextPartitionEnd)) {
584 assert(it->second.time <= nextPartitionEnd);
585 if (it != lastAddedIt) {
588 MemInputArchive in(it->second.savestate.data(),
589 it->second.savestate.size());
590 in.serialize(
"machine", *board);
591 replay.motherBoards.push_back(move(board));
595 while (it != chunks.end() && it->second.time > nextPartitionEnd) {
596 nextPartitionEnd += partitionLength;
600 assert(lastAddedIt == --chunks.end());
603 bool addSentinel = history.events.empty() ||
604 !
dynamic_cast<EndLogEvent*
>(history.events.back().get());
607 history.events.push_back(std::make_shared<EndLogEvent>(
611 XmlOutputArchive out(filename);
612 replay.events = &history.events;
613 out.serialize(
"replay", replay);
614 }
catch (MSXException&) {
616 history.events.pop_back();
626 history.events.pop_back();
629 result.setString(
"Saved replay to " + filename);
632 void ReverseManager::loadReplay(
const vector<TclObject>& tokens, TclObject& result)
634 if (tokens.size() < 3)
throw SyntaxError();
636 vector<string> arguments;
637 const TclObject* whereArg =
nullptr;
638 bool enableViewOnly =
false;
640 for (
auto i :
xrange(
size_t(2), tokens.size())) {
642 if (token ==
"-viewonly") {
643 enableViewOnly =
true;
644 }
else if (token ==
"-goto") {
645 if (++i == tokens.size()) {
646 throw CommandException(
"Missing argument");
648 whereArg = &tokens[i];
650 arguments.push_back(token.
str());
654 if (arguments.size() != 1)
throw SyntaxError();
657 UserDataFileContext context(REPLAY_DIR);
658 string fileNameArg = arguments[0];
662 filename = context.resolve(fileNameArg);
663 }
catch (MSXException& ) {
try {
665 filename = context.resolve(fileNameArg +
".omr");
666 }
catch (MSXException& e2) {
try {
669 filename = context.resolve(fileNameArg +
".gz");
670 }
catch (MSXException& ) {
679 replay.events = &events;
681 XmlInputArchive in(filename);
682 in.serialize(
"replay", replay);
683 }
catch (XMLException& e) {
684 throw CommandException(
"Cannot load replay, bad file format: " + e.getMessage());
685 }
catch (MSXException& e) {
686 throw CommandException(
"Cannot load replay: " + e.getMessage());
691 string_ref where = whereArg ? whereArg->getString() :
"begin";
692 if (where ==
"begin") {
694 }
else if (where ==
"end") {
696 }
else if (where ==
"savetime") {
697 destination = replay.currentTime;
707 assert(!replay.motherBoards.empty());
708 auto& newReverseManager = replay.motherBoards[0]->getReverseManager();
709 auto& newHistory = newReverseManager.history;
711 if (newReverseManager.reRecordCount == 0) {
713 newReverseManager.reRecordCount = replay.reRecordCount;
720 swap(newHistory.events, events);
721 auto& newEvents = newHistory.events;
724 unsigned replayIndex = 0;
725 for (
auto& m : replay.motherBoards) {
726 ReverseChunk newChunk;
727 newChunk.time = m->getCurrentTime();
729 MemOutputArchive out;
730 out.serialize(
"machine", *m);
731 newChunk.savestate = out.releaseBuffer();
735 while (replayIndex < newEvents.size() &&
736 (newEvents[replayIndex]->getTime() < newChunk.time)) {
739 newChunk.eventCount = replayIndex;
741 newHistory.chunks[newHistory.getNextSeqNum(newChunk.time)] =
747 reRecordCount = newReverseManager.reRecordCount;
748 bool novideo =
false;
749 goTo(destination, novideo, newHistory,
false);
751 result.setString(
"Loaded replay from " + filename);
754 void ReverseManager::transferHistory(ReverseHistory& oldHistory,
755 unsigned oldEventCount)
757 assert(!isCollecting());
758 assert(history.chunks.empty());
761 history.swap(oldHistory);
769 replayIndex = oldEventCount;
771 assert(replayIndex < history.events.size());
805 pendingTakeSnapshot =
true;
810 auto event = history.events[replayIndex];
814 }
catch (MSXException&) {
818 if (!dynamic_cast<const EndLogEvent*>(event.get())) {
822 assert(!isReplaying());
828 int ReverseManager::signalEvent(
const shared_ptr<const Event>& event)
835 if (pendingTakeSnapshot) {
836 pendingTakeSnapshot =
false;
842 unsigned ReverseManager::ReverseHistory::getNextSeqNum(
EmuTime::param time)
const
844 if (chunks.empty()) {
847 const auto& startTime = chunks.begin()->second.time;
848 double duration = (time - startTime).toDouble();
849 return unsigned(duration / SNAPSHOT_PERIOD + 0.5) + 1;
857 unsigned seqNum = history.getNextSeqNum(time);
858 dropOldSnapshots<25>(seqNum);
866 MemOutputArchive out;
867 out.serialize(
"machine", motherBoard);
868 ReverseChunk& newChunk = history.chunks[seqNum];
869 newChunk.time = time;
870 newChunk.savestate = out.releaseBuffer();
871 newChunk.eventCount = replayIndex;
877 void ReverseManager::replayNextEvent()
880 assert(replayIndex < history.events.size());
884 void ReverseManager::signalStateChange(
const shared_ptr<StateChange>& event)
888 assert(event == history.events[replayIndex]);
889 if (dynamic_cast<EndLogEvent*>(event.get())) {
890 signalStopReplay(event->getTime());
896 history.events.push_back(event);
898 assert(!isReplaying());
915 Events& events = history.events;
916 events.erase(events.begin() + replayIndex, events.end());
918 auto it = history.chunks.begin();
919 while ((it != history.chunks.end()) &&
920 (it->second.time <= time)) {
923 history.chunks.erase(it, history.chunks.end());
927 assert(!isReplaying());
943 void ReverseManager::dropOldSnapshots(
unsigned count)
945 unsigned y = (count + N - 1) ^ (count + N);
947 unsigned d2 = 2 * N + 1;
950 if ((y == 0) || (count <= d))
return;
951 history.chunks.erase(count - d);
966 :
Command(controller,
"reverse")
973 if (tokens.size() < 2) {
976 string_ref subcommand = tokens[1].getString();
977 if (subcommand ==
"start") {
979 }
else if (subcommand ==
"stop") {
981 }
else if (subcommand ==
"status") {
982 manager.status(result);
983 }
else if (subcommand ==
"debug") {
984 manager.debugInfo(result);
985 }
else if (subcommand ==
"goback") {
986 manager.goBack(tokens);
987 }
else if (subcommand ==
"goto") {
988 manager.goTo(tokens);
989 }
else if (subcommand ==
"savereplay") {
990 return manager.saveReplay(tokens, result);
991 }
else if (subcommand ==
"loadreplay") {
992 return manager.loadReplay(tokens, result);
993 }
else if (subcommand ==
"viewonlymode") {
995 switch (tokens.size()) {
997 result.
setString(distributor.isViewOnlyMode() ?
"true" :
"false");
1000 distributor.setViewOnlyMode(tokens[2].getBoolean());
1005 }
else if (subcommand ==
"truncatereplay") {
1006 if (manager.isReplaying()) {
1016 return "start start collecting reverse data\n"
1017 "stop stop collecting\n"
1018 "status show various status info on reverse\n"
1019 "goback <n> go back <n> seconds in time\n"
1020 "goto <time> go to an absolute moment in time\n"
1021 "viewonlymode <bool> switch viewonly mode on or off\n"
1022 "truncatereplay stop replaying and remove all 'future' data\n"
1023 "savereplay [<name>] save the first snapshot and all replay data as a 'replay' (with optional name)\n"
1024 "loadreplay [-goto <begin|end|savetime|<n>>] [-viewonly] <name> load a replay (snapshot and replay data) with given name and start replaying\n";
1029 if (tokens.size() == 2) {
1030 static const char*
const subCommands[] = {
1031 "start",
"stop",
"status",
"goback",
"goto",
1032 "savereplay",
"loadreplay",
"viewonlymode",
1036 }
else if ((tokens.size() == 3) || (tokens[1] ==
"loadreplay")) {
1037 if (tokens[1] ==
"loadreplay" || tokens[1] ==
"savereplay") {
1038 std::vector<const char*> cmds;
1039 if (tokens[1] ==
"loadreplay") {
1040 cmds.push_back(
"-goto");
1041 cmds.push_back(
"-viewonly");
1045 }
else if (tokens[1] ==
"viewonlymode") {
1046 static const char*
const options[] = {
"true",
"false" };