37static constexpr double SNAPSHOT_PERIOD = 1.0;
40static constexpr unsigned MAX_NOF_SNAPSHOTS = 10;
43static constexpr auto MIN_PARTITION_LENGTH = EmuDuration(60.0);
46static constexpr auto MAX_DIST_1_BEFORE_LAST_SNAPSHOT = EmuDuration(30.0);
48static constexpr const char*
const REPLAY_DIR =
"replays";
73 template<
typename Archive>
76 if (ar.versionAtLeast(version, 2)) {
80 ar.serialize(
"snapshot", *newBoard);
84 ar.serialize(
"events", *
events);
86 if (ar.versionAtLeast(version, 3)) {
89 assert(Archive::IS_LOADER);
94 if (ar.versionAtLeast(version, 4)) {
110void ReverseManager::ReverseHistory::clear()
113 Chunks().swap(chunks);
114 Events().swap(events);
127 template<
typename Archive>
void serialize(Archive& ar,
unsigned )
129 ar.template serializeBase<StateChange>(*
this);
137 : syncNewSnapshot(motherBoard_.getScheduler())
138 , syncInputEvent (motherBoard_.getScheduler())
139 , motherBoard(motherBoard_)
140 , eventDistributor(motherBoard.getReactor().getEventDistributor())
141 , reverseCmd(motherBoard.getCommandController())
145 assert(!isCollecting());
157 return replayIndex != history.events.size();
160void ReverseManager::start()
162 if (!isCollecting()) {
165 takeSnapshot(getCurrentTime());
167 schedule(getCurrentTime());
171 assert(isCollecting());
174void ReverseManager::stop()
176 if (isCollecting()) {
178 syncNewSnapshot.removeSyncPoint();
179 syncInputEvent .removeSyncPoint();
183 pendingTakeSnapshot =
false;
185 assert(!pendingTakeSnapshot);
186 assert(!isCollecting());
190EmuTime::param ReverseManager::getEndTime(
const ReverseHistory& hist)
const
192 if (!hist.events.empty()) {
193 if (
const auto* ev =
dynamic_cast<const EndLogEvent*
>(
194 hist.events.back().get())) {
196 return ev->getTime();
201 return getCurrentTime();
204void ReverseManager::status(TclObject& result)
const
206 result.addDictKeyValue(
"status", !isCollecting() ?
"disabled"
210 EmuTime b(isCollecting() ?
begin(history.chunks)->second.time
212 result.addDictKeyValue(
"begin", (b - EmuTime::zero()).toDouble());
214 EmuTime
end(isCollecting() ? getEndTime(history) : EmuTime::zero());
215 result.addDictKeyValue(
"end", (
end - EmuTime::zero()).toDouble());
217 EmuTime current(isCollecting() ? getCurrentTime() : EmuTime::zero());
218 result.addDictKeyValue(
"current", (current - EmuTime::zero()).toDouble());
221 snapshots.addListElements(
view::transform(history.chunks, [](
auto& p) {
222 return (p.second.time - EmuTime::zero()).toDouble();
224 result.addDictKeyValue(
"snapshots", snapshots);
226 auto lastEvent = rbegin(history.events);
227 if (lastEvent != rend(history.events) &&
dynamic_cast<const EndLogEvent*
>(lastEvent->get())) {
230 EmuTime le(isCollecting() && (lastEvent != rend(history.events)) ? (*lastEvent)->getTime() : EmuTime::zero());
231 result.addDictKeyValue(
"last_event", (le - EmuTime::zero()).toDouble());
234void ReverseManager::debugInfo(TclObject& result)
const
239 size_t totalSize = 0;
240 for (
const auto& [idx, chunk] : history.chunks) {
242 (chunk.time - EmuTime::zero()).toDouble(),
' ',
243 ((chunk.time - EmuTime::zero()).toDouble() / (getCurrentTime() - EmuTime::zero()).toDouble()) * 100,
"%"
244 " (", chunk.size,
")"
245 " (next event index: ", chunk.eventCount,
")\n");
246 totalSize += chunk.size;
248 strAppend(res,
"total size: ", totalSize,
'\n');
252static std::pair<bool, double> parseGoTo(Interpreter& interp, std::span<const TclObject> tokens)
254 bool noVideo =
false;
255 std::array info = {
flagArg(
"-novideo", noVideo)};
256 auto args =
parseTclArgs(interp, tokens.subspan(2), info);
257 if (args.size() != 1)
throw SyntaxError();
258 double time = args[0].getDouble(interp);
259 return {noVideo, time};
262void ReverseManager::goBack(std::span<const TclObject> tokens)
265 auto [noVideo,
t] = parseGoTo(interp, tokens);
267 EmuTime now = getCurrentTime();
268 EmuTime target(EmuTime::dummy());
271 if (d < (now - EmuTime::zero())) {
274 target = EmuTime::zero();
277 target = now + EmuDuration(-
t);
279 goTo(target, noVideo);
282void ReverseManager::goTo(std::span<const TclObject> tokens)
285 auto [noVideo,
t] = parseGoTo(interp, tokens);
287 EmuTime target = EmuTime::zero() + EmuDuration(
t);
288 goTo(target, noVideo);
291void ReverseManager::goTo(EmuTime::param target,
bool noVideo)
293 if (!isCollecting()) {
294 throw CommandException(
295 "Reverse was not enabled. First execute the 'reverse "
296 "start' command to start collecting data.");
298 goTo(target, noVideo, history,
true);
302static void reportProgress(Reactor& reactor,
const EmuTime& targetTime,
unsigned percentage)
304 double targetTimeDisp = (targetTime - EmuTime::zero()).toDouble();
305 std::ostringstream sstr;
306 sstr <<
"Time warping to " <<
307 int(targetTimeDisp / 60) <<
':' << std::setfill(
'0') <<
308 std::setw(5) << std::setprecision(2) << std::fixed <<
309 std::fmod(targetTimeDisp, 60.0) <<
310 "... " << percentage <<
'%';
311 reactor.getCliComm().printProgress(sstr.str());
312 reactor.getDisplay().repaint();
315void ReverseManager::goTo(
316 EmuTime::param target,
bool noVideo, ReverseHistory& hist,
332 assert(!hist.chunks.empty());
333 auto it =
begin(hist.chunks);
334 EmuTime firstTime = it->second.time;
335 EmuTime targetTime =
std::max(target, firstTime);
337 targetTime =
std::min(targetTime, getEndTime(hist));
344 double dur2frames = 2.0 * (313.0 * 1368.0) / (3579545.0 * 6.0);
345 EmuDuration preDelta(noVideo ? 0.0 : dur2frames);
346 EmuTime preTarget = ((targetTime - firstTime) > preDelta)
347 ? targetTime - preDelta
352 assert(it->second.time <= preTarget);
353 assert(it !=
end(hist.chunks));
356 }
while (it !=
end(hist.chunks) &&
357 it->second.time <= preTarget);
360 assert(it !=
begin(hist.chunks));
362 ReverseChunk& chunk = it->second;
363 EmuTime snapshotTime = chunk.time;
364 assert(snapshotTime <= preTarget);
375 EmuTime currentTime = getCurrentTime();
376 MSXMotherBoard* newBoard;
379 (currentTime <= preTarget) &&
380 ((snapshotTime <= currentTime) ||
381 ((preTarget - currentTime) < EmuDuration(1.0)))) {
382 newBoard = &motherBoard;
386 newBoard_ = reactor.createEmptyMotherBoard();
387 newBoard = newBoard_.get();
388 MemInputArchive in(chunk.savestate.data(),
391 in.serialize(
"machine", *newBoard);
401 if (hist.events.empty() ||
402 !
dynamic_cast<const EndLogEvent*
>(hist.events.back().get())) {
403 hist.events.push_back(
404 std::make_unique<EndLogEvent>(currentTime));
410 auto& newManager = newBoard->getReverseManager();
411 newManager.transferHistory(hist, chunk.eventCount);
414 transferState(*newBoard);
428 auto startMSXTime = newBoard->getCurrentTime();
429 auto lastSnapshotTarget = startMSXTime;
430 bool everShowedProgress =
false;
431 syncNewSnapshot.removeSyncPoint();
433 auto currentTimeNewBoard = newBoard->getCurrentTime();
437 EmuDuration(SNAPSHOT_PERIOD),
438 (preTarget - lastSnapshotTarget) / 2
441 newBoard->fastForward(nextTarget,
true);
443 if (((now - lastProgress) > 1000000) || ((currentTimeNewBoard >= preTarget) && everShowedProgress)) {
444 everShowedProgress =
true;
446 auto percentage = ((currentTimeNewBoard - startMSXTime) * 100u) / (preTarget - startMSXTime);
447 reportProgress(newBoard->getReactor(), targetTime, percentage);
451 if (currentTimeNewBoard >= preTarget)
break;
452 if (currentTimeNewBoard >= nextSnapshotTarget) {
459 newBoard->getReverseManager().takeSnapshot(currentTimeNewBoard);
460 lastSnapshotTarget = nextSnapshotTarget;
464 schedule(getCurrentTime());
472 reactor.replaceBoard(motherBoard, std::move(newBoard_));
477 newBoard->fastForward(targetTime,
false);
486 assert(newBoard->getReverseManager().isCollecting());
487 }
catch (MSXException&) {
494void ReverseManager::transferState(MSXMotherBoard& newBoard)
498 auto& newDistributor = newBoard .getStateChangeDistributor();
502 auto& newManager = newBoard.getReverseManager();
503 if (newManager.keyboard && keyboard) {
504 newManager.keyboard->transferHostKeyMatrix(*keyboard);
508 newBoard.getDebugger().transfer(motherBoard.
getDebugger());
511 newManager.reRecordCount = reRecordCount;
518void ReverseManager::saveReplay(
519 Interpreter& interp, std::span<const TclObject> tokens, TclObject& result)
521 const auto& chunks = history.chunks;
522 if (chunks.empty()) {
523 throw CommandException(
"No recording...");
526 std::string_view filenameArg;
527 int maxNofExtraSnapshots = MAX_NOF_SNAPSHOTS;
528 std::array info = {
valueArg(
"-maxnofextrasnapshots", maxNofExtraSnapshots)};
529 auto args =
parseTclArgs(interp, tokens.subspan(2), info);
530 switch (args.size()) {
532 case 1: filenameArg = args[0].getString();
break;
533 default:
throw SyntaxError();
535 if (maxNofExtraSnapshots < 0) {
536 throw CommandException(
"Maximum number of snapshots should be at least 0");
540 filenameArg, REPLAY_DIR,
"openmsx",
".omr");
544 replay.reRecordCount = reRecordCount;
548 replay.currentTime = getCurrentTime();
551 auto initialBoard = reactor.createEmptyMotherBoard();
552 MemInputArchive in(
begin(chunks)->second.savestate.data(),
553 begin(chunks)->second.size,
554 begin(chunks)->second.deltaBlocks);
555 in.serialize(
"machine", *initialBoard);
556 replay.motherBoards.push_back(std::move(initialBoard));
558 if (maxNofExtraSnapshots > 0) {
560 const auto& startTime =
begin(chunks)->second.time;
565 const auto& lastChunkTime = rbegin(chunks)->second.time;
566 const auto& endTime = ((startTime + MAX_DIST_1_BEFORE_LAST_SNAPSHOT) < lastChunkTime) ? lastChunkTime - MAX_DIST_1_BEFORE_LAST_SNAPSHOT : lastChunkTime;
567 EmuDuration totalLength = endTime - startTime;
568 EmuDuration partitionLength = totalLength.divRoundUp(maxNofExtraSnapshots);
569 partitionLength =
std::max(MIN_PARTITION_LENGTH, partitionLength);
570 EmuTime nextPartitionEnd = startTime + partitionLength;
571 auto it =
begin(chunks);
572 auto lastAddedIt =
begin(chunks);
573 while (it !=
end(chunks)) {
575 if (it ==
end(chunks) || (it->second.time > nextPartitionEnd)) {
577 assert(it->second.time <= nextPartitionEnd);
578 if (it != lastAddedIt) {
581 MemInputArchive in2(it->second.savestate.data(),
583 it->second.deltaBlocks);
584 in2.serialize(
"machine", *board);
585 replay.motherBoards.push_back(std::move(board));
589 while (it !=
end(chunks) && it->second.time > nextPartitionEnd) {
590 nextPartitionEnd += partitionLength;
594 assert(lastAddedIt == std::prev(
end(chunks)));
598 bool addSentinel = history.events.empty() ||
599 !
dynamic_cast<EndLogEvent*
>(history.events.back().get());
602 history.events.push_back(std::make_unique<EndLogEvent>(
606 XmlOutputArchive out(filename);
607 replay.events = &history.events;
608 out.serialize(
"replay", replay);
610 }
catch (MSXException&) {
612 history.events.pop_back();
622 history.events.pop_back();
625 result =
tmpStrCat(
"Saved replay to ", filename);
628void ReverseManager::loadReplay(
629 Interpreter& interp, std::span<const TclObject> tokens, TclObject& result)
631 bool enableViewOnly =
false;
632 std::optional<TclObject> where;
634 flagArg(
"-viewonly", enableViewOnly),
637 auto arguments =
parseTclArgs(interp, tokens.subspan(2), info);
638 if (arguments.size() != 1)
throw SyntaxError();
642 std::string fileNameArg(arguments[0].getString());
643 std::string filename;
646 filename =
context.resolve(fileNameArg);
647 }
catch (MSXException& ) {
try {
650 }
catch (MSXException& e2) {
try {
654 }
catch (MSXException& ) {
663 replay.events = &events;
665 XmlInputArchive in(filename);
666 in.serialize(
"replay", replay);
667 }
catch (XMLException&
e) {
668 throw CommandException(
"Cannot load replay, bad file format: ",
670 }
catch (MSXException&
e) {
671 throw CommandException(
"Cannot load replay: ",
e.getMessage());
675 auto destination = EmuTime::zero();
676 if (!where || (*where ==
"begin")) {
677 destination = EmuTime::zero();
678 }
else if (*where ==
"end") {
679 destination = EmuTime::infinity();
680 }
else if (*where ==
"savetime") {
681 destination = replay.currentTime;
683 destination += EmuDuration(where->getDouble(interp));
691 assert(!replay.motherBoards.empty());
692 auto& newReverseManager = replay.motherBoards[0]->getReverseManager();
693 auto& newHistory = newReverseManager.history;
695 if (newReverseManager.reRecordCount == 0) {
697 newReverseManager.reRecordCount = replay.reRecordCount;
704 swap(newHistory.events, events);
705 auto& newEvents = newHistory.events;
708 unsigned replayIdx = 0;
709 for (
auto& m : replay.motherBoards) {
710 ReverseChunk newChunk;
711 newChunk.time = m->getCurrentTime();
713 MemOutputArchive out(newHistory.lastDeltaBlocks,
714 newChunk.deltaBlocks,
false);
715 out.serialize(
"machine", *m);
716 newChunk.savestate = out.releaseBuffer(newChunk.size);
720 while (replayIdx < newEvents.size() &&
721 (newEvents[replayIdx]->getTime() < newChunk.time)) {
724 newChunk.eventCount = replayIdx;
726 newHistory.chunks[newHistory.getNextSeqNum(newChunk.time)] =
732 reRecordCount = newReverseManager.reRecordCount;
733 bool noVideo =
false;
734 goTo(destination, noVideo, newHistory,
false);
736 result =
tmpStrCat(
"Loaded replay from ", filename);
739void ReverseManager::transferHistory(ReverseHistory& oldHistory,
740 unsigned oldEventCount)
742 assert(!isCollecting());
743 assert(history.chunks.empty());
746 oldHistory.lastDeltaBlocks.clear();
749 history.swap(oldHistory);
753 schedule(getCurrentTime());
757 replayIndex = oldEventCount;
759 assert(replayIndex < history.events.size());
763void ReverseManager::execNewSnapshot()
791 pendingTakeSnapshot =
true;
793 Event::create<TakeReverseSnapshotEvent>());
796void ReverseManager::execInputEvent()
798 const auto&
event = *history.events[replayIndex];
802 }
catch (MSXException&) {
806 if (!
dynamic_cast<const EndLogEvent*
>(&event)) {
810 signalStopReplay(event.getTime());
815int ReverseManager::signalEvent(
const Event& event)
822 if (pendingTakeSnapshot) {
823 pendingTakeSnapshot =
false;
824 takeSnapshot(getCurrentTime());
826 schedule(getCurrentTime());
831unsigned ReverseManager::ReverseHistory::getNextSeqNum(EmuTime::param time)
const
833 if (chunks.empty()) {
836 const auto& startTime =
begin(chunks)->second.time;
837 double duration = (time - startTime).toDouble();
838 return narrow<unsigned>(lrint(duration / SNAPSHOT_PERIOD));
841void ReverseManager::takeSnapshot(EmuTime::param time)
846 unsigned seqNum = history.getNextSeqNum(time);
847 dropOldSnapshots<25>(seqNum);
855 ReverseChunk& newChunk = history.chunks[seqNum];
856 newChunk.deltaBlocks.clear();
857 MemOutputArchive out(history.lastDeltaBlocks, newChunk.deltaBlocks,
true);
858 out.serialize(
"machine", motherBoard);
859 newChunk.time = time;
860 newChunk.savestate = out.releaseBuffer(newChunk.size);
861 newChunk.eventCount = replayIndex;
864void ReverseManager::replayNextEvent()
867 assert(replayIndex < history.events.size());
868 syncInputEvent.setSyncPoint(history.events[replayIndex]->getTime());
871void ReverseManager::signalStopReplay(EmuTime::param time)
883 syncInputEvent.removeSyncPoint();
884 Events& events = history.events;
885 events.erase(
begin(events) + replayIndex,
end(events));
888 return p.second.time > time;
890 history.chunks.erase(it,
end(history.chunks));
894 assert(!isReplaying());
910void ReverseManager::dropOldSnapshots(
unsigned count)
914 unsigned d2 = 2 * N + 1;
917 if ((y == 0) || (
count < d))
return;
918 history.chunks.erase(
count - d);
924void ReverseManager::schedule(EmuTime::param time)
926 syncNewSnapshot.setSyncPoint(time + EmuDuration(SNAPSHOT_PERIOD));
932ReverseManager::ReverseCmd::ReverseCmd(CommandController& controller)
933 : Command(controller,
"reverse")
937void ReverseManager::ReverseCmd::execute(std::span<const TclObject> tokens, TclObject& result)
939 checkNumArgs(tokens, AtLeast{2},
"subcommand ?arg ...?");
940 auto& manager =
OUTER(ReverseManager, reverseCmd);
941 auto& interp = getInterpreter();
942 executeSubCommand(tokens[1].getString(),
943 "start", [&]{ manager.start(); },
944 "stop", [&]{ manager.stop(); },
945 "status", [&]{ manager.status(result); },
946 "debug", [&]{ manager.debugInfo(result); },
947 "goback", [&]{ manager.goBack(tokens); },
948 "goto", [&]{ manager.goTo(tokens); },
949 "savereplay", [&]{ manager.saveReplay(interp, tokens, result); },
950 "loadreplay", [&]{ manager.loadReplay(interp, tokens, result); },
952 auto& distributor = manager.motherBoard.getStateChangeDistributor();
953 switch (tokens.size()) {
955 result = distributor.isViewOnlyMode();
958 distributor.setViewOnlyMode(tokens[2].getBoolean(interp));
963 "truncatereplay", [&] {
964 if (manager.isReplaying()) {
965 manager.signalStopReplay(manager.getCurrentTime());
969std::string ReverseManager::ReverseCmd::help(std::span<const TclObject> )
const
971 return "start start collecting reverse data\n"
972 "stop stop collecting\n"
973 "status show various status info on reverse\n"
974 "goback <n> go back <n> seconds in time\n"
975 "goto <time> go to an absolute moment in time\n"
976 "viewonlymode <bool> switch viewonly mode on or off\n"
977 "truncatereplay stop replaying and remove all 'future' data\n"
978 "savereplay [<name>] save the first snapshot and all replay data as a 'replay' (with optional name)\n"
979 "loadreplay [-goto <begin|end|savetime|<n>>] [-viewonly] <name> load a replay (snapshot and replay data) with given name and start replaying\n";
982void ReverseManager::ReverseCmd::tabCompletion(std::vector<std::string>& tokens)
const
984 using namespace std::literals;
985 if (tokens.size() == 2) {
986 static constexpr std::array subCommands = {
987 "start"sv,
"stop"sv,
"status"sv,
"goback"sv,
"goto"sv,
988 "savereplay"sv,
"loadreplay"sv,
"viewonlymode"sv,
991 completeString(tokens, subCommands);
992 }
else if ((tokens.size() == 3) || (tokens[1] ==
"loadreplay")) {
993 if (tokens[1] ==
one_of(
"loadreplay",
"savereplay")) {
994 static constexpr std::array cmds = {
"-goto"sv,
"-viewonly"sv};
996 (tokens[1] ==
"loadreplay") ? cmds : std::span<const std::string_view>{});
997 }
else if (tokens[1] ==
"viewonlymode") {
998 static constexpr std::array options = {
"true"sv,
"false"sv};
999 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=OTHER)
Registers a given object to receive certain events.
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()
MSXCommandController & getMSXCommandController()
Contains the main loop of openMSX.
std::shared_ptr< MSXMotherBoard > Board
Board createEmptyMotherBoard()
Interpreter & getInterpreter()
void stopReplay(EmuTime::param time) noexcept
ReverseManager(MSXMotherBoard &motherBoard)
void unregisterRecorder(ReverseManager &recorder)
void registerRecorder(ReverseManager &recorder)
(Un)registers the given object to receive state change events.
void setViewOnlyMode(bool value)
Set viewOnlyMode.
void stopReplay(EmuTime::param time)
Explicitly stop replay.
void distributeReplay(const StateChange &event)
Base class for all external MSX state changing events.
ALWAYS_INLINE unsigned count(const uint8_t *pIn, const uint8_t *pMatch, const uint8_t *pInLimit)
constexpr vecN< N, T > min(const vecN< N, T > &x, const vecN< N, T > &y)
std::optional< Context > context
constexpr vecN< N, T > max(const vecN< N, T > &x, const vecN< N, T > &y)
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)
REGISTER_POLYMORPHIC_CLASS(StateChange, AutofireStateChange, "AutofireStateChange")
std::vector< TclObject > parseTclArgs(Interpreter &interp, std::span< const TclObject > inArgs, std::span< const ArgsInfo > table)
SERIALIZE_CLASS_VERSION(CassettePlayer, 2)
EventType getType(const Event &event)
ArgsInfo flagArg(std::string_view name, bool &flag)
FileContext userDataFileContext(string_view subDir)
auto find_if(InputRange &&range, UnaryPredicate pred)
void swap(openmsx::MemBuffer< T > &l, openmsx::MemBuffer< T > &r) noexcept
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)