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