duckstation

duckstation, but archived from the revision just before upstream changed it to a proprietary software project, this version is the libre one
git clone https://git.neptards.moe/u3shit/duckstation.git
Log | Files | Refs | README | LICENSE

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 }