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