regtest_host.cpp (22255B)
1 // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com> 2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) 3 4 #include "core/achievements.h" 5 #include "core/controller.h" 6 #include "core/fullscreen_ui.h" 7 #include "core/game_list.h" 8 #include "core/gpu.h" 9 #include "core/host.h" 10 #include "core/system.h" 11 12 #include "scmversion/scmversion.h" 13 14 #include "util/cd_image.h" 15 #include "util/gpu_device.h" 16 #include "util/imgui_fullscreen.h" 17 #include "util/imgui_manager.h" 18 #include "util/input_manager.h" 19 #include "util/platform_misc.h" 20 21 #include "common/assert.h" 22 #include "common/crash_handler.h" 23 #include "common/error.h" 24 #include "common/file_system.h" 25 #include "common/log.h" 26 #include "common/memory_settings_interface.h" 27 #include "common/path.h" 28 #include "common/string_util.h" 29 #include "common/timer.h" 30 31 #include <csignal> 32 #include <cstdio> 33 34 Log_SetChannel(RegTestHost); 35 36 namespace RegTestHost { 37 static bool ParseCommandLineParameters(int argc, char* argv[], std::optional<SystemBootParameters>& autoboot); 38 static void PrintCommandLineVersion(); 39 static void PrintCommandLineHelp(const char* progname); 40 static bool InitializeConfig(); 41 static void InitializeEarlyConsole(); 42 static void HookSignals(); 43 static bool SetFolders(); 44 static bool SetNewDataRoot(const std::string& filename); 45 static std::string GetFrameDumpFilename(u32 frame); 46 } // namespace RegTestHost 47 48 static std::unique_ptr<MemorySettingsInterface> s_base_settings_interface; 49 50 static u32 s_frames_to_run = 60 * 60; 51 static u32 s_frames_remaining = 0; 52 static u32 s_frame_dump_interval = 0; 53 static std::string s_dump_base_directory; 54 55 bool RegTestHost::SetFolders() 56 { 57 std::string program_path(FileSystem::GetProgramPath()); 58 INFO_LOG("Program Path: {}", program_path); 59 60 EmuFolders::AppRoot = Path::Canonicalize(Path::GetDirectory(program_path)); 61 EmuFolders::DataRoot = EmuFolders::AppRoot; 62 63 #ifdef __APPLE__ 64 static constexpr char MAC_DATA_DIR[] = "Library/Application Support/DuckStation"; 65 const char* home_dir = getenv("HOME"); 66 if (home_dir) 67 EmuFolders::DataRoot = Path::Combine(home_dir, MAC_DATA_DIR); 68 #endif 69 70 // On Windows/Linux, these are in the binary directory. 71 EmuFolders::Resources = Path::Combine(EmuFolders::AppRoot, "resources"); 72 73 DEV_LOG("AppRoot Directory: {}", EmuFolders::AppRoot); 74 DEV_LOG("DataRoot Directory: {}", EmuFolders::DataRoot); 75 DEV_LOG("Resources Directory: {}", EmuFolders::Resources); 76 77 // Write crash dumps to the data directory, since that'll be accessible for certain. 78 CrashHandler::SetWriteDirectory(EmuFolders::DataRoot); 79 80 // the resources directory should exist, bail out if not 81 if (!FileSystem::DirectoryExists(EmuFolders::Resources.c_str())) 82 { 83 ERROR_LOG("Resources directory is missing, your installation is incomplete."); 84 return false; 85 } 86 87 return true; 88 } 89 90 bool RegTestHost::InitializeConfig() 91 { 92 SetFolders(); 93 94 s_base_settings_interface = std::make_unique<MemorySettingsInterface>(); 95 Host::Internal::SetBaseSettingsLayer(s_base_settings_interface.get()); 96 97 // default settings for runner 98 SettingsInterface& si = *s_base_settings_interface.get(); 99 g_settings.Save(si, false); 100 si.SetStringValue("GPU", "Renderer", Settings::GetRendererName(GPURenderer::Software)); 101 si.SetBoolValue("GPU", "DisableShaderCache", true); 102 si.SetStringValue("Pad1", "Type", Controller::GetControllerInfo(ControllerType::AnalogController)->name); 103 si.SetStringValue("Pad2", "Type", Controller::GetControllerInfo(ControllerType::None)->name); 104 si.SetStringValue("MemoryCards", "Card1Type", Settings::GetMemoryCardTypeName(MemoryCardType::NonPersistent)); 105 si.SetStringValue("MemoryCards", "Card2Type", Settings::GetMemoryCardTypeName(MemoryCardType::None)); 106 si.SetStringValue("ControllerPorts", "MultitapMode", Settings::GetMultitapModeName(MultitapMode::Disabled)); 107 si.SetStringValue("Audio", "Backend", AudioStream::GetBackendName(AudioBackend::Null)); 108 si.SetBoolValue("Logging", "LogToConsole", false); 109 si.SetBoolValue("Logging", "LogToFile", false); 110 si.SetStringValue("Logging", "LogLevel", Settings::GetLogLevelName(LOGLEVEL_INFO)); 111 si.SetBoolValue("Main", "ApplyGameSettings", false); // don't want game settings interfering 112 si.SetBoolValue("BIOS", "PatchFastBoot", true); // no point validating the bios intro.. 113 si.SetFloatValue("Main", "EmulationSpeed", 0.0f); 114 115 // disable all sources 116 for (u32 i = 0; i < static_cast<u32>(InputSourceType::Count); i++) 117 si.SetBoolValue("InputSources", InputManager::InputSourceToString(static_cast<InputSourceType>(i)), false); 118 119 EmuFolders::LoadConfig(*s_base_settings_interface.get()); 120 EmuFolders::EnsureFoldersExist(); 121 122 // imgui setup, make sure it doesn't bug out 123 ImGuiManager::SetFontPathAndRange(std::string(), {0x0020, 0x00FF, 0, 0}); 124 125 return true; 126 } 127 128 void Host::ReportFatalError(std::string_view title, std::string_view message) 129 { 130 ERROR_LOG("ReportFatalError: {}", message); 131 abort(); 132 } 133 134 void Host::ReportErrorAsync(std::string_view title, std::string_view message) 135 { 136 if (!title.empty() && !message.empty()) 137 ERROR_LOG("ReportErrorAsync: {}: {}", title, message); 138 else if (!message.empty()) 139 ERROR_LOG("ReportErrorAsync: {}", message); 140 } 141 142 bool Host::ConfirmMessage(std::string_view title, std::string_view message) 143 { 144 if (!title.empty() && !message.empty()) 145 ERROR_LOG("ConfirmMessage: {}: {}", title, message); 146 else if (!message.empty()) 147 ERROR_LOG("ConfirmMessage: {}", message); 148 149 return true; 150 } 151 152 void Host::ReportDebuggerMessage(std::string_view message) 153 { 154 ERROR_LOG("ReportDebuggerMessage: {}", message); 155 } 156 157 std::span<const std::pair<const char*, const char*>> Host::GetAvailableLanguageList() 158 { 159 return {}; 160 } 161 162 bool Host::ChangeLanguage(const char* new_language) 163 { 164 return false; 165 } 166 167 s32 Host::Internal::GetTranslatedStringImpl(std::string_view context, std::string_view msg, char* tbuf, 168 size_t tbuf_space) 169 { 170 if (msg.size() > tbuf_space) 171 return -1; 172 else if (msg.empty()) 173 return 0; 174 175 std::memcpy(tbuf, msg.data(), msg.size()); 176 return static_cast<s32>(msg.size()); 177 } 178 179 std::string Host::TranslatePluralToString(const char* context, const char* msg, const char* disambiguation, int count) 180 { 181 TinyString count_str = TinyString::from_format("{}", count); 182 183 std::string ret(msg); 184 for (;;) 185 { 186 std::string::size_type pos = ret.find("%n"); 187 if (pos == std::string::npos) 188 break; 189 190 ret.replace(pos, pos + 2, count_str.view()); 191 } 192 193 return ret; 194 } 195 196 SmallString Host::TranslatePluralToSmallString(const char* context, const char* msg, const char* disambiguation, 197 int count) 198 { 199 SmallString ret(msg); 200 ret.replace("%n", TinyString::from_format("{}", count)); 201 return ret; 202 } 203 204 void Host::LoadSettings(SettingsInterface& si, std::unique_lock<std::mutex>& lock) 205 { 206 } 207 208 void Host::CheckForSettingsChanges(const Settings& old_settings) 209 { 210 } 211 212 void Host::CommitBaseSettingChanges() 213 { 214 // noop, in memory 215 } 216 217 bool Host::ResourceFileExists(std::string_view filename, bool allow_override) 218 { 219 const std::string path(Path::Combine(EmuFolders::Resources, filename)); 220 return FileSystem::FileExists(path.c_str()); 221 } 222 223 std::optional<DynamicHeapArray<u8>> Host::ReadResourceFile(std::string_view filename, bool allow_override) 224 { 225 const std::string path(Path::Combine(EmuFolders::Resources, filename)); 226 std::optional<DynamicHeapArray<u8>> ret(FileSystem::ReadBinaryFile(path.c_str())); 227 if (!ret.has_value()) 228 ERROR_LOG("Failed to read resource file '{}'", filename); 229 return ret; 230 } 231 232 std::optional<std::string> Host::ReadResourceFileToString(std::string_view filename, bool allow_override) 233 { 234 const std::string path(Path::Combine(EmuFolders::Resources, filename)); 235 std::optional<std::string> ret(FileSystem::ReadFileToString(path.c_str())); 236 if (!ret.has_value()) 237 ERROR_LOG("Failed to read resource file to string '{}'", filename); 238 return ret; 239 } 240 241 std::optional<std::time_t> Host::GetResourceFileTimestamp(std::string_view filename, bool allow_override) 242 { 243 const std::string path(Path::Combine(EmuFolders::Resources, filename)); 244 FILESYSTEM_STAT_DATA sd; 245 if (!FileSystem::StatFile(path.c_str(), &sd)) 246 { 247 ERROR_LOG("Failed to stat resource file '{}'", filename); 248 return std::nullopt; 249 } 250 251 return sd.ModificationTime; 252 } 253 254 void Host::OnSystemStarting() 255 { 256 // 257 } 258 259 void Host::OnSystemStarted() 260 { 261 // 262 } 263 264 void Host::OnSystemDestroyed() 265 { 266 // 267 } 268 269 void Host::OnSystemPaused() 270 { 271 // 272 } 273 274 void Host::OnSystemResumed() 275 { 276 // 277 } 278 279 void Host::OnIdleStateChanged() 280 { 281 // 282 } 283 284 void Host::OnPerformanceCountersUpdated() 285 { 286 // 287 } 288 289 void Host::OnGameChanged(const std::string& disc_path, const std::string& game_serial, const std::string& game_name) 290 { 291 INFO_LOG("Disc Path: {}", disc_path); 292 INFO_LOG("Game Serial: {}", game_serial); 293 INFO_LOG("Game Name: {}", game_name); 294 } 295 296 void Host::OnMediaCaptureStarted() 297 { 298 // 299 } 300 301 void Host::OnMediaCaptureStopped() 302 { 303 // 304 } 305 306 void Host::PumpMessagesOnCPUThread() 307 { 308 s_frames_remaining--; 309 if (s_frames_remaining == 0) 310 System::ShutdownSystem(false); 311 } 312 313 void Host::RunOnCPUThread(std::function<void()> function, bool block /* = false */) 314 { 315 // only one thread in this version... 316 function(); 317 } 318 319 void Host::RequestResizeHostDisplay(s32 width, s32 height) 320 { 321 // 322 } 323 324 void Host::RequestExitApplication(bool save_state_if_running) 325 { 326 // 327 } 328 329 void Host::RequestExitBigPicture() 330 { 331 // 332 } 333 334 void Host::RequestSystemShutdown(bool allow_confirm, bool save_state) 335 { 336 // 337 } 338 339 bool Host::IsFullscreen() 340 { 341 return false; 342 } 343 344 void Host::SetFullscreen(bool enabled) 345 { 346 // 347 } 348 349 std::optional<WindowInfo> Host::AcquireRenderWindow(bool recreate_window) 350 { 351 WindowInfo wi; 352 wi.SetSurfaceless(); 353 return wi; 354 } 355 356 void Host::ReleaseRenderWindow() 357 { 358 // 359 } 360 361 void Host::FrameDone() 362 { 363 const u32 frame = System::GetFrameNumber(); 364 if (s_frame_dump_interval > 0 && (s_frame_dump_interval == 1 || (frame % s_frame_dump_interval) == 0)) 365 { 366 std::string dump_filename(RegTestHost::GetFrameDumpFilename(frame)); 367 g_gpu->WriteDisplayTextureToFile(std::move(dump_filename)); 368 } 369 } 370 371 void Host::OpenURL(std::string_view url) 372 { 373 // 374 } 375 376 bool Host::CopyTextToClipboard(std::string_view text) 377 { 378 return false; 379 } 380 381 void Host::SetMouseMode(bool relative, bool hide_cursor) 382 { 383 // 384 } 385 386 void Host::OnAchievementsLoginRequested(Achievements::LoginRequestReason reason) 387 { 388 // noop 389 } 390 391 void Host::OnAchievementsLoginSuccess(const char* username, u32 points, u32 sc_points, u32 unread_messages) 392 { 393 // noop 394 } 395 396 void Host::OnAchievementsRefreshed() 397 { 398 // noop 399 } 400 401 void Host::OnAchievementsHardcoreModeChanged(bool enabled) 402 { 403 // noop 404 } 405 406 void Host::OnCoverDownloaderOpenRequested() 407 { 408 // noop 409 } 410 411 bool Host::ShouldPreferHostFileSelector() 412 { 413 return false; 414 } 415 416 void Host::OpenHostFileSelectorAsync(std::string_view title, bool select_directory, FileSelectorCallback callback, 417 FileSelectorFilters filters /* = FileSelectorFilters() */, 418 std::string_view initial_directory /* = std::string_view() */) 419 { 420 callback(std::string()); 421 } 422 423 std::optional<u32> InputManager::ConvertHostKeyboardStringToCode(std::string_view str) 424 { 425 return std::nullopt; 426 } 427 428 std::optional<std::string> InputManager::ConvertHostKeyboardCodeToString(u32 code) 429 { 430 return std::nullopt; 431 } 432 433 const char* InputManager::ConvertHostKeyboardCodeToIcon(u32 code) 434 { 435 return nullptr; 436 } 437 438 void Host::AddFixedInputBindings(SettingsInterface& si) 439 { 440 // noop 441 } 442 443 void Host::OnInputDeviceConnected(std::string_view identifier, std::string_view device_name) 444 { 445 // noop 446 } 447 448 void Host::OnInputDeviceDisconnected(InputBindingKey key, std::string_view identifier) 449 { 450 // noop 451 } 452 453 std::optional<WindowInfo> Host::GetTopLevelWindowInfo() 454 { 455 return std::nullopt; 456 } 457 458 void Host::RefreshGameListAsync(bool invalidate_cache) 459 { 460 // noop 461 } 462 463 void Host::CancelGameListRefresh() 464 { 465 // noop 466 } 467 468 BEGIN_HOTKEY_LIST(g_host_hotkeys) 469 END_HOTKEY_LIST() 470 471 static void SignalHandler(int signal) 472 { 473 std::signal(signal, SIG_DFL); 474 475 // MacOS is missing std::quick_exit() despite it being C++11... 476 #ifndef __APPLE__ 477 std::quick_exit(1); 478 #else 479 _Exit(1); 480 #endif 481 } 482 483 void RegTestHost::HookSignals() 484 { 485 std::signal(SIGINT, SignalHandler); 486 std::signal(SIGTERM, SignalHandler); 487 } 488 489 void RegTestHost::InitializeEarlyConsole() 490 { 491 const bool was_console_enabled = Log::IsConsoleOutputEnabled(); 492 if (!was_console_enabled) 493 Log::SetConsoleOutputParams(true); 494 } 495 496 void RegTestHost::PrintCommandLineVersion() 497 { 498 InitializeEarlyConsole(); 499 std::fprintf(stderr, "DuckStation Regression Test Runner Version %s (%s)\n", g_scm_tag_str, g_scm_branch_str); 500 std::fprintf(stderr, "https://github.com/stenzek/duckstation\n"); 501 std::fprintf(stderr, "\n"); 502 } 503 504 void RegTestHost::PrintCommandLineHelp(const char* progname) 505 { 506 InitializeEarlyConsole(); 507 PrintCommandLineVersion(); 508 std::fprintf(stderr, "Usage: %s [parameters] [--] [boot filename]\n", progname); 509 std::fprintf(stderr, "\n"); 510 std::fprintf(stderr, " -help: Displays this information and exits.\n"); 511 std::fprintf(stderr, " -version: Displays version information and exits.\n"); 512 std::fprintf(stderr, " -dumpdir: Set frame dump base directory (will be dumped to basedir/gametitle).\n"); 513 std::fprintf(stderr, " -dumpinterval: Dumps every N frames.\n"); 514 std::fprintf(stderr, " -frames: Sets the number of frames to execute.\n"); 515 std::fprintf(stderr, " -log <level>: Sets the log level. Defaults to verbose.\n"); 516 std::fprintf(stderr, " -renderer <renderer>: Sets the graphics renderer. Default to software.\n"); 517 std::fprintf(stderr, " --: Signals that no more arguments will follow and the remaining\n" 518 " parameters make up the filename. Use when the filename contains\n" 519 " spaces or starts with a dash.\n"); 520 std::fprintf(stderr, "\n"); 521 } 522 523 static std::optional<SystemBootParameters>& AutoBoot(std::optional<SystemBootParameters>& autoboot) 524 { 525 if (!autoboot) 526 autoboot.emplace(); 527 528 return autoboot; 529 } 530 531 bool RegTestHost::ParseCommandLineParameters(int argc, char* argv[], std::optional<SystemBootParameters>& autoboot) 532 { 533 bool no_more_args = false; 534 for (int i = 1; i < argc; i++) 535 { 536 if (!no_more_args) 537 { 538 #define CHECK_ARG(str) !std::strcmp(argv[i], str) 539 #define CHECK_ARG_PARAM(str) (!std::strcmp(argv[i], str) && ((i + 1) < argc)) 540 541 if (CHECK_ARG("-help")) 542 { 543 PrintCommandLineHelp(argv[0]); 544 return false; 545 } 546 else if (CHECK_ARG("-version")) 547 { 548 PrintCommandLineVersion(); 549 return false; 550 } 551 else if (CHECK_ARG_PARAM("-dumpdir")) 552 { 553 s_dump_base_directory = argv[++i]; 554 if (s_dump_base_directory.empty()) 555 { 556 ERROR_LOG("Invalid dump directory specified."); 557 return false; 558 } 559 560 continue; 561 } 562 else if (CHECK_ARG_PARAM("-dumpinterval")) 563 { 564 s_frame_dump_interval = StringUtil::FromChars<u32>(argv[++i]).value_or(0); 565 if (s_frame_dump_interval <= 0) 566 { 567 ERROR_LOG("Invalid dump interval specified: {}", argv[i]); 568 return false; 569 } 570 571 continue; 572 } 573 else if (CHECK_ARG_PARAM("-frames")) 574 { 575 s_frames_to_run = StringUtil::FromChars<u32>(argv[++i]).value_or(0); 576 if (s_frames_to_run == 0) 577 { 578 ERROR_LOG("Invalid frame count specified: {}", argv[i]); 579 return false; 580 } 581 582 continue; 583 } 584 else if (CHECK_ARG_PARAM("-log")) 585 { 586 std::optional<LOGLEVEL> level = Settings::ParseLogLevelName(argv[++i]); 587 if (!level.has_value()) 588 { 589 ERROR_LOG("Invalid log level specified."); 590 return false; 591 } 592 593 Log::SetLogLevel(level.value()); 594 s_base_settings_interface->SetStringValue("Logging", "LogLevel", Settings::GetLogLevelName(level.value())); 595 continue; 596 } 597 else if (CHECK_ARG_PARAM("-console")) 598 { 599 Log::SetConsoleOutputParams(true); 600 s_base_settings_interface->SetBoolValue("Logging", "LogToConsole", true); 601 continue; 602 } 603 else if (CHECK_ARG_PARAM("-renderer")) 604 { 605 std::optional<GPURenderer> renderer = Settings::ParseRendererName(argv[++i]); 606 if (!renderer.has_value()) 607 { 608 ERROR_LOG("Invalid renderer specified."); 609 return false; 610 } 611 612 s_base_settings_interface->SetStringValue("GPU", "Renderer", Settings::GetRendererName(renderer.value())); 613 continue; 614 } 615 else if (CHECK_ARG_PARAM("-upscale")) 616 { 617 const u32 upscale = StringUtil::FromChars<u32>(argv[++i]).value_or(0); 618 if (upscale == 0) 619 { 620 ERROR_LOG("Invalid upscale value."); 621 return false; 622 } 623 624 INFO_LOG("Setting upscale to {}.", upscale); 625 s_base_settings_interface->SetIntValue("GPU", "ResolutionScale", static_cast<s32>(upscale)); 626 continue; 627 } 628 else if (CHECK_ARG_PARAM("-cpu")) 629 { 630 const std::optional<CPUExecutionMode> cpu = Settings::ParseCPUExecutionMode(argv[++i]); 631 if (!cpu.has_value()) 632 { 633 ERROR_LOG("Invalid CPU execution mode."); 634 return false; 635 } 636 637 INFO_LOG("Setting CPU execution mode to {}.", Settings::GetCPUExecutionModeName(cpu.value())); 638 s_base_settings_interface->SetStringValue("CPU", "ExecutionMode", 639 Settings::GetCPUExecutionModeName(cpu.value())); 640 continue; 641 } 642 else if (CHECK_ARG("-pgxp")) 643 { 644 INFO_LOG("Enabling PGXP."); 645 s_base_settings_interface->SetBoolValue("GPU", "PGXPEnable", true); 646 continue; 647 } 648 else if (CHECK_ARG("-pgxp-cpu")) 649 { 650 INFO_LOG("Enabling PGXP CPU mode."); 651 s_base_settings_interface->SetBoolValue("GPU", "PGXPEnable", true); 652 s_base_settings_interface->SetBoolValue("GPU", "PGXPCPU", true); 653 continue; 654 } 655 else if (CHECK_ARG("--")) 656 { 657 no_more_args = true; 658 continue; 659 } 660 else if (argv[i][0] == '-') 661 { 662 ERROR_LOG("Unknown parameter: '{}'", argv[i]); 663 return false; 664 } 665 666 #undef CHECK_ARG 667 #undef CHECK_ARG_PARAM 668 } 669 670 if (autoboot && !autoboot->filename.empty()) 671 autoboot->filename += ' '; 672 AutoBoot(autoboot)->filename += argv[i]; 673 } 674 675 return true; 676 } 677 678 bool RegTestHost::SetNewDataRoot(const std::string& filename) 679 { 680 Error error; 681 std::unique_ptr<CDImage> image = CDImage::Open(filename.c_str(), false, &error); 682 if (!image) 683 { 684 ERROR_LOG("Failed to open CD image '{}' to set data root: {}", Path::GetFileName(filename), error.GetDescription()); 685 return false; 686 } 687 688 const GameDatabase::Entry* dbentry = GameDatabase::GetEntryForDisc(image.get()); 689 std::string_view game_name; 690 if (dbentry) 691 { 692 game_name = dbentry->title; 693 INFO_LOG("Game name from database: {}", game_name); 694 } 695 else 696 { 697 game_name = Path::GetFileTitle(filename); 698 WARNING_LOG("Game not found in database, using filename: {}", game_name); 699 } 700 701 if (!s_dump_base_directory.empty()) 702 { 703 std::string dump_directory = Path::Combine(s_dump_base_directory, game_name); 704 if (!FileSystem::DirectoryExists(dump_directory.c_str())) 705 { 706 INFO_LOG("Creating directory '{}'...", dump_directory); 707 if (!FileSystem::CreateDirectory(dump_directory.c_str(), false)) 708 Panic("Failed to create dump directory."); 709 } 710 711 // Switch to file logging. 712 INFO_LOG("Dumping frames to '{}'...", dump_directory); 713 EmuFolders::DataRoot = std::move(dump_directory); 714 s_base_settings_interface->SetBoolValue("Logging", "LogToConsole", false); 715 s_base_settings_interface->SetBoolValue("Logging", "LogToFile", true); 716 s_base_settings_interface->SetStringValue("Logging", "LogLevel", Settings::GetLogLevelName(LOGLEVEL_DEV)); 717 System::ApplySettings(false); 718 } 719 720 return true; 721 } 722 723 std::string RegTestHost::GetFrameDumpFilename(u32 frame) 724 { 725 return Path::Combine(EmuFolders::DataRoot, fmt::format("frame_{:05d}.png", frame)); 726 } 727 728 int main(int argc, char* argv[]) 729 { 730 RegTestHost::InitializeEarlyConsole(); 731 732 if (!RegTestHost::InitializeConfig()) 733 return EXIT_FAILURE; 734 735 std::optional<SystemBootParameters> autoboot; 736 if (!RegTestHost::ParseCommandLineParameters(argc, argv, autoboot)) 737 return EXIT_FAILURE; 738 739 if (!autoboot || autoboot->filename.empty()) 740 { 741 ERROR_LOG("No boot path specified."); 742 return EXIT_FAILURE; 743 } 744 745 if (!RegTestHost::SetNewDataRoot(autoboot->filename)) 746 return EXIT_FAILURE; 747 748 { 749 Error startup_error; 750 if (!System::Internal::PerformEarlyHardwareChecks(&startup_error) || 751 !System::Internal::ProcessStartup(&startup_error) || !System::Internal::CPUThreadInitialize(&startup_error)) 752 { 753 ERROR_LOG("CPUThreadInitialize() failed: {}", startup_error.GetDescription()); 754 return EXIT_FAILURE; 755 } 756 } 757 758 RegTestHost::HookSignals(); 759 760 Error error; 761 int result = -1; 762 INFO_LOG("Trying to boot '{}'...", autoboot->filename); 763 if (!System::BootSystem(std::move(autoboot.value()), &error)) 764 { 765 ERROR_LOG("Failed to boot system: {}", error.GetDescription()); 766 goto cleanup; 767 } 768 769 if (s_frame_dump_interval > 0) 770 { 771 if (s_dump_base_directory.empty()) 772 { 773 ERROR_LOG("Dump directory not specified."); 774 goto cleanup; 775 } 776 777 INFO_LOG("Dumping every {}th frame to '{}'.", s_frame_dump_interval, s_dump_base_directory); 778 } 779 780 INFO_LOG("Running for {} frames...", s_frames_to_run); 781 s_frames_remaining = s_frames_to_run; 782 783 { 784 const Common::Timer::Value start_time = Common::Timer::GetCurrentValue(); 785 786 System::Execute(); 787 788 const Common::Timer::Value elapsed_time = Common::Timer::GetCurrentValue() - start_time; 789 const double elapsed_time_ms = Common::Timer::ConvertValueToMilliseconds(elapsed_time); 790 INFO_LOG("Total execution time: {:.2f}ms, average frame time {:.2f}ms, {:.2f} FPS", elapsed_time_ms, 791 elapsed_time_ms / static_cast<double>(s_frames_to_run), 792 static_cast<double>(s_frames_to_run) / elapsed_time_ms * 1000.0); 793 } 794 795 INFO_LOG("Exiting with success."); 796 result = 0; 797 798 cleanup: 799 System::Internal::CPUThreadShutdown(); 800 System::Internal::ProcessShutdown(); 801 return result; 802 }