Path: blob/master/src/duckstation-regtest/regtest_host.cpp
4242 views
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <[email protected]>1// SPDX-License-Identifier: CC-BY-NC-ND-4.023#include "core/achievements.h"4#include "core/bus.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/gpu_backend.h"10#include "core/gpu_presenter.h"11#include "core/gpu_thread.h"12#include "core/host.h"13#include "core/spu.h"14#include "core/system.h"15#include "core/system_private.h"1617#include "scmversion/scmversion.h"1819#include "util/cd_image.h"20#include "util/gpu_device.h"21#include "util/imgui_fullscreen.h"22#include "util/imgui_manager.h"23#include "util/input_manager.h"24#include "util/platform_misc.h"2526#include "common/assert.h"27#include "common/crash_handler.h"28#include "common/error.h"29#include "common/file_system.h"30#include "common/log.h"31#include "common/memory_settings_interface.h"32#include "common/path.h"33#include "common/sha256_digest.h"34#include "common/string_util.h"35#include "common/threading.h"36#include "common/timer.h"3738#include "fmt/format.h"3940#include <csignal>41#include <cstdio>42#include <ctime>4344LOG_CHANNEL(Host);4546namespace RegTestHost {4748static bool ParseCommandLineParameters(int argc, char* argv[], std::optional<SystemBootParameters>& autoboot);49static void PrintCommandLineVersion();50static void PrintCommandLineHelp(const char* progname);51static bool InitializeConfig();52static void InitializeEarlyConsole();53static void HookSignals();54static bool SetFolders();55static bool SetNewDataRoot(const std::string& filename);56static void DumpSystemStateHashes();57static std::string GetFrameDumpPath(u32 frame);58static void ProcessCPUThreadEvents();59static void GPUThreadEntryPoint();6061struct RegTestHostState62{63ALIGN_TO_CACHE_LINE std::mutex cpu_thread_events_mutex;64std::condition_variable cpu_thread_event_done;65std::deque<std::pair<std::function<void()>, bool>> cpu_thread_events;66u32 blocking_cpu_events_pending = 0;67};6869static RegTestHostState s_state;7071} // namespace RegTestHost7273static MemorySettingsInterface s_base_settings_interface;74static Threading::Thread s_gpu_thread;7576static u32 s_frames_to_run = 60 * 60;77static u32 s_frames_remaining = 0;78static u32 s_frame_dump_interval = 0;79static std::string s_dump_base_directory;8081bool RegTestHost::SetFolders()82{83std::string program_path(FileSystem::GetProgramPath());84DEV_LOG("Program Path: {}", program_path);8586EmuFolders::AppRoot = Path::Canonicalize(Path::GetDirectory(program_path));87EmuFolders::DataRoot = Host::Internal::ComputeDataDirectory();88EmuFolders::Resources = Path::Combine(EmuFolders::AppRoot, "resources");8990DEV_LOG("AppRoot Directory: {}", EmuFolders::AppRoot);91DEV_LOG("DataRoot Directory: {}", EmuFolders::DataRoot);92DEV_LOG("Resources Directory: {}", EmuFolders::Resources);9394// Write crash dumps to the data directory, since that'll be accessible for certain.95CrashHandler::SetWriteDirectory(EmuFolders::DataRoot);9697// the resources directory should exist, bail out if not98if (!FileSystem::DirectoryExists(EmuFolders::Resources.c_str()))99{100ERROR_LOG("Resources directory is missing, your installation is incomplete.");101return false;102}103104if (EmuFolders::DataRoot.empty() || !FileSystem::EnsureDirectoryExists(EmuFolders::DataRoot.c_str(), false))105{106ERROR_LOG("Failed to create data directory '{}'", EmuFolders::DataRoot);107return false;108}109110return true;111}112113bool RegTestHost::InitializeConfig()114{115SetFolders();116117Host::Internal::SetBaseSettingsLayer(&s_base_settings_interface);118119// default settings for runner120SettingsInterface& si = s_base_settings_interface;121g_settings.Load(si, si);122g_settings.Save(si, false);123si.SetStringValue("GPU", "Renderer", Settings::GetRendererName(GPURenderer::Software));124si.SetBoolValue("GPU", "DisableShaderCache", true);125si.SetStringValue("Pad1", "Type", Controller::GetControllerInfo(ControllerType::AnalogController).name);126si.SetStringValue("Pad2", "Type", Controller::GetControllerInfo(ControllerType::None).name);127si.SetStringValue("MemoryCards", "Card1Type", Settings::GetMemoryCardTypeName(MemoryCardType::NonPersistent));128si.SetStringValue("MemoryCards", "Card2Type", Settings::GetMemoryCardTypeName(MemoryCardType::None));129si.SetStringValue("ControllerPorts", "MultitapMode", Settings::GetMultitapModeName(MultitapMode::Disabled));130si.SetStringValue("Audio", "Backend", AudioStream::GetBackendName(AudioBackend::Null));131si.SetBoolValue("Logging", "LogToConsole", false);132si.SetBoolValue("Logging", "LogToFile", false);133si.SetStringValue("Logging", "LogLevel", Settings::GetLogLevelName(Log::Level::Info));134si.SetBoolValue("Main", "ApplyGameSettings", false); // don't want game settings interfering135si.SetBoolValue("BIOS", "PatchFastBoot", true); // no point validating the bios intro..136si.SetFloatValue("Main", "EmulationSpeed", 0.0f);137138// disable all sources139for (u32 i = 0; i < static_cast<u32>(InputSourceType::Count); i++)140si.SetBoolValue("InputSources", InputManager::InputSourceToString(static_cast<InputSourceType>(i)), false);141142EmuFolders::LoadConfig(s_base_settings_interface);143EmuFolders::EnsureFoldersExist();144145return true;146}147148void Host::ReportFatalError(std::string_view title, std::string_view message)149{150ERROR_LOG("ReportFatalError: {}", message);151abort();152}153154void Host::ReportErrorAsync(std::string_view title, std::string_view message)155{156if (!title.empty() && !message.empty())157ERROR_LOG("ReportErrorAsync: {}: {}", title, message);158else if (!message.empty())159ERROR_LOG("ReportErrorAsync: {}", message);160}161162bool Host::ConfirmMessage(std::string_view title, std::string_view message)163{164if (!title.empty() && !message.empty())165ERROR_LOG("ConfirmMessage: {}: {}", title, message);166else if (!message.empty())167ERROR_LOG("ConfirmMessage: {}", message);168169return true;170}171172void Host::ConfirmMessageAsync(std::string_view title, std::string_view message, ConfirmMessageAsyncCallback callback,173std::string_view yes_text, std::string_view no_text)174{175if (!title.empty() && !message.empty())176ERROR_LOG("ConfirmMessage: {}: {}", title, message);177else if (!message.empty())178ERROR_LOG("ConfirmMessage: {}", message);179180callback(true);181}182183void Host::ReportDebuggerMessage(std::string_view message)184{185ERROR_LOG("ReportDebuggerMessage: {}", message);186}187188std::span<const std::pair<const char*, const char*>> Host::GetAvailableLanguageList()189{190return {};191}192193const char* Host::GetLanguageName(std::string_view language_code)194{195return "";196}197198bool Host::ChangeLanguage(const char* new_language)199{200return false;201}202203s32 Host::Internal::GetTranslatedStringImpl(std::string_view context, std::string_view msg,204std::string_view disambiguation, char* tbuf, size_t tbuf_space)205{206if (msg.size() > tbuf_space)207return -1;208else if (msg.empty())209return 0;210211std::memcpy(tbuf, msg.data(), msg.size());212return static_cast<s32>(msg.size());213}214215std::string Host::TranslatePluralToString(const char* context, const char* msg, const char* disambiguation, int count)216{217TinyString count_str = TinyString::from_format("{}", count);218219std::string ret(msg);220for (;;)221{222std::string::size_type pos = ret.find("%n");223if (pos == std::string::npos)224break;225226ret.replace(pos, pos + 2, count_str.view());227}228229return ret;230}231232SmallString Host::TranslatePluralToSmallString(const char* context, const char* msg, const char* disambiguation,233int count)234{235SmallString ret(msg);236ret.replace("%n", TinyString::from_format("{}", count));237return ret;238}239240void Host::LoadSettings(const SettingsInterface& si, std::unique_lock<std::mutex>& lock)241{242}243244void Host::CheckForSettingsChanges(const Settings& old_settings)245{246}247248void Host::CommitBaseSettingChanges()249{250// noop, in memory251}252253bool Host::ResourceFileExists(std::string_view filename, bool allow_override)254{255const std::string path(Path::Combine(EmuFolders::Resources, filename));256return FileSystem::FileExists(path.c_str());257}258259std::optional<DynamicHeapArray<u8>> Host::ReadResourceFile(std::string_view filename, bool allow_override, Error* error)260{261const std::string path(Path::Combine(EmuFolders::Resources, filename));262return FileSystem::ReadBinaryFile(path.c_str(), error);263}264265std::optional<std::string> Host::ReadResourceFileToString(std::string_view filename, bool allow_override, Error* error)266{267const std::string path(Path::Combine(EmuFolders::Resources, filename));268return FileSystem::ReadFileToString(path.c_str(), error);269}270271std::optional<std::time_t> Host::GetResourceFileTimestamp(std::string_view filename, bool allow_override)272{273const std::string path(Path::Combine(EmuFolders::Resources, filename));274FILESYSTEM_STAT_DATA sd;275if (!FileSystem::StatFile(path.c_str(), &sd))276{277ERROR_LOG("Failed to stat resource file '{}'", filename);278return std::nullopt;279}280281return sd.ModificationTime;282}283284void Host::OnSystemStarting()285{286//287}288289void Host::OnSystemStarted()290{291//292}293294void Host::OnSystemStopping()295{296//297}298299void Host::OnSystemDestroyed()300{301//302}303304void Host::OnSystemPaused()305{306//307}308309void Host::OnSystemResumed()310{311//312}313314void Host::OnSystemAbnormalShutdown(const std::string_view reason)315{316// Already logged in core.317}318319void Host::OnGPUThreadRunIdleChanged(bool is_active)320{321//322}323324void Host::OnPerformanceCountersUpdated(const GPUBackend* gpu_backend)325{326//327}328329void Host::OnSystemGameChanged(const std::string& disc_path, const std::string& game_serial,330const std::string& game_name, GameHash hash)331{332INFO_LOG("Disc Path: {}", disc_path);333INFO_LOG("Game Serial: {}", game_serial);334INFO_LOG("Game Name: {}", game_name);335}336337void Host::OnSystemUndoStateAvailabilityChanged(bool available, u64 timestamp)338{339//340}341342void Host::OnMediaCaptureStarted()343{344//345}346347void Host::OnMediaCaptureStopped()348{349//350}351352void Host::PumpMessagesOnCPUThread()353{354RegTestHost::ProcessCPUThreadEvents();355356s_frames_remaining--;357if (s_frames_remaining == 0)358{359RegTestHost::DumpSystemStateHashes();360System::ShutdownSystem(false);361}362}363364void Host::RunOnCPUThread(std::function<void()> function, bool block /* = false */)365{366using namespace RegTestHost;367368std::unique_lock lock(s_state.cpu_thread_events_mutex);369s_state.cpu_thread_events.emplace_back(std::move(function), block);370s_state.blocking_cpu_events_pending += BoolToUInt32(block);371if (block)372s_state.cpu_thread_event_done.wait(lock, []() { return s_state.blocking_cpu_events_pending == 0; });373}374375void RegTestHost::ProcessCPUThreadEvents()376{377std::unique_lock lock(s_state.cpu_thread_events_mutex);378379for (;;)380{381if (s_state.cpu_thread_events.empty())382break;383384auto event = std::move(s_state.cpu_thread_events.front());385s_state.cpu_thread_events.pop_front();386lock.unlock();387event.first();388lock.lock();389390if (event.second)391{392s_state.blocking_cpu_events_pending--;393s_state.cpu_thread_event_done.notify_one();394}395}396}397398void Host::RunOnUIThread(std::function<void()> function, bool block /* = false */)399{400RunOnCPUThread(std::move(function), block);401}402403void Host::RequestResizeHostDisplay(s32 width, s32 height)404{405//406}407408void Host::RequestResetSettings(bool system, bool controller)409{410//411}412413void Host::RequestExitApplication(bool save_state_if_running)414{415//416}417418void Host::RequestExitBigPicture()419{420//421}422423void Host::RequestSystemShutdown(bool allow_confirm, bool save_state, bool check_memcard_busy)424{425//426}427428bool Host::IsFullscreen()429{430return false;431}432433void Host::SetFullscreen(bool enabled)434{435//436}437438std::optional<WindowInfo> Host::AcquireRenderWindow(RenderAPI render_api, bool fullscreen, bool exclusive_fullscreen,439Error* error)440{441return WindowInfo();442}443444void Host::ReleaseRenderWindow()445{446//447}448449void Host::BeginTextInput()450{451//452}453454void Host::EndTextInput()455{456//457}458459bool Host::CreateAuxiliaryRenderWindow(s32 x, s32 y, u32 width, u32 height, std::string_view title,460std::string_view icon_name, AuxiliaryRenderWindowUserData userdata,461AuxiliaryRenderWindowHandle* handle, WindowInfo* wi, Error* error)462{463return false;464}465466void Host::DestroyAuxiliaryRenderWindow(AuxiliaryRenderWindowHandle handle, s32* pos_x /* = nullptr */,467s32* pos_y /* = nullptr */, u32* width /* = nullptr */,468u32* height /* = nullptr */)469{470}471472void Host::FrameDoneOnGPUThread(GPUBackend* gpu_backend, u32 frame_number)473{474const GPUPresenter& presenter = gpu_backend->GetPresenter();475if (s_frame_dump_interval == 0 || (frame_number % s_frame_dump_interval) != 0 || !presenter.HasDisplayTexture())476return;477478// Need to take a copy of the display texture.479GPUTexture* const read_texture = presenter.GetDisplayTexture();480const u32 read_x = static_cast<u32>(presenter.GetDisplayTextureViewX());481const u32 read_y = static_cast<u32>(presenter.GetDisplayTextureViewY());482const u32 read_width = static_cast<u32>(presenter.GetDisplayTextureViewWidth());483const u32 read_height = static_cast<u32>(presenter.GetDisplayTextureViewHeight());484const ImageFormat read_format = GPUTexture::GetImageFormatForTextureFormat(read_texture->GetFormat());485if (read_format == ImageFormat::None)486return;487488Image image(read_width, read_height, read_format);489std::unique_ptr<GPUDownloadTexture> dltex;490if (g_gpu_device->GetFeatures().memory_import)491{492dltex = g_gpu_device->CreateDownloadTexture(read_width, read_height, read_texture->GetFormat(), image.GetPixels(),493image.GetStorageSize(), image.GetPitch());494}495if (!dltex)496{497if (!(dltex = g_gpu_device->CreateDownloadTexture(read_width, read_height, read_texture->GetFormat())))498{499ERROR_LOG("Failed to create {}x{} {} download texture", read_width, read_height,500GPUTexture::GetFormatName(read_texture->GetFormat()));501return;502}503}504505dltex->CopyFromTexture(0, 0, read_texture, read_x, read_y, read_width, read_height, 0, 0, !dltex->IsImported());506if (!dltex->ReadTexels(0, 0, read_width, read_height, image.GetPixels(), image.GetPitch()))507{508ERROR_LOG("Failed to read {}x{} download texture", read_width, read_height);509gpu_backend->RestoreDeviceContext();510return;511}512513// no more GPU calls514gpu_backend->RestoreDeviceContext();515516Error error;517const std::string path = RegTestHost::GetFrameDumpPath(frame_number);518auto fp = FileSystem::OpenManagedCFile(path.c_str(), "wb", &error);519if (!fp)520{521ERROR_LOG("Can't open file '{}': {}", Path::GetFileName(path), error.GetDescription());522return;523}524525System::QueueAsyncTask([path = std::move(path), fp = fp.release(), image = std::move(image)]() mutable {526Error error;527528if (image.GetFormat() != ImageFormat::RGBA8)529{530std::optional<Image> convert_image = image.ConvertToRGBA8(&error);531if (!convert_image.has_value())532{533ERROR_LOG("Failed to convert {} screenshot to RGBA8: {}", Image::GetFormatName(image.GetFormat()),534error.GetDescription());535image.Invalidate();536}537else538{539image = std::move(convert_image.value());540}541}542543bool result = false;544if (image.IsValid())545{546image.SetAllPixelsOpaque();547548result = image.SaveToFile(path.c_str(), fp, Image::DEFAULT_SAVE_QUALITY, &error);549if (!result)550ERROR_LOG("Failed to save screenshot to '{}': '{}'", Path::GetFileName(path), error.GetDescription());551}552553std::fclose(fp);554return result;555});556}557558void Host::OpenURL(std::string_view url)559{560//561}562563std::string Host::GetClipboardText()564{565return std::string();566}567568bool Host::CopyTextToClipboard(std::string_view text)569{570return false;571}572573std::string Host::FormatNumber(NumberFormatType type, s64 value)574{575std::string ret;576577if (type >= NumberFormatType::ShortDate && type <= NumberFormatType::LongDateTime)578{579const char* format;580switch (type)581{582case NumberFormatType::ShortDate:583format = "%x";584break;585586case NumberFormatType::LongDate:587format = "%A %B %e %Y";588break;589590case NumberFormatType::ShortTime:591case NumberFormatType::LongTime:592format = "%X";593break;594595case NumberFormatType::ShortDateTime:596format = "%X %x";597break;598599case NumberFormatType::LongDateTime:600format = "%c";601break;602603DefaultCaseIsUnreachable();604}605606struct tm ttime = {};607const std::time_t tvalue = static_cast<std::time_t>(value);608#ifdef _MSC_VER609localtime_s(&ttime, &tvalue);610#else611localtime_r(&tvalue, &ttime);612#endif613614char buf[128];615std::strftime(buf, std::size(buf), format, &ttime);616ret.assign(buf);617}618else619{620ret = fmt::format("{}", value);621}622623return ret;624}625626std::string Host::FormatNumber(NumberFormatType type, double value)627{628return fmt::format("{}", value);629}630631void Host::SetMouseMode(bool relative, bool hide_cursor)632{633//634}635636void Host::OnAchievementsLoginRequested(Achievements::LoginRequestReason reason)637{638// noop639}640641void Host::OnAchievementsLoginSuccess(const char* username, u32 points, u32 sc_points, u32 unread_messages)642{643// noop644}645646void Host::OnAchievementsRefreshed()647{648// noop649}650651void Host::OnAchievementsActiveChanged(bool active)652{653// noop654}655656void Host::OnAchievementsHardcoreModeChanged(bool enabled)657{658// noop659}660661void Host::OnAchievementsAllProgressRefreshed()662{663// noop664}665666#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION667668void Host::OnRAIntegrationMenuChanged()669{670// noop671}672673#endif674675const char* Host::GetDefaultFullscreenUITheme()676{677return "";678}679680bool Host::ShouldPreferHostFileSelector()681{682return false;683}684685void Host::OpenHostFileSelectorAsync(std::string_view title, bool select_directory, FileSelectorCallback callback,686FileSelectorFilters filters /* = FileSelectorFilters() */,687std::string_view initial_directory /* = std::string_view() */)688{689callback(std::string());690}691692void Host::AddFixedInputBindings(const SettingsInterface& si)693{694// noop695}696697void Host::OnInputDeviceConnected(InputBindingKey key, std::string_view identifier, std::string_view device_name)698{699// noop700}701702void Host::OnInputDeviceDisconnected(InputBindingKey key, std::string_view identifier)703{704// noop705}706707std::optional<WindowInfo> Host::GetTopLevelWindowInfo()708{709return std::nullopt;710}711712void Host::RefreshGameListAsync(bool invalidate_cache)713{714// noop715}716717void Host::CancelGameListRefresh()718{719// noop720}721722void Host::OnGameListEntriesChanged(std::span<const u32> changed_indices)723{724// noop725}726727BEGIN_HOTKEY_LIST(g_host_hotkeys)728END_HOTKEY_LIST()729730static void SignalHandler(int signal)731{732std::signal(signal, SIG_DFL);733734// MacOS is missing std::quick_exit() despite it being C++11...735#ifndef __APPLE__736std::quick_exit(1);737#else738_Exit(1);739#endif740}741742void RegTestHost::HookSignals()743{744std::signal(SIGINT, SignalHandler);745std::signal(SIGTERM, SignalHandler);746747#ifndef _WIN32748// Ignore SIGCHLD by default on Linux, since we kick off aplay asynchronously.749struct sigaction sa_chld = {};750sigemptyset(&sa_chld.sa_mask);751sa_chld.sa_handler = SIG_IGN;752sa_chld.sa_flags = SA_RESTART | SA_NOCLDSTOP | SA_NOCLDWAIT;753sigaction(SIGCHLD, &sa_chld, nullptr);754#endif755}756757void RegTestHost::GPUThreadEntryPoint()758{759Threading::SetNameOfCurrentThread("CPU Thread");760GPUThread::Internal::GPUThreadEntryPoint();761}762763void RegTestHost::DumpSystemStateHashes()764{765Error error;766767// don't save full state on gpu dump, it's not going to be complete...768if (!System::IsReplayingGPUDump())769{770DynamicHeapArray<u8> state_data(System::GetMaxSaveStateSize());771size_t state_data_size;772if (!System::SaveStateDataToBuffer(state_data, &state_data_size, &error))773{774ERROR_LOG("Failed to save system state: {}", error.GetDescription());775return;776}777778INFO_LOG("Save State Hash: {}",779SHA256Digest::DigestToString(SHA256Digest::GetDigest(state_data.cspan(0, state_data_size))));780INFO_LOG("RAM Hash: {}",781SHA256Digest::DigestToString(SHA256Digest::GetDigest(std::span<const u8>(Bus::g_ram, Bus::g_ram_size))));782INFO_LOG("SPU RAM Hash: {}", SHA256Digest::DigestToString(SHA256Digest::GetDigest(SPU::GetRAM())));783}784785INFO_LOG("VRAM Hash: {}", SHA256Digest::DigestToString(SHA256Digest::GetDigest(786std::span<const u8>(reinterpret_cast<const u8*>(g_vram), VRAM_SIZE))));787}788789void RegTestHost::InitializeEarlyConsole()790{791const bool was_console_enabled = Log::IsConsoleOutputEnabled();792if (!was_console_enabled)793{794Log::SetConsoleOutputParams(true);795Log::SetLogLevel(Log::Level::Info);796}797}798799void RegTestHost::PrintCommandLineVersion()800{801InitializeEarlyConsole();802std::fprintf(stderr, "DuckStation Regression Test Runner Version %s (%s)\n", g_scm_tag_str, g_scm_branch_str);803std::fprintf(stderr, "https://github.com/stenzek/duckstation\n");804std::fprintf(stderr, "\n");805}806807void RegTestHost::PrintCommandLineHelp(const char* progname)808{809InitializeEarlyConsole();810PrintCommandLineVersion();811std::fprintf(stderr, "Usage: %s [parameters] [--] [boot filename]\n", progname);812std::fprintf(stderr, "\n");813std::fprintf(stderr, " -help: Displays this information and exits.\n");814std::fprintf(stderr, " -version: Displays version information and exits.\n");815std::fprintf(stderr, " -dumpdir: Set frame dump base directory (will be dumped to basedir/gametitle).\n");816std::fprintf(stderr, " -dumpinterval: Dumps every N frames.\n");817std::fprintf(stderr, " -frames: Sets the number of frames to execute.\n");818std::fprintf(stderr, " -log <level>: Sets the log level. Defaults to verbose.\n");819std::fprintf(stderr, " -console: Enables console logging output.\n");820std::fprintf(stderr, " -pgxp: Enables PGXP.\n");821std::fprintf(stderr, " -pgxp-cpu: Forces PGXP CPU mode.\n");822std::fprintf(stderr, " -renderer <renderer>: Sets the graphics renderer. Default to software.\n");823std::fprintf(stderr, " -upscale <multiplier>: Enables upscaled rendering at the specified multiplier.\n");824std::fprintf(stderr, " --: Signals that no more arguments will follow and the remaining\n"825" parameters make up the filename. Use when the filename contains\n"826" spaces or starts with a dash.\n");827std::fprintf(stderr, "\n");828}829830static std::optional<SystemBootParameters>& AutoBoot(std::optional<SystemBootParameters>& autoboot)831{832if (!autoboot)833autoboot.emplace();834835return autoboot;836}837838bool RegTestHost::ParseCommandLineParameters(int argc, char* argv[], std::optional<SystemBootParameters>& autoboot)839{840bool no_more_args = false;841for (int i = 1; i < argc; i++)842{843if (!no_more_args)844{845#define CHECK_ARG(str) !std::strcmp(argv[i], str)846#define CHECK_ARG_PARAM(str) (!std::strcmp(argv[i], str) && ((i + 1) < argc))847848if (CHECK_ARG("-help"))849{850PrintCommandLineHelp(argv[0]);851return false;852}853else if (CHECK_ARG("-version"))854{855PrintCommandLineVersion();856return false;857}858else if (CHECK_ARG_PARAM("-dumpdir"))859{860s_dump_base_directory = argv[++i];861if (s_dump_base_directory.empty())862{863ERROR_LOG("Invalid dump directory specified.");864return false;865}866867continue;868}869else if (CHECK_ARG_PARAM("-dumpinterval"))870{871s_frame_dump_interval = StringUtil::FromChars<u32>(argv[++i]).value_or(0);872if (s_frame_dump_interval <= 0)873{874ERROR_LOG("Invalid dump interval specified: {}", argv[i]);875return false;876}877878continue;879}880else if (CHECK_ARG_PARAM("-frames"))881{882s_frames_to_run = StringUtil::FromChars<u32>(argv[++i]).value_or(0);883if (s_frames_to_run == 0)884{885ERROR_LOG("Invalid frame count specified: {}", argv[i]);886return false;887}888889continue;890}891else if (CHECK_ARG_PARAM("-log"))892{893std::optional<Log::Level> level = Settings::ParseLogLevelName(argv[++i]);894if (!level.has_value())895{896ERROR_LOG("Invalid log level specified.");897return false;898}899900Log::SetLogLevel(level.value());901s_base_settings_interface.SetStringValue("Logging", "LogLevel", Settings::GetLogLevelName(level.value()));902continue;903}904else if (CHECK_ARG("-console"))905{906Log::SetConsoleOutputParams(true);907s_base_settings_interface.SetBoolValue("Logging", "LogToConsole", true);908continue;909}910else if (CHECK_ARG_PARAM("-renderer"))911{912std::optional<GPURenderer> renderer = Settings::ParseRendererName(argv[++i]);913if (!renderer.has_value())914{915ERROR_LOG("Invalid renderer specified.");916return false;917}918919s_base_settings_interface.SetStringValue("GPU", "Renderer", Settings::GetRendererName(renderer.value()));920continue;921}922else if (CHECK_ARG_PARAM("-upscale"))923{924const u32 upscale = StringUtil::FromChars<u32>(argv[++i]).value_or(0);925if (upscale == 0)926{927ERROR_LOG("Invalid upscale value.");928return false;929}930931INFO_LOG("Setting upscale to {}.", upscale);932s_base_settings_interface.SetIntValue("GPU", "ResolutionScale", static_cast<s32>(upscale));933continue;934}935else if (CHECK_ARG_PARAM("-cpu"))936{937const std::optional<CPUExecutionMode> cpu = Settings::ParseCPUExecutionMode(argv[++i]);938if (!cpu.has_value())939{940ERROR_LOG("Invalid CPU execution mode.");941return false;942}943944INFO_LOG("Setting CPU execution mode to {}.", Settings::GetCPUExecutionModeName(cpu.value()));945s_base_settings_interface.SetStringValue("CPU", "ExecutionMode",946Settings::GetCPUExecutionModeName(cpu.value()));947continue;948}949else if (CHECK_ARG("-pgxp"))950{951INFO_LOG("Enabling PGXP.");952s_base_settings_interface.SetBoolValue("GPU", "PGXPEnable", true);953continue;954}955else if (CHECK_ARG("-pgxp-cpu"))956{957INFO_LOG("Enabling PGXP CPU mode.");958s_base_settings_interface.SetBoolValue("GPU", "PGXPEnable", true);959s_base_settings_interface.SetBoolValue("GPU", "PGXPCPU", true);960continue;961}962else if (CHECK_ARG("--"))963{964no_more_args = true;965continue;966}967else if (argv[i][0] == '-')968{969ERROR_LOG("Unknown parameter: '{}'", argv[i]);970return false;971}972973#undef CHECK_ARG974#undef CHECK_ARG_PARAM975}976977if (autoboot && !autoboot->path.empty())978autoboot->path += ' ';979AutoBoot(autoboot)->path += argv[i];980}981982return true;983}984985bool RegTestHost::SetNewDataRoot(const std::string& filename)986{987if (!s_dump_base_directory.empty())988{989std::string game_subdir = Path::SanitizeFileName(Path::GetFileTitle(filename));990INFO_LOG("Writing to subdirectory '{}'", game_subdir);991992std::string dump_directory = Path::Combine(s_dump_base_directory, game_subdir);993if (!FileSystem::DirectoryExists(dump_directory.c_str()))994{995INFO_LOG("Creating directory '{}'...", dump_directory);996if (!FileSystem::CreateDirectory(dump_directory.c_str(), false))997Panic("Failed to create dump directory.");998}9991000// Switch to file logging.1001INFO_LOG("Dumping frames to '{}'...", dump_directory);1002EmuFolders::DataRoot = std::move(dump_directory);1003s_base_settings_interface.SetBoolValue("Logging", "LogToFile", true);1004s_base_settings_interface.SetStringValue("Logging", "LogLevel", Settings::GetLogLevelName(Log::Level::Dev));1005Settings::UpdateLogConfig(s_base_settings_interface);1006}10071008return true;1009}10101011std::string RegTestHost::GetFrameDumpPath(u32 frame)1012{1013return Path::Combine(EmuFolders::DataRoot, fmt::format("frame_{:05d}.png", frame));1014}10151016int main(int argc, char* argv[])1017{1018CrashHandler::Install(&Bus::CleanupMemoryMap);10191020Error startup_error;1021if (!System::PerformEarlyHardwareChecks(&startup_error) || !System::ProcessStartup(&startup_error))1022{1023ERROR_LOG("CPUThreadInitialize() failed: {}", startup_error.GetDescription());1024return EXIT_FAILURE;1025}10261027RegTestHost::InitializeEarlyConsole();10281029if (!RegTestHost::InitializeConfig())1030return EXIT_FAILURE;10311032std::optional<SystemBootParameters> autoboot;1033if (!RegTestHost::ParseCommandLineParameters(argc, argv, autoboot))1034return EXIT_FAILURE;10351036if (!autoboot || autoboot->path.empty())1037{1038ERROR_LOG("No boot path specified.");1039return EXIT_FAILURE;1040}10411042if (!RegTestHost::SetNewDataRoot(autoboot->path))1043return EXIT_FAILURE;10441045// Only one async worker.1046if (!System::CPUThreadInitialize(&startup_error, 1))1047{1048ERROR_LOG("CPUThreadInitialize() failed: {}", startup_error.GetDescription());1049return EXIT_FAILURE;1050}10511052RegTestHost::HookSignals();1053s_gpu_thread.Start(&RegTestHost::GPUThreadEntryPoint);10541055Error error;1056int result = -1;1057INFO_LOG("Trying to boot '{}'...", autoboot->path);1058if (!System::BootSystem(std::move(autoboot.value()), &error))1059{1060ERROR_LOG("Failed to boot system: {}", error.GetDescription());1061goto cleanup;1062}10631064if (System::IsReplayingGPUDump() && !s_dump_base_directory.empty())1065{1066INFO_LOG("Replaying GPU dump, dumping all frames.");1067s_frame_dump_interval = 1;1068s_frames_to_run = static_cast<u32>(System::GetGPUDumpFrameCount());1069}10701071if (s_frame_dump_interval > 0)1072{1073if (s_dump_base_directory.empty())1074{1075ERROR_LOG("Dump directory not specified.");1076goto cleanup;1077}10781079INFO_LOG("Dumping every {}th frame to '{}'.", s_frame_dump_interval, s_dump_base_directory);1080}10811082INFO_LOG("Running for {} frames...", s_frames_to_run);1083s_frames_remaining = s_frames_to_run;10841085{1086const Timer::Value start_time = Timer::GetCurrentValue();10871088System::Execute();10891090const Timer::Value elapsed_time = Timer::GetCurrentValue() - start_time;1091const double elapsed_time_ms = Timer::ConvertValueToMilliseconds(elapsed_time);1092INFO_LOG("Total execution time: {:.2f}ms, average frame time {:.2f}ms, {:.2f} FPS", elapsed_time_ms,1093elapsed_time_ms / static_cast<double>(s_frames_to_run),1094static_cast<double>(s_frames_to_run) / elapsed_time_ms * 1000.0);1095}10961097INFO_LOG("Exiting with success.");1098result = 0;10991100cleanup:1101if (s_gpu_thread.Joinable())1102{1103GPUThread::Internal::RequestShutdown();1104s_gpu_thread.Join();1105}11061107RegTestHost::ProcessCPUThreadEvents();1108System::CPUThreadShutdown();1109System::ProcessShutdown();1110return result;1111}111211131114