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