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