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