OXYGENHUB
◆ main openmsx / src / Reactor.cc
38.4 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#include "Reactor.hh"
2
3
#include "AfterCommand.hh"
4
#include "AviRecorder.hh"
5
#include "BooleanSetting.hh"
6
#include "Command.hh"
7
#include "CommandException.hh"
8
#include "CommandLineParser.hh"
9
#include "DiskChanger.hh"
10
#include "DiskFactory.hh"
11
#include "DiskManipulator.hh"
12
#include "Display.hh"
13
#include "EnumSetting.hh"
14
#include "Event.hh"
15
#include "EventDistributor.hh"
16
#include "FileContext.hh"
17
#include "FileException.hh"
18
#include "FilePool.hh"
19
#include "GlobalCliComm.hh"
20
#include "GlobalCommandController.hh"
21
#include "GlobalSettings.hh"
22
#include "HardwareConfig.hh"
23
#include "ImGuiManager.hh"
24
#include "InfoTopic.hh"
25
#include "InputEventGenerator.hh"
26
#include "Keyboard.hh"
27
#include "MSXMotherBoard.hh"
28
#include "MessageCommand.hh"
29
#include "Mixer.hh"
30
#include "MsxChar2Unicode.hh"
31
#include "RTScheduler.hh"
32
#include "RomDatabase.hh"
33
#include "RomInfo.hh"
34
#include "StateChangeDistributor.hh"
35
#include "SymbolManager.hh"
36
#include "TclCallbackMessages.hh"
37
#include "TclObject.hh"
38
#include "UserSettings.hh"
39
#include "VideoSystem.hh"
40
#include "XMLElement.hh"
41
#include "XMLException.hh"
42
43
#include "FileOperations.hh"
44
#include "foreach_file.hh"
45
46
#include "Thread.hh"
47
#include "Timer.hh"
48
49
#include "narrow.hh"
50
#include "serialize.hh"
51
#include "stl.hh"
52
#include "unreachable.hh"
53
54
#include "build-info.hh"
55
56
#include <algorithm>
57
#include <array>
58
#include <cassert>
59
#include <memory>
60
#include <ranges>
61
62
namespace openmsx {
63
64
static constexpr std::string_view DEFAULT_SETUP_NAME = "last_used";
65
66
using enum SetupDepth;
67
68
// global variable to communicate the exit-code from the 'exit' command to main()
69
int exitCode = 0;
70
71
class ExitCommand final : public Command
72
{
73
public:
74
	ExitCommand(CommandController& commandController, EventDistributor& distributor);
75
	void execute(std::span<const TclObject> tokens, TclObject& result) override;
76
	[[nodiscard]] std::string help(std::span<const TclObject> tokens) const override;
77
private:
78
	EventDistributor& distributor;
79
};
80
81
class MachineCommand final : public Command
82
{
83
public:
84
	MachineCommand(CommandController& commandController, Reactor& reactor);
85
	void execute(std::span<const TclObject> tokens, TclObject& result) override;
86
	[[nodiscard]] std::string help(std::span<const TclObject> tokens) const override;
87
	void tabCompletion(std::vector<std::string>& tokens) const override;
88
private:
89
	Reactor& reactor;
90
};
91
92
class TestMachineCommand final : public Command
93
{
94
public:
95
	TestMachineCommand(CommandController& commandController, Reactor& reactor);
96
	void execute(std::span<const TclObject> tokens, TclObject& result) override;
97
	[[nodiscard]] std::string help(std::span<const TclObject> tokens) const override;
98
	void tabCompletion(std::vector<std::string>& tokens) const override;
99
private:
100
	Reactor& reactor;
101
};
102
103
class CreateMachineCommand final : public Command
104
{
105
public:
106
	CreateMachineCommand(CommandController& commandController, Reactor& reactor);
107
	void execute(std::span<const TclObject> tokens, TclObject& result) override;
108
	[[nodiscard]] std::string help(std::span<const TclObject> tokens) const override;
109
private:
110
	Reactor& reactor;
111
};
112
113
class DeleteMachineCommand final : public Command
114
{
115
public:
116
	DeleteMachineCommand(CommandController& commandController, Reactor& reactor);
117
	void execute(std::span<const TclObject> tokens, TclObject& result) override;
118
	[[nodiscard]] std::string help(std::span<const TclObject> tokens) const override;
119
	void tabCompletion(std::vector<std::string>& tokens) const override;
120
private:
121
	Reactor& reactor;
122
};
123
124
class ListMachinesCommand final : public Command
125
{
126
public:
127
	ListMachinesCommand(CommandController& commandController, Reactor& reactor);
128
	void execute(std::span<const TclObject> tokens, TclObject& result) override;
129
	[[nodiscard]] std::string help(std::span<const TclObject> tokens) const override;
130
private:
131
	Reactor& reactor;
132
};
133
134
class ActivateMachineCommand final : public Command
135
{
136
public:
137
	ActivateMachineCommand(CommandController& commandController, Reactor& reactor);
138
	void execute(std::span<const TclObject> tokens, TclObject& result) override;
139
	[[nodiscard]] std::string help(std::span<const TclObject> tokens) const override;
140
	void tabCompletion(std::vector<std::string>& tokens) const override;
141
private:
142
	Reactor& reactor;
143
};
144
145
class StoreMachineCommand final : public Command
146
{
147
public:
148
	StoreMachineCommand(CommandController& commandController, Reactor& reactor);
149
	void execute(std::span<const TclObject> tokens, TclObject& result) override;
150
	[[nodiscard]] std::string help(std::span<const TclObject> tokens) const override;
151
	void tabCompletion(std::vector<std::string>& tokens) const override;
152
private:
153
	Reactor& reactor;
154
};
155
156
class RestoreMachineCommand final : public Command
157
{
158
public:
159
	RestoreMachineCommand(CommandController& commandController, Reactor& reactor);
160
	void execute(std::span<const TclObject> tokens, TclObject& result) override;
161
	[[nodiscard]] std::string help(std::span<const TclObject> tokens) const override;
162
	void tabCompletion(std::vector<std::string>& tokens) const override;
163
private:
164
	Reactor& reactor;
165
};
166
167
class SetupCommand final : public Command
168
{
169
public:
170
	SetupCommand(CommandController& commandController, Reactor& reactor);
171
	void execute(std::span<const TclObject> tokens, TclObject& result) override;
172
	[[nodiscard]] std::string help(std::span<const TclObject> tokens) const override;
173
	void tabCompletion(std::vector<std::string>& tokens) const override;
174
private:
175
	Reactor& reactor;
176
};
177
178
class GetClipboardCommand final : public Command
179
{
180
public:
181
	GetClipboardCommand(CommandController& commandController, Reactor& reactor);
182
	void execute(std::span<const TclObject> tokens, TclObject& result) override;
183
	[[nodiscard]] std::string help(std::span<const TclObject> tokens) const override;
184
private:
185
	Reactor& reactor;
186
};
187
188
class SetClipboardCommand final : public Command
189
{
190
public:
191
	SetClipboardCommand(CommandController& commandController, Reactor& reactor);
192
	void execute(std::span<const TclObject> tokens, TclObject& result) override;
193
	[[nodiscard]] std::string help(std::span<const TclObject> tokens) const override;
194
private:
195
	Reactor& reactor;
196
};
197
198
class ConfigInfo final : public InfoTopic
199
{
200
public:
201
	ConfigInfo(InfoCommand& openMSXInfoCommand, const std::string& configName);
202
	void execute(std::span<const TclObject> tokens,
203
	             TclObject& result) const override;
204
	[[nodiscard]] std::string help(std::span<const TclObject> tokens) const override;
205
	void tabCompletion(std::vector<std::string>& tokens) const override;
206
private:
207
	const std::string configName;
208
};
209
210
class RealTimeInfo final : public InfoTopic
211
{
212
public:
213
	explicit RealTimeInfo(InfoCommand& openMSXInfoCommand);
214
	void execute(std::span<const TclObject> tokens,
215
	             TclObject& result) const override;
216
	[[nodiscard]] std::string help(std::span<const TclObject> tokens) const override;
217
private:
218
	const uint64_t reference;
219
};
220
221
class SoftwareInfoTopic final : public InfoTopic
222
{
223
public:
224
	SoftwareInfoTopic(InfoCommand& openMSXInfoCommand, Reactor& reactor);
225
	void execute(std::span<const TclObject> tokens,
226
	             TclObject& result) const override;
227
	[[nodiscard]] std::string help(std::span<const TclObject> tokens) const override;
228
private:
229
	Reactor& reactor;
230
};
231
232
233
Reactor::Reactor() = default;
234
235
void Reactor::init()
236
{
237
	shortcuts = std::make_unique<Shortcuts>();
238
	rtScheduler = std::make_unique<RTScheduler>();
239
	eventDistributor = std::make_unique<EventDistributor>(*this);
240
	globalCliComm = std::make_unique<GlobalCliComm>();
241
	globalCommandController = std::make_unique<GlobalCommandController>(
242
		*eventDistributor, *globalCliComm, *this);
243
	globalSettings = std::make_unique<GlobalSettings>(
244
		*globalCommandController);
245
	inputEventGenerator = std::make_unique<InputEventGenerator>(
246
		*globalCommandController, *eventDistributor);
247
	symbolManager = std::make_unique<SymbolManager>(
248
		*globalCommandController);
249
	imGuiManager = std::make_unique<ImGuiManager>(*this);
250
	diskFactory = std::make_unique<DiskFactory>(*this);
251
	diskManipulator = std::make_unique<DiskManipulator>(
252
		*globalCommandController, *this);
253
	virtualDrive = std::make_unique<DiskChanger>(
254
		*this, "virtual_drive");
255
	filePool = std::make_unique<FilePool>(*globalCommandController, *this);
256
	userSettings = std::make_unique<UserSettings>(
257
		*globalCommandController);
258
	afterCommand = std::make_unique<AfterCommand>(
259
		*this, *eventDistributor, *globalCommandController);
260
	exitCommand = std::make_unique<ExitCommand>(
261
		*globalCommandController, *eventDistributor);
262
	messageCommand = std::make_unique<MessageCommand>(
263
		*globalCommandController);
264
	machineCommand = std::make_unique<MachineCommand>(
265
		*globalCommandController, *this);
266
	testMachineCommand = std::make_unique<TestMachineCommand>(
267
		*globalCommandController, *this);
268
	createMachineCommand = std::make_unique<CreateMachineCommand>(
269
		*globalCommandController, *this);
270
	deleteMachineCommand = std::make_unique<DeleteMachineCommand>(
271
		*globalCommandController, *this);
272
	listMachinesCommand = std::make_unique<ListMachinesCommand>(
273
		*globalCommandController, *this);
274
	activateMachineCommand = std::make_unique<ActivateMachineCommand>(
275
		*globalCommandController, *this);
276
	storeMachineCommand = std::make_unique<StoreMachineCommand>(
277
		*globalCommandController, *this);
278
	restoreMachineCommand = std::make_unique<RestoreMachineCommand>(
279
		*globalCommandController, *this);
280
	setupCommand = std::make_unique<SetupCommand>(
281
		*globalCommandController, *this);
282
	getClipboardCommand = std::make_unique<GetClipboardCommand>(
283
		*globalCommandController, *this);
284
	setClipboardCommand = std::make_unique<SetClipboardCommand>(
285
		*globalCommandController, *this);
286
	aviRecordCommand = std::make_unique<AviRecorder>(*this);
287
	extensionInfo = std::make_unique<ConfigInfo>(
288
		getOpenMSXInfoCommand(), "extensions");
289
	machineInfo   = std::make_unique<ConfigInfo>(
290
		getOpenMSXInfoCommand(), "machines");
291
	realTimeInfo = std::make_unique<RealTimeInfo>(
292
		getOpenMSXInfoCommand());
293
	softwareInfoTopic = std::make_unique<SoftwareInfoTopic>(
294
		getOpenMSXInfoCommand(), *this);
295
	tclCallbackMessages = std::make_unique<TclCallbackMessages>(
296
		*globalCliComm, *globalCommandController);
297
298
	createDefaultMachineAndSetupSettings();
299
300
	saveSetupAtExitNameSetting = std::make_unique<StringSetting>(
301
		*globalCommandController, "save_setup_at_exit_name",
302
		"Setup name to use for saving at openMSX exit, if configured.", DEFAULT_SETUP_NAME);
303
304
	saveSetupAtExitDepthSetting = std::make_unique<EnumSetting<SetupDepth>>(
305
		*globalCommandController, "save_setup_at_exit_depth",
306
		"Setup depth to use for saving at openMSX exit.",
307
		NONE,
308
		EnumSetting<SetupDepth>::Map{
309
			{"none"          , NONE          },
310
			{"machine"       , MACHINE       },
311
			{"extensions"    , EXTENSIONS    },
312
			{"connectors"    , CONNECTORS    },
313
			{"media"         , MEDIA         },
314
			{"complete_state", COMPLETE_STATE}});
315
316
	getGlobalSettings().getPauseSetting().attach(*this);
317
318
	eventDistributor->registerEventListener(EventType::QUIT, *this);
319
#if PLATFORM_ANDROID
320
	eventDistributor->registerEventListener(EventType::WINDOW, *this);
321
#endif
322
	isInit = true;
323
}
324
325
Reactor::~Reactor()
326
{
327
	if (!isInit) return;
328
	deleteBoard(activeBoard);
329
330
	eventDistributor->unregisterEventListener(EventType::QUIT, *this);
331
#if PLATFORM_ANDROID
332
	eventDistributor->unregisterEventListener(EventType::WINDOW, *this);
333
#endif
334
335
	getGlobalSettings().getPauseSetting().detach(*this);
336
}
337
338
Mixer& Reactor::getMixer()
339
{
340
	if (!mixer) {
341
		mixer = std::make_unique<Mixer>(*this, *globalCommandController);
342
	}
343
	return *mixer;
344
}
345
346
RomDatabase& Reactor::getSoftwareDatabase()
347
{
348
	if (!softwareDatabase) {
349
		softwareDatabase = std::make_unique<RomDatabase>(*globalCliComm);
350
	}
351
	return *softwareDatabase;
352
}
353
354
CliComm& Reactor::getCliComm()
355
{
356
	return *globalCliComm;
357
}
358
359
Interpreter& Reactor::getInterpreter()
360
{
361
	return getGlobalCommandController().getInterpreter();
362
}
363
364
CommandController& Reactor::getCommandController()
365
{
366
	return *globalCommandController;
367
}
368
369
InfoCommand& Reactor::getOpenMSXInfoCommand()
370
{
371
	return globalCommandController->getOpenMSXInfoCommand();
372
}
373
374
const HotKey& Reactor::getHotKey() const
375
{
376
	return globalCommandController->getHotKey();
377
}
378
379
std::vector<std::string> Reactor::getHwConfigs(std::string_view type)
380
{
381
	std::vector<std::string> result;
382
	for (const auto& p : systemFileContext().getPaths()) {
383
		auto fileAction = [&](const std::string& /*path*/, std::string_view name) {
384
			if (name.ends_with(".xml")) {
385
				name.remove_suffix(4);
386
				result.emplace_back(name);
387
			}
388
		};
389
		auto dirAction = [&](std::string& path, std::string_view name) {
390
			auto size = path.size();
391
			path += "/hardwareconfig.xml";
392
			if (FileOperations::isRegularFile(path)) {
393
				result.emplace_back(name);
394
			}
395
			path.resize(size);
396
		};
397
		foreach_file_and_directory(FileOperations::join(p, type), fileAction, dirAction);
398
	}
399
	// remove duplicates
400
	std::ranges::sort(result);
401
	auto u = std::ranges::unique(result);
402
	result.erase(u.begin(), u.end());
403
	return result;
404
}
405
406
std::vector<std::string> Reactor::getSetups()
407
{
408
	std::vector<std::string> result;
409
	std::string_view extension = Reactor::SETUP_EXTENSION;
410
	foreach_file(FileOperations::getUserOpenMSXDir(SETUP_DIR), [&](const std::string& /*fullName*/, std::string_view name) {
411
		if (name.ends_with(extension)) {
412
			name.remove_suffix(extension.size());
413
			result.emplace_back(name);
414
		}
415
	});
416
	return result;
417
}
418
419
const MsxChar2Unicode& Reactor::getMsxChar2Unicode() const
420
{
421
	// TODO cleanup this code. It should be easier to get a hold of the
422
	// 'MsxChar2Unicode' object. Probably the 'Keyboard' class is not the
423
	// right location to store it.
424
	try {
425
		if (const auto* board = getMotherBoard()) {
426
			if (const auto* keyb = board->getKeyboard()) {
427
				return keyb->getMsxChar2Unicode();
428
			}
429
		}
430
	} catch (MSXException&) {
431
		// ignore
432
	}
433
	static const MsxChar2Unicode defaultMsxChars("MSXVID.TXT");
434
	return defaultMsxChars;
435
}
436
437
438
void Reactor::createDefaultMachineAndSetupSettings()
439
{
440
	auto names = getHwConfigs("machines");
441
	EnumSetting<int>::Map machines; // int's are unique dummy values
442
	machines.reserve(names.size() + 1);
443
	int count = 1;
444
	append(machines, std::views::transform(names,
445
		[&](auto& name) { return EnumSettingBase::MapEntry(std::move(name), count++); }));
446
	machines.emplace_back("C-BIOS_MSX2+", 0); // initial default machine
447
448
	defaultMachineSetting = std::make_unique<EnumSetting<int>>(
449
		*globalCommandController, "default_machine",
450
		"default machine (takes effect next time openMSX is started) - if no default setup is configured",
451
		0, std::move(machines));
452
453
	// TODO: add tabCompletion for this setting, so that it's easy to set with an existing setup file?
454
	defaultSetupSetting = std::make_unique<StringSetting>(
455
		*globalCommandController, "default_setup",
456
		"default setup (takes effect next time openMSX is started)",
457
		"");
458
}
459
460
MSXMotherBoard* Reactor::getMotherBoard() const
461
{
462
	assert(Thread::isMainThread());
463
	return activeBoard.get();
464
}
465
466
std::string_view Reactor::getMachineID() const
467
{
468
	return activeBoard ? activeBoard->getMachineID() : std::string_view{};
469
}
470
471
Reactor::Board Reactor::getMachine(std::string_view machineID) const
472
{
473
	if (auto it = std::ranges::find(boards, machineID, &MSXMotherBoard::getMachineID);
474
	    it != boards.end()) {
475
		return *it;
476
	}
477
	throw CommandException("No machine with ID: ", machineID);
478
}
479
480
Reactor::Board Reactor::createEmptyMotherBoard()
481
{
482
	return std::make_shared<MSXMotherBoard>(*this);
483
}
484
485
void Reactor::replaceBoard(MSXMotherBoard& oldBoard_, Board newBoard)
486
{
487
	assert(Thread::isMainThread());
488
489
	// Add new board.
490
	boards.push_back(newBoard);
491
492
	// Lookup old board (it must be present).
493
	auto it = find_unguarded(boards, &oldBoard_,
494
	                         [](auto& b) { return b.get(); });
495
496
	// If the old board was the active board, then activate the new board
497
	if (*it == activeBoard) {
498
		switchBoard(newBoard);
499
	}
500
501
	// Remove the old board.
502
	move_pop_back(boards, it);
503
}
504
505
void Reactor::switchMachine(const std::string& machine)
506
{
507
	if (!display) {
508
		display = std::make_unique<Display>(*this);
509
		// TODO: Currently it is not possible to move this call into the
510
		//       constructor of Display because the call to createVideoSystem()
511
		//       indirectly calls Reactor.getDisplay().
512
		display->createVideoSystem();
513
	}
514
515
	// create+load new machine
516
	// switch to new machine
517
	// delete old active machine
518
519
	assert(Thread::isMainThread());
520
	// Note: loadMachine can throw an exception and in that case the
521
	//       motherboard must be considered as not created at all.
522
	auto newBoard = createEmptyMotherBoard();
523
	newBoard->loadMachine(machine);
524
	boards.push_back(newBoard);
525
526
	auto oldBoard = activeBoard;
527
	switchBoard(newBoard);
528
	deleteBoard(oldBoard);
529
}
530
531
void Reactor::switchMachineFromSetup(zstring_view filename)
532
{
533
	if (!display) {
534
		display = std::make_unique<Display>(*this);
535
		// TODO: Currently it is not possible to move this call into the
536
		//       constructor of Display because the call to createVideoSystem()
537
		//       indirectly calls Reactor.getDisplay().
538
		display->createVideoSystem();
539
	}
540
541
	// create new machine
542
	// load state into machine
543
	// switch to new machine
544
	// delete old active machine
545
546
	assert(Thread::isMainThread());
547
	auto newBoard = createEmptyMotherBoard();
548
549
	try {
550
		XmlInputArchive in(filename);
551
		in.serialize("machine", *newBoard);
552
	} catch (XMLException& e) {
553
		throw CommandException("Cannot load setup, bad file format: ",
554
				       e.getMessage());
555
	} catch (MSXException& e) {
556
		throw CommandException("Cannot load setup: ", e.getMessage());
557
	}
558
559
	boards.push_back(newBoard);
560
561
	auto oldBoard = activeBoard;
562
	switchBoard(newBoard);
563
	deleteBoard(oldBoard);
564
}
565
566
void Reactor::switchBoard(Board newBoard)
567
{
568
	assert(Thread::isMainThread());
569
	assert(!newBoard || contains(boards, newBoard));
570
	assert(!activeBoard || contains(boards, activeBoard));
571
	if (activeBoard) {
572
		activeBoard->activate(false);
573
	}
574
	{
575
		// Don't hold the lock for longer than the actual switch.
576
		// In the past we had a potential for deadlocks here, because
577
		// (indirectly) the code below still acquires other locks.
578
		std::scoped_lock lock(mbMutex);
579
		activeBoard = newBoard;
580
	}
581
	eventDistributor->distributeEvent(MachineLoadedEvent());
582
	globalCliComm->update(CliComm::UpdateType::HARDWARE, getMachineID(), "select");
583
	if (activeBoard) {
584
		activeBoard->activate(true);
585
	}
586
}
587
588
void Reactor::deleteBoard(Board board)
589
{
590
	// Note: pass 'board' by-value to keep the parameter from changing
591
	// after the call to switchBoard(). switchBoard() changes the
592
	// 'activeBoard' member variable, so the 'board' parameter would change
593
	// if it were passed by reference to this method (AFAICS this only
594
	// happens in ~Reactor()).
595
	assert(Thread::isMainThread());
596
	if (!board) return;
597
598
	if (board == activeBoard) {
599
		// delete active board -> there is no active board anymore
600
		switchBoard(nullptr);
601
	}
602
	auto it = rfind_unguarded(boards, board);
603
	move_pop_back(boards, it);
604
}
605
606
void Reactor::enterMainLoop()
607
{
608
	// Note: this method can get called from different threads
609
	if (Thread::isMainThread()) {
610
		// Don't take lock in main thread to avoid recursive locking.
611
		if (activeBoard) {
612
			activeBoard->exitCPULoopSync();
613
		}
614
	} else {
615
		std::scoped_lock lock(mbMutex);
616
		if (activeBoard) {
617
			activeBoard->exitCPULoopAsync();
618
		}
619
		// Wake SDL_WaitEventTimeout() in case main thread is blocked
620
		// waiting for events. SDL_PushEvent() is thread-safe.
621
		SDL_Event wakeEvent{};
622
		wakeEvent.type = SDL_USEREVENT;
623
		SDL_PushEvent(&wakeEvent);
624
	}
625
}
626
627
void Reactor::runStartupScripts(const CommandLineParser& parser)
628
{
629
	auto& commandController = *globalCommandController;
630
631
	// execute init.tcl
632
	try {
633
		commandController.source(
634
			preferSystemFileContext().resolve("init.tcl"));
635
	} catch (FileException& e) {
636
		throw FatalError("Couldn't execute \"<openmsx>/share/init.tcl\": ", e.getMessage(), "\n"
637
		                 "Most likely you have an incomplete openMSX installation!!!");
638
	}
639
640
	// execute startup scripts
641
	for (const auto& s : parser.getStartupScripts()) {
642
		try {
643
			commandController.source(userFileContext().resolve(s));
644
		} catch (FileException& e) {
645
			throw FatalError("Couldn't execute script: ",
646
			                 e.getMessage());
647
		}
648
	}
649
	for (const auto& cmd : parser.getStartupCommands()) {
650
		try {
651
			commandController.executeCommand(cmd);
652
		} catch (CommandException& e) {
653
			throw FatalError("Couldn't execute command: ", cmd,
654
			                 '\n', e.getMessage());
655
		}
656
	}
657
658
	fullyStarted = true;
659
660
	// At this point openmsx is fully started, it's OK now to start
661
	// accepting external commands
662
	getGlobalCliComm().setAllowExternalCommands();
663
664
	// ...and re-emit any postponed message callbacks now that the scripts
665
	// are loaded
666
	tclCallbackMessages->redoPostponedCallbacks();
667
}
668
669
void Reactor::powerOn()
670
{
671
	// don't use Tcl to power up the machine, we cannot pass
672
	// exceptions through Tcl and ADVRAM might throw in its
673
	// powerUp() method. Solution is to implement dependencies
674
	// between devices so ADVRAM can check the error condition
675
	// in its constructor
676
	//commandController.executeCommand("set power on");
677
	if (activeBoard) {
678
		activeBoard->powerUp();
679
	}
680
}
681
682
void Reactor::run()
683
{
684
	bool blocked = (blockedCounter > 0) || !activeBoard;
685
	while (running) {
686
		// Compute timeout: sleep if blocked, but not past next RT-event.
687
		// This keeps UI responsive while avoiding busy-waiting when paused.
688
		auto timeoutMs = [&] -> std::optional<int> {
689
			static constexpr int MAX_WAIT_MS = 8;
690
			if (!blocked) return {};
691
			auto nextTime = getRTScheduler().getNextTime();
692
			if (!nextTime) return MAX_WAIT_MS;
693
			auto deltaUs = int64_t(*nextTime - Timer::getTime());
694
			return std::clamp(narrow<int>(deltaUs / 1000), 0, MAX_WAIT_MS);
695
		}();
696
		eventDistributor->deliverEvents(timeoutMs);
697
		blocked = (blockedCounter > 0) || !activeBoard;  // re-evaluate
698
		if (!blocked) {
699
			// copy shared_ptr to keep Board alive (e.g. in case of
700
			// Tcl callbacks)
701
			auto copy = activeBoard;
702
			blocked = !copy->execute();
703
		}
704
	}
705
}
706
707
void Reactor::unpause()
708
{
709
	if (paused) {
710
		paused = false;
711
		globalCliComm->update(CliComm::UpdateType::STATUS, "paused", "false");
712
		unblock();
713
	}
714
}
715
716
void Reactor::pause()
717
{
718
	if (!paused) {
719
		paused = true;
720
		globalCliComm->update(CliComm::UpdateType::STATUS, "paused", "true");
721
		block();
722
	}
723
}
724
725
void Reactor::block()
726
{
727
	++blockedCounter;
728
	enterMainLoop();
729
	getMixer().mute();
730
}
731
732
void Reactor::unblock()
733
{
734
	--blockedCounter;
735
	assert(blockedCounter >= 0);
736
	getMixer().unmute();
737
}
738
739
740
// Observer<Setting>
741
void Reactor::update(const Setting& setting) noexcept
742
{
743
	const auto& pauseSetting = getGlobalSettings().getPauseSetting();
744
	if (&setting == &pauseSetting) {
745
		if (pauseSetting.getBoolean()) {
746
			pause();
747
		} else {
748
			unpause();
749
		}
750
	}
751
}
752
753
// EventListener
754
bool Reactor::signalEvent(const Event& event)
755
{
756
	std::visit(overloaded{
757
		[&](const QuitEvent& /*e*/) {
758
			// check whether we should store the current setup
759
			if (auto* board = getMotherBoard()) {
760
				auto depth = saveSetupAtExitDepthSetting->getEnum();
761
				auto name = saveSetupAtExitNameSetting->getString();
762
				if (depth != NONE && !name.empty()) {
763
					auto filename = FileOperations::parseCommandFileArgument(
764
						name, Reactor::SETUP_DIR, "",
765
						Reactor::SETUP_EXTENSION);
766
					try {
767
						board->storeAsSetup(filename, depth);
768
					} catch (MSXException& e) {
769
						getGlobalCliComm().printWarning(strCat("Couldn't save setup to ",
770
						filename, " at exit: ", e.getMessage()));
771
					}
772
				}
773
			}
774
775
			enterMainLoop();
776
			running = false;
777
		},
778
		[&](const WindowEvent& e) {
779
			(void)e;
780
#if PLATFORM_ANDROID
781
			if (e.isMainWindow()) {
782
				// Android SDL port sends a (un)focus event when an app is put in background
783
				// by the OS for whatever reason (like an incoming phone call) and all screen
784
				// resources are taken away from the app.
785
				// In such case the app is supposed to behave as a good citizen
786
				// and minimize its resource usage and related battery drain.
787
				// The SDL Android port already takes care of halting the Java
788
				// part of the sound processing. The Display class makes sure that it wont try
789
				// to render anything to the (temporary missing) graphics resources but the
790
				// main emulation should also be temporary stopped, in order to minimize CPU usage
791
				if (e.getSdlWindowEvent().type == SDL_WINDOWEVENT_FOCUS_GAINED) {
792
					unblock();
793
				} else if (e.getSdlWindowEvent().type == SDL_WINDOWEVENT_FOCUS_LOST) {
794
					block();
795
				}
796
			}
797
#endif
798
		},
799
		[](const EventBase& /*e*/) {
800
			// clang-20 workaround: 'UNREACHABLE' is correct, but increases compile time from 11s to 680s
801
			//UNREACHABLE; // we didn't subscribe to this event...
802
		}
803
	}, event);
804
	return false;
805
}
806
807
808
// class ExitCommand
809
810
ExitCommand::ExitCommand(CommandController& commandController_,
811
                         EventDistributor& distributor_)
812
	: Command(commandController_, "exit")
813
	, distributor(distributor_)
814
{
815
}
816
817
void ExitCommand::execute(std::span<const TclObject> tokens, TclObject& /*result*/)
818
{
819
	checkNumArgs(tokens, Between{1, 2}, Prefix{1}, "?exitcode?");
820
	switch (tokens.size()) {
821
	case 1:
822
		exitCode = 0;
823
		break;
824
	case 2:
825
		exitCode = tokens[1].getInt(getInterpreter());
826
		break;
827
	}
828
	distributor.distributeEvent(QuitEvent());
829
}
830
831
std::string ExitCommand::help(std::span<const TclObject> /*tokens*/) const
832
{
833
	return "Use this command to stop the emulator.\n"
834
	       "Optionally you can pass an exit-code.\n";
835
}
836
837
838
// class MachineCommand
839
840
MachineCommand::MachineCommand(CommandController& commandController_,
841
                               Reactor& reactor_)
842
	: Command(commandController_, "machine")
843
	, reactor(reactor_)
844
{
845
}
846
847
void MachineCommand::execute(std::span<const TclObject> tokens, TclObject& result)
848
{
849
	checkNumArgs(tokens, Between{1, 2}, Prefix{1}, "?machinetype?");
850
	switch (tokens.size()) {
851
	case 1: // get current machine
852
		// nothing
853
		break;
854
	case 2:
855
		try {
856
			reactor.switchMachine(std::string(tokens[1].getString()));
857
		} catch (MSXException& e) {
858
			throw CommandException("Machine switching failed: ",
859
			                       e.getMessage());
860
		}
861
		break;
862
	}
863
	// Always return machineID (of current or of new machine).
864
	result = reactor.getMachineID();
865
}
866
867
std::string MachineCommand::help(std::span<const TclObject> /*tokens*/) const
868
{
869
	return "Switch to a different MSX machine.";
870
}
871
872
void MachineCommand::tabCompletion(std::vector<std::string>& tokens) const
873
{
874
	completeString(tokens, Reactor::getHwConfigs("machines"));
875
}
876
877
878
// class TestMachineCommand
879
880
TestMachineCommand::TestMachineCommand(CommandController& commandController_,
881
                                       Reactor& reactor_)
882
	: Command(commandController_, "test_machine")
883
	, reactor(reactor_)
884
{
885
}
886
887
void TestMachineCommand::execute(std::span<const TclObject> tokens,
888
                                 TclObject& result)
889
{
890
	checkNumArgs(tokens, 2, "machinetype");
891
	try {
892
		MSXMotherBoard mb(reactor);
893
		mb.loadMachine(std::string(tokens[1].getString()));
894
	} catch (MSXException& e) {
895
		result = e.getMessage(); // error
896
	}
897
}
898
899
std::string TestMachineCommand::help(std::span<const TclObject> /*tokens*/) const
900
{
901
	return "Test the configuration for the given machine. "
902
	       "Returns an error message explaining why the configuration is "
903
	       "invalid or an empty string in case of success.";
904
}
905
906
void TestMachineCommand::tabCompletion(std::vector<std::string>& tokens) const
907
{
908
	completeString(tokens, Reactor::getHwConfigs("machines"));
909
}
910
911
912
// class CreateMachineCommand
913
914
CreateMachineCommand::CreateMachineCommand(
915
	CommandController& commandController_, Reactor& reactor_)
916
	: Command(commandController_, "create_machine")
917
	, reactor(reactor_)
918
{
919
}
920
921
void CreateMachineCommand::execute(std::span<const TclObject> tokens, TclObject& result)
922
{
923
	checkNumArgs(tokens, 1, Prefix{1}, nullptr);
924
	auto newBoard = reactor.createEmptyMotherBoard();
925
	result = newBoard->getMachineID();
926
	reactor.boards.push_back(std::move(newBoard));
927
}
928
929
std::string CreateMachineCommand::help(std::span<const TclObject> /*tokens*/) const
930
{
931
	return "Creates a new (empty) MSX machine. Returns the ID for the new "
932
	       "machine.\n"
933
	       "Use 'load_machine' to actually load a machine configuration "
934
	       "into this new machine.\n"
935
	       "The main reason create_machine and load_machine are two "
936
	       "separate commands is that sometimes you already want to know "
937
	       "the ID of the machine before load_machine starts emitting "
938
	       "events for this machine.";
939
}
940
941
942
// class DeleteMachineCommand
943
944
DeleteMachineCommand::DeleteMachineCommand(
945
	CommandController& commandController_, Reactor& reactor_)
946
	: Command(commandController_, "delete_machine")
947
	, reactor(reactor_)
948
{
949
}
950
951
void DeleteMachineCommand::execute(std::span<const TclObject> tokens,
952
                                   TclObject& /*result*/)
953
{
954
	checkNumArgs(tokens, 2, "id");
955
	reactor.deleteBoard(reactor.getMachine(tokens[1].getString()));
956
}
957
958
std::string DeleteMachineCommand::help(std::span<const TclObject> /*tokens*/) const
959
{
960
	return "Deletes the given MSX machine.";
961
}
962
963
void DeleteMachineCommand::tabCompletion(std::vector<std::string>& tokens) const
964
{
965
	completeString(tokens, reactor.getMachineIDs());
966
}
967
968
969
// class ListMachinesCommand
970
971
ListMachinesCommand::ListMachinesCommand(
972
	CommandController& commandController_, Reactor& reactor_)
973
	: Command(commandController_, "list_machines")
974
	, reactor(reactor_)
975
{
976
}
977
978
void ListMachinesCommand::execute(std::span<const TclObject> /*tokens*/,
979
                                  TclObject& result)
980
{
981
	result.addListElements(reactor.getMachineIDs());
982
}
983
984
std::string ListMachinesCommand::help(std::span<const TclObject> /*tokens*/) const
985
{
986
	return "Returns a list of all machine IDs.";
987
}
988
989
990
// class ActivateMachineCommand
991
992
ActivateMachineCommand::ActivateMachineCommand(
993
	CommandController& commandController_, Reactor& reactor_)
994
	: Command(commandController_, "activate_machine")
995
	, reactor(reactor_)
996
{
997
}
998
999
void ActivateMachineCommand::execute(std::span<const TclObject> tokens,
1000
                                     TclObject& result)
1001
{
1002
	checkNumArgs(tokens, Between{1, 2}, Prefix{1}, "id");
1003
	switch (tokens.size()) {
1004
	case 1:
1005
		break;
1006
	case 2:
1007
		reactor.switchBoard(reactor.getMachine(tokens[1].getString()));
1008
		break;
1009
	}
1010
	result = reactor.getMachineID();
1011
}
1012
1013
std::string ActivateMachineCommand::help(std::span<const TclObject> /*tokens*/) const
1014
{
1015
	return "Make another machine the active msx machine.\n"
1016
	       "Or when invoked without arguments, query the ID of the "
1017
	       "active msx machine.";
1018
}
1019
1020
void ActivateMachineCommand::tabCompletion(std::vector<std::string>& tokens) const
1021
{
1022
	completeString(tokens, reactor.getMachineIDs());
1023
}
1024
1025
1026
// class StoreMachineCommand
1027
1028
StoreMachineCommand::StoreMachineCommand(
1029
	CommandController& commandController_, Reactor& reactor_)
1030
	: Command(commandController_, "store_machine")
1031
	, reactor(reactor_)
1032
{
1033
}
1034
1035
void StoreMachineCommand::execute(std::span<const TclObject> tokens, TclObject& result)
1036
{
1037
	checkNumArgs(tokens, 3, Prefix{1}, "id filename");
1038
	const auto& machineID = tokens[1].getString();
1039
	const auto& filename = tokens[2].getString();
1040
1041
	const auto& board = *reactor.getMachine(machineID);
1042
1043
	XmlOutputArchive out(filename);
1044
	out.serialize("machine", board);
1045
	out.close();
1046
	result = filename;
1047
}
1048
1049
std::string StoreMachineCommand::help(std::span<const TclObject> /*tokens*/) const
1050
{
1051
	return
1052
		"store_machine machineID <filename>  Save state of machine \"machineID\" to indicated file\n"
1053
		"\n"
1054
		"This is a low-level command, the 'savestate' script is easier to use.";
1055
}
1056
1057
void StoreMachineCommand::tabCompletion(std::vector<std::string>& tokens) const
1058
{
1059
	completeString(tokens, reactor.getMachineIDs());
1060
}
1061
1062
1063
// class RestoreMachineCommand
1064
1065
RestoreMachineCommand::RestoreMachineCommand(
1066
	CommandController& commandController_, Reactor& reactor_)
1067
	: Command(commandController_, "restore_machine")
1068
	, reactor(reactor_)
1069
{
1070
}
1071
1072
void RestoreMachineCommand::execute(std::span<const TclObject> tokens,
1073
                                    TclObject& result)
1074
{
1075
	checkNumArgs(tokens, 2, Prefix{1}, "filename");
1076
	auto newBoard = reactor.createEmptyMotherBoard();
1077
1078
	const auto filename = FileOperations::expandTilde(std::string(tokens[1].getString()));
1079
1080
	try {
1081
		XmlInputArchive in(filename);
1082
		in.serialize("machine", *newBoard);
1083
	} catch (XMLException& e) {
1084
		throw CommandException("Cannot load state, bad file format: ",
1085
		                       e.getMessage());
1086
	} catch (MSXException& e) {
1087
		throw CommandException("Cannot load state: ", e.getMessage());
1088
	}
1089
1090
	// Savestate also contains stuff like the keyboard state at the moment
1091
	// the snapshot was created (this is required for reverse/replay). But
1092
	// now we want the MSX to see the actual host keyboard state.
1093
	newBoard->getStateChangeDistributor().stopReplay(newBoard->getCurrentTime());
1094
1095
	result = newBoard->getMachineID();
1096
	reactor.boards.push_back(std::move(newBoard));
1097
}
1098
1099
std::string RestoreMachineCommand::help(std::span<const TclObject> /*tokens*/) const
1100
{
1101
	return "restore_machine                       Load state from last saved state in default directory\n"
1102
	       "restore_machine <filename>            Load state from indicated file\n"
1103
	       "\n"
1104
	       "This is a low-level command, the 'loadstate' script is easier to use.";
1105
}
1106
1107
void RestoreMachineCommand::tabCompletion(std::vector<std::string>& tokens) const
1108
{
1109
	completeFileName(tokens, userFileContext());
1110
}
1111
1112
1113
// class SetupCommand
1114
1115
SetupCommand::SetupCommand(CommandController& commandController_,
1116
                               Reactor& reactor_)
1117
	: Command(commandController_, "setup")
1118
	, reactor(reactor_)
1119
{
1120
}
1121
1122
void SetupCommand::execute(std::span<const TclObject> tokens, TclObject& result)
1123
{
1124
	checkNumArgs(tokens, 2, Prefix{1}, "filename");
1125
1126
	// resolve the filename
1127
	auto context = userDataFileContext(Reactor::SETUP_DIR);
1128
	std::string fileNameArg(tokens[1].getString());
1129
	// Assume the user left out the extension, so add the normal extension
1130
	auto filename = context.resolve(tmpStrCat(fileNameArg, Reactor::SETUP_EXTENSION));
1131
1132
	// switch to this setup
1133
	try {
1134
		reactor.switchMachineFromSetup(filename);
1135
	} catch (MSXException& e) {
1136
		throw CommandException("Switching to setup failed: ",
1137
				       e.getMessage());
1138
	}
1139
1140
	// Always return machineID (of current or of new machine).
1141
	result = reactor.getMachineID();
1142
}
1143
1144
std::string SetupCommand::help(std::span<const TclObject> /*tokens*/) const
1145
{
1146
	return "Switch to a different MSX setup.";
1147
}
1148
1149
void SetupCommand::tabCompletion(std::vector<std::string>& tokens) const
1150
{
1151
	completeString(tokens, Reactor::getSetups());
1152
}
1153
1154
1155
// class GetClipboardCommand
1156
1157
GetClipboardCommand::GetClipboardCommand(
1158
	CommandController& commandController_, Reactor& reactor_)
1159
	: Command(commandController_, "get_clipboard_text")
1160
	, reactor(reactor_)
1161
{
1162
	// Note: cannot yet call getReactor().getDisplay() (e.g. to cache it)
1163
	//   display may not yet be initialized.
1164
}
1165
1166
void GetClipboardCommand::execute(std::span<const TclObject> tokens, TclObject& result)
1167
{
1168
	checkNumArgs(tokens, 1, Prefix{1}, nullptr);
1169
	result = reactor.getDisplay().getVideoSystem().getClipboardText();
1170
}
1171
1172
std::string GetClipboardCommand::help(std::span<const TclObject> /*tokens*/) const
1173
{
1174
	return "Returns the (text) content of the clipboard as a string.";
1175
}
1176
1177
1178
// class SetClipboardCommand
1179
1180
SetClipboardCommand::SetClipboardCommand(
1181
	CommandController& commandController_, Reactor& reactor_)
1182
	: Command(commandController_, "set_clipboard_text")
1183
	, reactor(reactor_)
1184
{
1185
}
1186
1187
void SetClipboardCommand::execute(std::span<const TclObject> tokens, TclObject& /*result*/)
1188
{
1189
	checkNumArgs(tokens, 2, "text");
1190
	reactor.getDisplay().getVideoSystem().setClipboardText(tokens[1].getString());
1191
}
1192
1193
std::string SetClipboardCommand::help(std::span<const TclObject> /*tokens*/) const
1194
{
1195
	return "Send the given string to the clipboard.";
1196
}
1197
1198
1199
// class ConfigInfo
1200
1201
ConfigInfo::ConfigInfo(InfoCommand& openMSXInfoCommand,
1202
	               const std::string& configName_)
1203
	: InfoTopic(openMSXInfoCommand, configName_)
1204
	, configName(configName_)
1205
{
1206
}
1207
1208
void ConfigInfo::execute(std::span<const TclObject> tokens, TclObject& result) const
1209
{
1210
	// TODO make meta info available through this info topic
1211
	switch (tokens.size()) {
1212
	case 2: {
1213
		result.addListElements(Reactor::getHwConfigs(configName));
1214
		break;
1215
	}
1216
	case 3: {
1217
		try {
1218
			std::array<char, 8192> allocBuffer; // tweak
1219
			XMLDocument doc{allocBuffer.data(), sizeof(allocBuffer)};
1220
			HardwareConfig::loadConfig(
1221
				doc, configName, tokens[2].getString());
1222
			if (const auto* info = doc.getRoot()->findChild("info")) {
1223
				for (const auto& c : info->getChildren()) {
1224
					result.addDictKeyValue(c.getName(), c.getData());
1225
				}
1226
			}
1227
		} catch (MSXException& e) {
1228
			throw CommandException(
1229
				"Couldn't get config info: ", e.getMessage());
1230
		}
1231
		break;
1232
	}
1233
	default:
1234
		throw CommandException("Too many parameters");
1235
	}
1236
}
1237
1238
std::string ConfigInfo::help(std::span<const TclObject> /*tokens*/) const
1239
{
1240
	return strCat("Shows a list of available ", configName, ", "
1241
	              "or get meta information about the selected item.\n");
1242
}
1243
1244
void ConfigInfo::tabCompletion(std::vector<std::string>& tokens) const
1245
{
1246
	completeString(tokens, Reactor::getHwConfigs(configName));
1247
}
1248
1249
1250
// class RealTimeInfo
1251
1252
RealTimeInfo::RealTimeInfo(InfoCommand& openMSXInfoCommand)
1253
	: InfoTopic(openMSXInfoCommand, "realtime")
1254
	, reference(Timer::getTime())
1255
{
1256
}
1257
1258
void RealTimeInfo::execute(std::span<const TclObject> /*tokens*/,
1259
                           TclObject& result) const
1260
{
1261
	auto delta = Timer::getTime() - reference;
1262
	result = narrow_cast<double>(delta) * (1.0 / 1000000.0);
1263
}
1264
1265
std::string RealTimeInfo::help(std::span<const TclObject> /*tokens*/) const
1266
{
1267
	return "Returns the time in seconds since openMSX was started.";
1268
}
1269
1270
1271
// SoftwareInfoTopic
1272
1273
SoftwareInfoTopic::SoftwareInfoTopic(InfoCommand& openMSXInfoCommand, Reactor& reactor_)
1274
	: InfoTopic(openMSXInfoCommand, "software")
1275
	, reactor(reactor_)
1276
{
1277
}
1278
1279
void SoftwareInfoTopic::execute(
1280
	std::span<const TclObject> tokens, TclObject& result) const
1281
{
1282
	const auto& romDatabase = reactor.getSoftwareDatabase();
1283
	if (tokens.size() == 2) {
1284
		std::array<char, 40> buf;
1285
		for (const auto& [sha1, _] : romDatabase.getFullDB()) {
1286
			result.addListElement(sha1.toString(buf));
1287
		}
1288
		return;
1289
	}
1290
1291
	if (tokens.size() != 3) {
1292
		throw CommandException("Wrong number of parameters");
1293
	}
1294
1295
	Sha1Sum sha1sum(tokens[2].getString());
1296
	const RomInfo* romInfo = romDatabase.fetchRomInfo(sha1sum);
1297
	if (!romInfo) {
1298
		// no match found
1299
		throw CommandException(
1300
			"Software with sha1sum ", sha1sum, " not found");
1301
	}
1302
1303
	const char* bufStart = romDatabase.getBufferStart();
1304
	result.addDictKeyValues("title",            romInfo->getTitle(bufStart),
1305
	                        "year",             romInfo->getYear(bufStart),
1306
	                        "company",          romInfo->getCompany(bufStart),
1307
	                        "country",          romInfo->getCountry(bufStart),
1308
	                        "orig_type",        romInfo->getOrigType(bufStart),
1309
	                        "remark",           romInfo->getRemark(bufStart),
1310
	                        "original",         romInfo->getOriginal(),
1311
	                        "mapper_type_name", RomInfo::romTypeToName(romInfo->getRomType()),
1312
	                        "genmsxid",         romInfo->getGenMSXid());
1313
}
1314
1315
std::string SoftwareInfoTopic::help(std::span<const TclObject> /*tokens*/) const
1316
{
1317
	return "Returns information about the software "
1318
	       "given its sha1sum, in a paired list.";
1319
}
1320
1321
} // namespace openmsx