40static constexpr double SNAPSHOT_PERIOD = 1.0;
43static constexpr unsigned MAX_NOF_SNAPSHOTS = 10;
46static constexpr auto MIN_PARTITION_LENGTH = EmuDuration(60.0);
49static constexpr auto MAX_DIST_1_BEFORE_LAST_SNAPSHOT = EmuDuration(30.0);
74 template<
typename Archive>
77 if (ar.versionAtLeast(version, 2)) {
81 ar.serialize(
"snapshot", *newBoard);
85 ar.serialize(
"events", *
events);
87 if (ar.versionAtLeast(version, 3)) {
90 assert(Archive::IS_LOADER);
95 if (ar.versionAtLeast(version, 4)) {
105void ReverseManager::ReverseHistory::swap(ReverseHistory& other)
noexcept
107 std::swap(chunks, other.chunks);
108 std::swap(events, other.events);
111void ReverseManager::ReverseHistory::clear()
114 Chunks().swap(chunks);
115 Events().swap(events);
128 template<
typename Archive>
void serialize(Archive& ar,
unsigned )
130 ar.template serializeBase<StateChange>(*
this);
138 : syncNewSnapshot(motherBoard_.getScheduler())
139 , syncInputEvent (motherBoard_.getScheduler())
140 , motherBoard(motherBoard_)
141 , eventDistributor(motherBoard.getReactor().getEventDistributor())
142 , reverseCmd(motherBoard.getCommandController())
158 return replayIndex != history.events.size();
161void ReverseManager::start()
166 takeSnapshot(getCurrentTime());
168 schedule(getCurrentTime());
175void ReverseManager::stop()
179 syncNewSnapshot.removeSyncPoint();
180 syncInputEvent .removeSyncPoint();
184 pendingTakeSnapshot =
false;
186 assert(!pendingTakeSnapshot);
191EmuTime::param ReverseManager::getEndTime(
const ReverseHistory& hist)
const
193 if (!hist.events.empty()) {
194 if (
const auto* ev =
dynamic_cast<const EndLogEvent*
>(
195 hist.events.back().get())) {
197 return ev->getTime();
202 return getCurrentTime();
213 return (b - EmuTime::zero()).toDouble();
218 return (
end - EmuTime::zero()).toDouble();
222 EmuTime current(
isCollecting() ? getCurrentTime() : EmuTime::zero());
223 return (current - EmuTime::zero()).toDouble();
226void ReverseManager::status(
TclObject& result)
const
237 return (p.second.time - EmuTime::zero()).toDouble();
241 auto lastEvent = rbegin(history.events);
242 if (lastEvent != rend(history.events) &&
dynamic_cast<const EndLogEvent*
>(lastEvent->get())) {
245 EmuTime le(
isCollecting() && (lastEvent != rend(history.events)) ? (*lastEvent)->getTime() : EmuTime::zero());
246 result.
addDictKeyValue(
"last_event", (le - EmuTime::zero()).toDouble());
249void ReverseManager::debugInfo(TclObject& result)
const
254 size_t totalSize = 0;
255 for (
const auto& [idx, chunk] : history.chunks) {
257 (chunk.time - EmuTime::zero()).toDouble(),
' ',
258 ((chunk.time - EmuTime::zero()).toDouble() / (getCurrentTime() - EmuTime::zero()).toDouble()) * 100,
"%"
259 " (", chunk.size,
")"
260 " (next event index: ", chunk.eventCount,
")\n");
261 totalSize += chunk.size;
263 strAppend(res,
"total size: ", totalSize,
'\n');
267static std::pair<bool, double> parseGoTo(Interpreter& interp, std::span<const TclObject> tokens)
269 bool noVideo =
false;
270 std::array info = {
flagArg(
"-novideo", noVideo)};
271 auto args =
parseTclArgs(interp, tokens.subspan(2), info);
272 if (args.size() != 1)
throw SyntaxError();
273 double time = args[0].getDouble(interp);
274 return {noVideo, time};
277void ReverseManager::goBack(std::span<const TclObject> tokens)
280 auto [noVideo,
t] = parseGoTo(interp, tokens);
282 EmuTime now = getCurrentTime();
283 EmuTime target(EmuTime::dummy());
286 if (d < (now - EmuTime::zero())) {
289 target = EmuTime::zero();
292 target = now + EmuDuration(-
t);
294 goTo(target, noVideo);
297void ReverseManager::goTo(std::span<const TclObject> tokens)
300 auto [noVideo,
t] = parseGoTo(interp, tokens);
302 EmuTime target = EmuTime::zero() + EmuDuration(
t);
303 goTo(target, noVideo);
306void ReverseManager::goTo(EmuTime::param target,
bool noVideo)
309 throw CommandException(
310 "Reverse was not enabled. First execute the 'reverse "
311 "start' command to start collecting data.");
313 goTo(target, noVideo, history,
true);
317static void reportProgress(Reactor& reactor,
const EmuTime& targetTime,
float fraction)
319 double targetTimeDisp = (targetTime - EmuTime::zero()).toDouble();
320 std::ostringstream sstr;
321 sstr <<
"Time warping to " <<
322 int(targetTimeDisp / 60) <<
':' << std::setfill(
'0') <<
323 std::setw(5) << std::setprecision(2) << std::fixed <<
324 std::fmod(targetTimeDisp, 60.0);
325 reactor.getCliComm().printProgress(sstr.str(), fraction);
326 reactor.getDisplay().repaint();
329void ReverseManager::goTo(
330 EmuTime::param target,
bool noVideo, ReverseHistory& hist,
346 assert(!hist.chunks.empty());
347 auto it =
begin(hist.chunks);
348 EmuTime firstTime = it->second.time;
349 EmuTime targetTime = std::max(target, firstTime);
351 targetTime = std::min(targetTime, getEndTime(hist));
358 static constexpr double dur2frames = 2.0 * (313.0 * 1368.0) / (3579545.0 * 6.0);
359 EmuDuration preDelta(noVideo ? 0.0 : dur2frames);
360 EmuTime preTarget = ((targetTime - firstTime) > preDelta)
361 ? targetTime - preDelta
366 assert(it->second.time <= preTarget);
367 assert(it !=
end(hist.chunks));
370 }
while (it !=
end(hist.chunks) &&
371 it->second.time <= preTarget);
374 assert(it !=
begin(hist.chunks));
376 ReverseChunk& chunk = it->second;
377 EmuTime snapshotTime = chunk.time;
378 assert(snapshotTime <= preTarget);
389 EmuTime currentTime = getCurrentTime();
390 MSXMotherBoard* newBoard;
393 (currentTime <= preTarget) &&
394 ((snapshotTime <= currentTime) ||
395 ((preTarget - currentTime) < EmuDuration(1.0)))) {
396 newBoard = &motherBoard;
403 newBoard_ = reactor.createEmptyMotherBoard();
404 newBoard = newBoard_.get();
407 newBoard->getMSXCliComm().setSuppressMessages(
true);
408 MemInputArchive in(chunk.savestate.data(),
411 in.serialize(
"machine", *newBoard);
421 if (hist.events.empty() ||
422 !
dynamic_cast<const EndLogEvent*
>(hist.events.back().get())) {
423 hist.events.push_back(
424 std::make_unique<EndLogEvent>(currentTime));
430 auto& newManager = newBoard->getReverseManager();
431 newManager.transferHistory(hist, chunk.eventCount);
434 transferState(*newBoard);
448 auto startMSXTime = newBoard->getCurrentTime();
449 auto lastSnapshotTarget = startMSXTime;
450 bool everShowedProgress =
false;
451 syncNewSnapshot.removeSyncPoint();
453 auto currentTimeNewBoard = newBoard->getCurrentTime();
454 auto nextSnapshotTarget = std::min(
456 lastSnapshotTarget + std::max(
457 EmuDuration(SNAPSHOT_PERIOD),
458 (preTarget - lastSnapshotTarget) / 2
460 auto nextTarget = std::min(nextSnapshotTarget, currentTimeNewBoard +
EmuDuration::sec(1));
461 newBoard->fastForward(nextTarget,
true);
463 ((now - lastProgress) > 1000000) || ((currentTimeNewBoard >= preTarget) && everShowedProgress)) {
464 everShowedProgress =
true;
466 auto fraction = (currentTimeNewBoard - startMSXTime).toDouble() / (preTarget - startMSXTime).toDouble();
467 reportProgress(newBoard->getReactor(), targetTime,
float(fraction));
471 if (currentTimeNewBoard >= preTarget)
break;
472 if (currentTimeNewBoard >= nextSnapshotTarget) {
479 newBoard->getReverseManager().takeSnapshot(currentTimeNewBoard);
480 lastSnapshotTarget = nextSnapshotTarget;
484 newBoard->getMSXCliComm().setSuppressMessages(
false);
486 schedule(getCurrentTime());
494 reactor.replaceBoard(motherBoard, std::move(newBoard_));
499 newBoard->fastForward(targetTime,
false);
508 assert(newBoard->getReverseManager().isCollecting());
509 }
catch (MSXException&) {
516void ReverseManager::transferState(MSXMotherBoard& newBoard)
520 auto& newDistributor = newBoard .getStateChangeDistributor();
524 auto& newManager = newBoard.getReverseManager();
525 if (
auto* newKeyb = newManager.motherBoard.getKeyboard()) {
526 if (
const auto* oldKeyb = motherBoard.
getKeyboard()) {
527 newKeyb->transferHostKeyMatrix(*oldKeyb);
532 newBoard.getDebugger().transfer(motherBoard.
getDebugger());
535 newManager.reRecordCount = reRecordCount;
542void ReverseManager::saveReplay(
543 Interpreter& interp, std::span<const TclObject> tokens, TclObject& result)
545 const auto& chunks = history.chunks;
546 if (chunks.empty()) {
547 throw CommandException(
"No recording...");
550 std::string_view filenameArg;
551 int maxNofExtraSnapshots = MAX_NOF_SNAPSHOTS;
552 std::array info = {
valueArg(
"-maxnofextrasnapshots", maxNofExtraSnapshots)};
553 auto args =
parseTclArgs(interp, tokens.subspan(2), info);
554 switch (args.size()) {
556 case 1: filenameArg = args[0].getString();
break;
557 default:
throw SyntaxError();
559 if (maxNofExtraSnapshots < 0) {
560 throw CommandException(
"Maximum number of snapshots should be at least 0");
568 replay.reRecordCount = reRecordCount;
572 replay.currentTime = getCurrentTime();
575 auto initialBoard = reactor.createEmptyMotherBoard();
576 MemInputArchive in(
begin(chunks)->second.savestate.data(),
577 begin(chunks)->second.size,
578 begin(chunks)->second.deltaBlocks);
579 in.serialize(
"machine", *initialBoard);
580 replay.motherBoards.push_back(std::move(initialBoard));
582 if (maxNofExtraSnapshots > 0) {
584 const auto& startTime =
begin(chunks)->second.time;
589 const auto& lastChunkTime = rbegin(chunks)->second.time;
590 const auto& endTime = ((startTime + MAX_DIST_1_BEFORE_LAST_SNAPSHOT) < lastChunkTime) ? lastChunkTime - MAX_DIST_1_BEFORE_LAST_SNAPSHOT : lastChunkTime;
591 EmuDuration totalLength = endTime - startTime;
592 EmuDuration partitionLength = totalLength.divRoundUp(maxNofExtraSnapshots);
593 partitionLength = std::max(MIN_PARTITION_LENGTH, partitionLength);
594 EmuTime nextPartitionEnd = startTime + partitionLength;
595 auto it =
begin(chunks);
596 auto lastAddedIt =
begin(chunks);
597 while (it !=
end(chunks)) {
599 if (it ==
end(chunks) || (it->second.time > nextPartitionEnd)) {
601 assert(it->second.time <= nextPartitionEnd);
602 if (it != lastAddedIt) {
605 MemInputArchive in2(it->second.savestate.data(),
607 it->second.deltaBlocks);
608 in2.serialize(
"machine", *board);
609 replay.motherBoards.push_back(std::move(board));
613 while (it !=
end(chunks) && it->second.time > nextPartitionEnd) {
614 nextPartitionEnd += partitionLength;
618 assert(lastAddedIt == std::prev(
end(chunks)));
622 bool addSentinel = history.events.empty() ||
623 !
dynamic_cast<EndLogEvent*
>(history.events.back().get());
626 history.events.push_back(std::make_unique<EndLogEvent>(
630 XmlOutputArchive out(filename);
631 replay.events = &history.events;
632 out.serialize(
"replay", replay);
634 }
catch (MSXException&) {
636 history.events.pop_back();
646 history.events.pop_back();
649 result =
tmpStrCat(
"Saved replay to ", filename);
652void ReverseManager::loadReplay(
653 Interpreter& interp, std::span<const TclObject> tokens, TclObject& result)
655 bool enableViewOnly =
false;
656 std::optional<TclObject> where;
658 flagArg(
"-viewonly", enableViewOnly),
661 auto arguments =
parseTclArgs(interp, tokens.subspan(2), info);
662 if (arguments.size() != 1)
throw SyntaxError();
666 std::string fileNameArg(arguments[0].getString());
667 std::string filename;
670 filename =
context.resolve(fileNameArg);
671 }
catch (MSXException& ) {
try {
674 }
catch (MSXException& e2) {
try {
678 }
catch (MSXException& ) {
687 replay.events = &events;
689 XmlInputArchive in(filename);
690 in.serialize(
"replay", replay);
691 }
catch (XMLException& e) {
692 throw CommandException(
"Cannot load replay, bad file format: ",
694 }
catch (MSXException& e) {
695 throw CommandException(
"Cannot load replay: ",
e.getMessage());
699 auto destination = EmuTime::zero();
700 if (!where || (*where ==
"begin")) {
701 destination = EmuTime::zero();
702 }
else if (*where ==
"end") {
703 destination = EmuTime::infinity();
704 }
else if (*where ==
"savetime") {
705 destination = replay.currentTime;
707 destination += EmuDuration(where->getDouble(interp));
715 assert(!replay.motherBoards.empty());
716 auto& newReverseManager = replay.motherBoards[0]->getReverseManager();
717 auto& newHistory = newReverseManager.history;
719 if (newReverseManager.reRecordCount == 0) {
721 newReverseManager.reRecordCount = replay.reRecordCount;
728 swap(newHistory.events, events);
729 auto& newEvents = newHistory.events;
732 unsigned replayIdx = 0;
733 for (
const auto& m : replay.motherBoards) {
734 ReverseChunk newChunk;
735 newChunk.time = m->getCurrentTime();
737 MemOutputArchive out(newHistory.lastDeltaBlocks,
738 newChunk.deltaBlocks,
false);
739 out.serialize(
"machine", *m);
740 newChunk.savestate = out.releaseBuffer(newChunk.size);
744 while (replayIdx < newEvents.size() &&
745 (newEvents[replayIdx]->getTime() < newChunk.time)) {
748 newChunk.eventCount = replayIdx;
750 newHistory.chunks[newHistory.getNextSeqNum(newChunk.time)] =
756 reRecordCount = newReverseManager.reRecordCount;
757 bool noVideo =
false;
758 goTo(destination, noVideo, newHistory,
false);
760 result =
tmpStrCat(
"Loaded replay from ", filename);
763void ReverseManager::transferHistory(ReverseHistory& oldHistory,
764 unsigned oldEventCount)
767 assert(history.chunks.empty());
770 oldHistory.lastDeltaBlocks.clear();
773 history.swap(oldHistory);
777 schedule(getCurrentTime());
781 replayIndex = oldEventCount;
783 assert(replayIndex < history.events.size());
787void ReverseManager::execNewSnapshot()
815 pendingTakeSnapshot =
true;
819void ReverseManager::execInputEvent()
821 const auto&
event = *history.events[replayIndex];
825 }
catch (MSXException&) {
829 if (!
dynamic_cast<const EndLogEvent*
>(&event)) {
833 signalStopReplay(event.getTime());
838bool ReverseManager::signalEvent(
const Event& event)
845 if (pendingTakeSnapshot) {
846 pendingTakeSnapshot =
false;
847 takeSnapshot(getCurrentTime());
849 schedule(getCurrentTime());
854unsigned ReverseManager::ReverseHistory::getNextSeqNum(EmuTime::param time)
const
856 if (chunks.empty()) {
859 const auto& startTime =
begin(chunks)->second.time;
860 double duration = (time - startTime).toDouble();
861 return narrow<unsigned>(lrint(duration / SNAPSHOT_PERIOD));
864void ReverseManager::takeSnapshot(EmuTime::param time)
869 unsigned seqNum = history.getNextSeqNum(time);
870 dropOldSnapshots<25>(seqNum);
878 ReverseChunk& newChunk = history.chunks[seqNum];
879 newChunk.deltaBlocks.clear();
880 MemOutputArchive out(history.lastDeltaBlocks, newChunk.deltaBlocks,
true);
881 out.serialize(
"machine", motherBoard);
882 newChunk.time = time;
883 newChunk.savestate = out.releaseBuffer(newChunk.size);
884 newChunk.eventCount = replayIndex;
887void ReverseManager::replayNextEvent()
890 assert(replayIndex < history.events.size());
891 syncInputEvent.setSyncPoint(history.events[replayIndex]->getTime());
894void ReverseManager::signalStopReplay(EmuTime::param time)
906 syncInputEvent.removeSyncPoint();
907 Events& events = history.events;
908 events.erase(
begin(events) + replayIndex,
end(events));
911 return p.second.time > time;
913 history.chunks.erase(it,
end(history.chunks));
917 assert(!isReplaying());
933void ReverseManager::dropOldSnapshots(
unsigned count)
935 unsigned y = (count + N) ^ (count + N + 1);
937 unsigned d2 = 2 * N + 1;
940 if ((y == 0) || (count < d))
return;
941 history.chunks.erase(count - d);
947void ReverseManager::schedule(EmuTime::param time)
949 syncNewSnapshot.setSyncPoint(time + EmuDuration(SNAPSHOT_PERIOD));
955ReverseManager::ReverseCmd::ReverseCmd(CommandController& controller)
956 : Command(controller,
"reverse")
960void ReverseManager::ReverseCmd::execute(std::span<const TclObject> tokens, TclObject& result)
962 checkNumArgs(tokens, AtLeast{2},
"subcommand ?arg ...?");
963 auto& manager =
OUTER(ReverseManager, reverseCmd);
964 auto& interp = getInterpreter();
965 executeSubCommand(tokens[1].getString(),
966 "start", [&]{ manager.start(); },
967 "stop", [&]{ manager.stop(); },
968 "status", [&]{ manager.status(result); },
969 "debug", [&]{ manager.debugInfo(result); },
970 "goback", [&]{ manager.goBack(tokens); },
971 "goto", [&]{ manager.goTo(tokens); },
972 "savereplay", [&]{ manager.saveReplay(interp, tokens, result); },
973 "loadreplay", [&]{ manager.loadReplay(interp, tokens, result); },
975 auto& distributor = manager.motherBoard.getStateChangeDistributor();
976 switch (tokens.size()) {
978 result = distributor.isViewOnlyMode();
981 distributor.setViewOnlyMode(tokens[2].getBoolean(interp));
986 "truncatereplay", [&] {
987 if (manager.isReplaying()) {
988 manager.signalStopReplay(manager.getCurrentTime());
992std::string ReverseManager::ReverseCmd::help(std::span<const TclObject> )
const
994 return "start start collecting reverse data\n"
995 "stop stop collecting\n"
996 "status show various status info on reverse\n"
997 "goback <n> go back <n> seconds in time\n"
998 "goto <time> go to an absolute moment in time\n"
999 "viewonlymode <bool> switch viewonly mode on or off\n"
1000 "truncatereplay stop replaying and remove all 'future' data\n"
1001 "savereplay [<name>] save the first snapshot and all replay data as a 'replay' (with optional name)\n"
1002 "loadreplay [-goto <begin|end|savetime|<n>>] [-viewonly] <name> load a replay (snapshot and replay data) with given name and start replaying\n";
1005void ReverseManager::ReverseCmd::tabCompletion(std::vector<std::string>& tokens)
const
1007 using namespace std::literals;
1008 if (tokens.size() == 2) {
1009 static constexpr std::array subCommands = {
1010 "start"sv,
"stop"sv,
"status"sv,
"goback"sv,
"goto"sv,
1011 "savereplay"sv,
"loadreplay"sv,
"viewonlymode"sv,
1014 completeString(tokens, subCommands);
1015 }
else if ((tokens.size() == 3) || (tokens[1] ==
"loadreplay")) {
1016 if (tokens[1] ==
one_of(
"loadreplay",
"savereplay")) {
1017 static constexpr std::array cmds = {
"-goto"sv,
"-viewonly"sv};
1019 (tokens[1] ==
"loadreplay") ? cmds :
std::span<const
std::string_view>{});
1020 }
else if (tokens[1] ==
"viewonlymode") {
1021 static constexpr std::array options = {
"true"sv,
"false"sv};
1022 completeString(tokens, options);
static constexpr EmuDuration sec(unsigned x)
void serialize(Archive &ar, unsigned)
EndLogEvent(EmuTime::param time_)
void unregisterEventListener(EventType type, EventListener &listener)
Unregisters a previously registered event listener.
void distributeEvent(Event &&event)
Schedule the given event for delivery.
void registerEventListener(EventType type, EventListener &listener, Priority priority=Priority::OTHER)
Registers a given object to receive certain events.
void setSuppressMessages(bool enable)
void transferSettings(const MSXCommandController &from)
Transfer setting values from one machine to another, used for during 'reverse'.
void mute()
TODO This methods (un)mute the sound.
StateChangeDistributor & getStateChangeDistributor()
Keyboard * getKeyboard() const
MSXCliComm & getMSXCliComm()
MSXCommandController & getMSXCommandController()
Contains the main loop of openMSX.
std::shared_ptr< MSXMotherBoard > Board
Board createEmptyMotherBoard()
Interpreter & getInterpreter()
bool isCollecting() const
double getCurrent() const
void stopReplay(EmuTime::param time) noexcept
static constexpr std::string_view REPLAY_DIR
bool isViewOnlyMode() const
static constexpr std::string_view REPLAY_EXTENSION
ReverseManager(MSXMotherBoard &motherBoard)
void unregisterRecorder(ReverseManager &recorder)
void registerRecorder(ReverseManager &recorder)
(Un)registers the given object to receive state change events.
void distributeReplay(const StateChange &event) const
bool isViewOnlyMode() const
void setViewOnlyMode(bool value)
Set viewOnlyMode.
void stopReplay(EmuTime::param time)
Explicitly stop replay.
Base class for all external MSX state changing events.
void addListElements(ITER first, ITER last)
void addDictKeyValue(const Key &key, const Value &value)
std::optional< Context > context
string parseCommandFileArgument(string_view argument, string_view directory, string_view prefix, string_view extension)
Helper function for parsing filename arguments in Tcl commands.
uint64_t getTime()
Get current (real) time in us.
This file implemented 3 utility functions:
ArgsInfo valueArg(std::string_view name, T &value)
std::vector< TclObject > parseTclArgs(Interpreter &interp, std::span< const TclObject > inArgs, std::span< const ArgsInfo > table)
EventType getType(const Event &event)
std::variant< KeyUpEvent, KeyDownEvent, MouseMotionEvent, MouseButtonUpEvent, MouseButtonDownEvent, MouseWheelEvent, JoystickAxisMotionEvent, JoystickHatEvent, JoystickButtonUpEvent, JoystickButtonDownEvent, OsdControlReleaseEvent, OsdControlPressEvent, WindowEvent, TextEvent, FileDropEvent, QuitEvent, FinishFrameEvent, CliCommandEvent, GroupEvent, BootEvent, FrameDrawnEvent, BreakEvent, SwitchRendererEvent, TakeReverseSnapshotEvent, AfterTimedEvent, MachineLoadedEvent, MachineActivatedEvent, MachineDeactivatedEvent, MidiInReaderEvent, MidiInWindowsEvent, MidiInCoreMidiEvent, MidiInCoreMidiVirtualEvent, MidiInALSAEvent, Rs232TesterEvent, Rs232NetEvent, ImGuiDelayedActionEvent, ImGuiActiveEvent > Event
ArgsInfo flagArg(std::string_view name, bool &flag)
FileContext userDataFileContext(string_view subDir)
auto find_if(InputRange &&range, UnaryPredicate pred)
constexpr auto transform(Range &&range, UnaryOp op)
#define OUTER(type, member)
TemporaryString tmpStrCat(Ts &&... ts)
void strAppend(std::string &result, Ts &&...ts)
std::vector< Reactor::Board > motherBoards
void serialize(Archive &ar, unsigned version)
Replay(Reactor &reactor_)
ReverseManager::Events * events
constexpr auto begin(const zstring_view &x)
constexpr auto end(const zstring_view &x)