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