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