Path: blob/master/src/duckstation-regtest/regtest_host.cpp
4802 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/time_helpers.h"37#include "common/timer.h"3839#include "fmt/format.h"4041#include <csignal>42#include <cstdio>43#include <ctime>4445LOG_CHANNEL(Host);4647namespace RegTestHost {4849static bool ParseCommandLineParameters(int argc, char* argv[], std::optional<SystemBootParameters>& autoboot);50static void PrintCommandLineVersion();51static void PrintCommandLineHelp(const char* progname);52static bool InitializeConfig();53static void InitializeEarlyConsole();54static void HookSignals();55static bool SetFolders();56static bool SetNewDataRoot(const std::string& filename);57static void DumpSystemStateHashes();58static std::string GetFrameDumpPath(u32 frame);59static void ProcessCPUThreadEvents();60static void GPUThreadEntryPoint();6162struct RegTestHostState63{64ALIGN_TO_CACHE_LINE std::mutex cpu_thread_events_mutex;65std::condition_variable cpu_thread_event_done;66std::deque<std::pair<std::function<void()>, bool>> cpu_thread_events;67u32 blocking_cpu_events_pending = 0;68};6970static RegTestHostState s_state;7172} // namespace RegTestHost7374static MemorySettingsInterface s_base_settings_interface;75static Threading::Thread s_gpu_thread;7677static u32 s_frames_to_run = 60 * 60;78static u32 s_frames_remaining = 0;79static u32 s_frame_dump_interval = 0;80static std::string s_dump_base_directory;8182bool RegTestHost::SetFolders()83{84std::string program_path(FileSystem::GetProgramPath());85DEV_LOG("Program Path: {}", program_path);8687EmuFolders::AppRoot = Path::Canonicalize(Path::GetDirectory(program_path));88EmuFolders::DataRoot = Host::Internal::ComputeDataDirectory();89EmuFolders::Resources = Path::Combine(EmuFolders::AppRoot, "resources");9091DEV_LOG("AppRoot Directory: {}", EmuFolders::AppRoot);92DEV_LOG("DataRoot Directory: {}", EmuFolders::DataRoot);93DEV_LOG("Resources Directory: {}", EmuFolders::Resources);9495// Write crash dumps to the data directory, since that'll be accessible for certain.96CrashHandler::SetWriteDirectory(EmuFolders::DataRoot);9798// the resources directory should exist, bail out if not99if (!FileSystem::DirectoryExists(EmuFolders::Resources.c_str()))100{101ERROR_LOG("Resources directory is missing, your installation is incomplete.");102return false;103}104105if (EmuFolders::DataRoot.empty() || !FileSystem::EnsureDirectoryExists(EmuFolders::DataRoot.c_str(), false))106{107ERROR_LOG("Failed to create data directory '{}'", EmuFolders::DataRoot);108return false;109}110111return true;112}113114bool RegTestHost::InitializeConfig()115{116SetFolders();117118Host::Internal::SetBaseSettingsLayer(&s_base_settings_interface);119120// default settings for runner121SettingsInterface& si = s_base_settings_interface;122g_settings.Load(si, si);123g_settings.Save(si, false);124si.SetStringValue("GPU", "Renderer", Settings::GetRendererName(GPURenderer::Software));125si.SetBoolValue("GPU", "DisableShaderCache", true);126si.SetStringValue("Pad1", "Type", Controller::GetControllerInfo(ControllerType::AnalogController).name);127si.SetStringValue("Pad2", "Type", Controller::GetControllerInfo(ControllerType::None).name);128si.SetStringValue("MemoryCards", "Card1Type", Settings::GetMemoryCardTypeName(MemoryCardType::NonPersistent));129si.SetStringValue("MemoryCards", "Card2Type", Settings::GetMemoryCardTypeName(MemoryCardType::None));130si.SetStringValue("ControllerPorts", "MultitapMode", Settings::GetMultitapModeName(MultitapMode::Disabled));131si.SetStringValue("Audio", "Backend", AudioStream::GetBackendName(AudioBackend::Null));132si.SetBoolValue("Logging", "LogToConsole", false);133si.SetBoolValue("Logging", "LogToFile", false);134si.SetStringValue("Logging", "LogLevel", Settings::GetLogLevelName(Log::Level::Info));135si.SetBoolValue("Main", "ApplyGameSettings", false); // don't want game settings interfering136si.SetBoolValue("BIOS", "PatchFastBoot", true); // no point validating the bios intro..137si.SetFloatValue("Main", "EmulationSpeed", 0.0f);138139// disable all sources140for (u32 i = 0; i < static_cast<u32>(InputSourceType::Count); i++)141si.SetBoolValue("InputSources", InputManager::InputSourceToString(static_cast<InputSourceType>(i)), false);142143EmuFolders::LoadConfig(s_base_settings_interface);144EmuFolders::EnsureFoldersExist();145146return true;147}148149void Host::ReportFatalError(std::string_view title, std::string_view message)150{151ERROR_LOG("ReportFatalError: {}", message);152abort();153}154155void Host::ReportErrorAsync(std::string_view title, std::string_view message)156{157if (!title.empty() && !message.empty())158ERROR_LOG("ReportErrorAsync: {}: {}", title, message);159else if (!message.empty())160ERROR_LOG("ReportErrorAsync: {}", message);161}162163bool Host::ConfirmMessage(std::string_view title, std::string_view message)164{165if (!title.empty() && !message.empty())166ERROR_LOG("ConfirmMessage: {}: {}", title, message);167else if (!message.empty())168ERROR_LOG("ConfirmMessage: {}", message);169170return true;171}172173void Host::ConfirmMessageAsync(std::string_view title, std::string_view message, ConfirmMessageAsyncCallback callback,174std::string_view yes_text, std::string_view no_text)175{176if (!title.empty() && !message.empty())177ERROR_LOG("ConfirmMessage: {}: {}", title, message);178else if (!message.empty())179ERROR_LOG("ConfirmMessage: {}", message);180181callback(true);182}183184void Host::ReportDebuggerMessage(std::string_view message)185{186ERROR_LOG("ReportDebuggerMessage: {}", message);187}188189std::span<const std::pair<const char*, const char*>> Host::GetAvailableLanguageList()190{191return {};192}193194const char* Host::GetLanguageName(std::string_view language_code)195{196return "";197}198199bool Host::ChangeLanguage(const char* new_language)200{201return false;202}203204s32 Host::Internal::GetTranslatedStringImpl(std::string_view context, std::string_view msg,205std::string_view disambiguation, char* tbuf, size_t tbuf_space)206{207if (msg.size() > tbuf_space)208return -1;209else if (msg.empty())210return 0;211212std::memcpy(tbuf, msg.data(), msg.size());213return static_cast<s32>(msg.size());214}215216std::string Host::TranslatePluralToString(const char* context, const char* msg, const char* disambiguation, int count)217{218TinyString count_str = TinyString::from_format("{}", count);219220std::string ret(msg);221for (;;)222{223std::string::size_type pos = ret.find("%n");224if (pos == std::string::npos)225break;226227ret.replace(pos, pos + 2, count_str.view());228}229230return ret;231}232233SmallString Host::TranslatePluralToSmallString(const char* context, const char* msg, const char* disambiguation,234int count)235{236SmallString ret(msg);237ret.replace("%n", TinyString::from_format("{}", count));238return ret;239}240241void Host::LoadSettings(const SettingsInterface& si, std::unique_lock<std::mutex>& lock)242{243}244245void Host::CheckForSettingsChanges(const Settings& old_settings)246{247}248249void Host::CommitBaseSettingChanges()250{251// noop, in memory252}253254bool Host::ResourceFileExists(std::string_view filename, bool allow_override)255{256const std::string path(Path::Combine(EmuFolders::Resources, filename));257return FileSystem::FileExists(path.c_str());258}259260std::optional<DynamicHeapArray<u8>> Host::ReadResourceFile(std::string_view filename, bool allow_override, Error* error)261{262const std::string path(Path::Combine(EmuFolders::Resources, filename));263return FileSystem::ReadBinaryFile(path.c_str(), error);264}265266std::optional<std::string> Host::ReadResourceFileToString(std::string_view filename, bool allow_override, Error* error)267{268const std::string path(Path::Combine(EmuFolders::Resources, filename));269return FileSystem::ReadFileToString(path.c_str(), error);270}271272std::optional<std::time_t> Host::GetResourceFileTimestamp(std::string_view filename, bool allow_override)273{274const std::string path(Path::Combine(EmuFolders::Resources, filename));275FILESYSTEM_STAT_DATA sd;276if (!FileSystem::StatFile(path.c_str(), &sd))277{278ERROR_LOG("Failed to stat resource file '{}'", filename);279return std::nullopt;280}281282return sd.ModificationTime;283}284285void Host::OnSystemStarting()286{287//288}289290void Host::OnSystemStarted()291{292//293}294295void Host::OnSystemStopping()296{297//298}299300void Host::OnSystemDestroyed()301{302//303}304305void Host::OnSystemPaused()306{307//308}309310void Host::OnSystemResumed()311{312//313}314315void Host::OnSystemAbnormalShutdown(const std::string_view reason)316{317// Already logged in core.318}319320void Host::OnGPUThreadRunIdleChanged(bool is_active)321{322//323}324325void Host::OnPerformanceCountersUpdated(const GPUBackend* gpu_backend)326{327//328}329330void Host::OnSystemGameChanged(const std::string& disc_path, const std::string& game_serial,331const std::string& game_name, GameHash hash)332{333INFO_LOG("Disc Path: {}", disc_path);334INFO_LOG("Game Serial: {}", game_serial);335INFO_LOG("Game Name: {}", game_name);336}337338void Host::OnSystemUndoStateAvailabilityChanged(bool available, u64 timestamp)339{340//341}342343void Host::OnMediaCaptureStarted()344{345//346}347348void Host::OnMediaCaptureStopped()349{350//351}352353void Host::PumpMessagesOnCPUThread()354{355RegTestHost::ProcessCPUThreadEvents();356357s_frames_remaining--;358if (s_frames_remaining == 0)359{360RegTestHost::DumpSystemStateHashes();361System::ShutdownSystem(false);362}363}364365void Host::RunOnCPUThread(std::function<void()> function, bool block /* = false */)366{367using namespace RegTestHost;368369std::unique_lock lock(s_state.cpu_thread_events_mutex);370s_state.cpu_thread_events.emplace_back(std::move(function), block);371s_state.blocking_cpu_events_pending += BoolToUInt32(block);372if (block)373s_state.cpu_thread_event_done.wait(lock, []() { return s_state.blocking_cpu_events_pending == 0; });374}375376void RegTestHost::ProcessCPUThreadEvents()377{378std::unique_lock lock(s_state.cpu_thread_events_mutex);379380for (;;)381{382if (s_state.cpu_thread_events.empty())383break;384385auto event = std::move(s_state.cpu_thread_events.front());386s_state.cpu_thread_events.pop_front();387lock.unlock();388event.first();389lock.lock();390391if (event.second)392{393s_state.blocking_cpu_events_pending--;394s_state.cpu_thread_event_done.notify_one();395}396}397}398399void Host::RunOnUIThread(std::function<void()> function, bool block /* = false */)400{401RunOnCPUThread(std::move(function), block);402}403404void Host::RequestResizeHostDisplay(s32 width, s32 height)405{406//407}408409void Host::RequestResetSettings(bool system, bool controller)410{411//412}413414void Host::RequestExitApplication(bool save_state_if_running)415{416//417}418419void Host::RequestExitBigPicture()420{421//422}423424void Host::RequestSystemShutdown(bool allow_confirm, bool save_state, bool check_memcard_busy)425{426//427}428429bool Host::IsFullscreen()430{431return false;432}433434void Host::SetFullscreen(bool enabled)435{436//437}438439std::optional<WindowInfo> Host::AcquireRenderWindow(RenderAPI render_api, bool fullscreen, bool exclusive_fullscreen,440Error* error)441{442return WindowInfo();443}444445void Host::ReleaseRenderWindow()446{447//448}449450void Host::BeginTextInput()451{452//453}454455void Host::EndTextInput()456{457//458}459460bool Host::CreateAuxiliaryRenderWindow(s32 x, s32 y, u32 width, u32 height, std::string_view title,461std::string_view icon_name, AuxiliaryRenderWindowUserData userdata,462AuxiliaryRenderWindowHandle* handle, WindowInfo* wi, Error* error)463{464return false;465}466467void Host::DestroyAuxiliaryRenderWindow(AuxiliaryRenderWindowHandle handle, s32* pos_x /* = nullptr */,468s32* pos_y /* = nullptr */, u32* width /* = nullptr */,469u32* height /* = nullptr */)470{471}472473void Host::FrameDoneOnGPUThread(GPUBackend* gpu_backend, u32 frame_number)474{475const GPUPresenter& presenter = gpu_backend->GetPresenter();476if (s_frame_dump_interval == 0 || (frame_number % s_frame_dump_interval) != 0 || !presenter.HasDisplayTexture())477return;478479// Need to take a copy of the display texture.480GPUTexture* const read_texture = presenter.GetDisplayTexture();481const u32 read_x = static_cast<u32>(presenter.GetDisplayTextureViewX());482const u32 read_y = static_cast<u32>(presenter.GetDisplayTextureViewY());483const u32 read_width = static_cast<u32>(presenter.GetDisplayTextureViewWidth());484const u32 read_height = static_cast<u32>(presenter.GetDisplayTextureViewHeight());485const ImageFormat read_format = GPUTexture::GetImageFormatForTextureFormat(read_texture->GetFormat());486if (read_format == ImageFormat::None)487return;488489Image image(read_width, read_height, read_format);490std::unique_ptr<GPUDownloadTexture> dltex;491if (g_gpu_device->GetFeatures().memory_import)492{493dltex = g_gpu_device->CreateDownloadTexture(read_width, read_height, read_texture->GetFormat(), image.GetPixels(),494image.GetStorageSize(), image.GetPitch());495}496if (!dltex)497{498if (!(dltex = g_gpu_device->CreateDownloadTexture(read_width, read_height, read_texture->GetFormat())))499{500ERROR_LOG("Failed to create {}x{} {} download texture", read_width, read_height,501GPUTexture::GetFormatName(read_texture->GetFormat()));502return;503}504}505506dltex->CopyFromTexture(0, 0, read_texture, read_x, read_y, read_width, read_height, 0, 0, !dltex->IsImported());507if (!dltex->ReadTexels(0, 0, read_width, read_height, image.GetPixels(), image.GetPitch()))508{509ERROR_LOG("Failed to read {}x{} download texture", read_width, read_height);510gpu_backend->RestoreDeviceContext();511return;512}513514// no more GPU calls515gpu_backend->RestoreDeviceContext();516517Error error;518const std::string path = RegTestHost::GetFrameDumpPath(frame_number);519auto fp = FileSystem::OpenManagedCFile(path.c_str(), "wb", &error);520if (!fp)521{522ERROR_LOG("Can't open file '{}': {}", Path::GetFileName(path), error.GetDescription());523return;524}525526System::QueueAsyncTask([path = std::move(path), fp = fp.release(), image = std::move(image)]() mutable {527Error error;528529if (image.GetFormat() != ImageFormat::RGBA8)530{531std::optional<Image> convert_image = image.ConvertToRGBA8(&error);532if (!convert_image.has_value())533{534ERROR_LOG("Failed to convert {} screenshot to RGBA8: {}", Image::GetFormatName(image.GetFormat()),535error.GetDescription());536image.Invalidate();537}538else539{540image = std::move(convert_image.value());541}542}543544bool result = false;545if (image.IsValid())546{547image.SetAllPixelsOpaque();548549result = image.SaveToFile(path.c_str(), fp, Image::DEFAULT_SAVE_QUALITY, &error);550if (!result)551ERROR_LOG("Failed to save screenshot to '{}': '{}'", Path::GetFileName(path), error.GetDescription());552}553554std::fclose(fp);555return result;556});557}558559void Host::OpenURL(std::string_view url)560{561//562}563564std::string Host::GetClipboardText()565{566return std::string();567}568569bool Host::CopyTextToClipboard(std::string_view text)570{571return false;572}573574std::string Host::FormatNumber(NumberFormatType type, s64 value)575{576std::string ret;577578if (type >= NumberFormatType::ShortDate && type <= NumberFormatType::LongDateTime)579{580const char* format;581switch (type)582{583case NumberFormatType::ShortDate:584format = "%x";585break;586587case NumberFormatType::LongDate:588format = "%A %B %e %Y";589break;590591case NumberFormatType::ShortTime:592case NumberFormatType::LongTime:593format = "%X";594break;595596case NumberFormatType::ShortDateTime:597format = "%X %x";598break;599600case NumberFormatType::LongDateTime:601format = "%c";602break;603604DefaultCaseIsUnreachable();605}606607ret.resize(128);608609if (const std::optional<std::tm> ltime = Common::LocalTime(static_cast<std::time_t>(value)))610ret.resize(std::strftime(ret.data(), ret.size(), format, <ime.value()));611else612ret = "Invalid";613}614else615{616ret = fmt::format("{}", value);617}618619return ret;620}621622std::string Host::FormatNumber(NumberFormatType type, double value)623{624return fmt::format("{}", value);625}626627void Host::SetMouseMode(bool relative, bool hide_cursor)628{629//630}631632void Host::OnAchievementsLoginRequested(Achievements::LoginRequestReason reason)633{634// noop635}636637void Host::OnAchievementsLoginSuccess(const char* username, u32 points, u32 sc_points, u32 unread_messages)638{639// noop640}641642void Host::OnAchievementsRefreshed()643{644// noop645}646647void Host::OnAchievementsActiveChanged(bool active)648{649// noop650}651652void Host::OnAchievementsHardcoreModeChanged(bool enabled)653{654// noop655}656657void Host::OnAchievementsAllProgressRefreshed()658{659// noop660}661662#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION663664void Host::OnRAIntegrationMenuChanged()665{666// noop667}668669#endif670671const char* Host::GetDefaultFullscreenUITheme()672{673return "";674}675676bool Host::ShouldPreferHostFileSelector()677{678return false;679}680681void Host::OpenHostFileSelectorAsync(std::string_view title, bool select_directory, FileSelectorCallback callback,682FileSelectorFilters filters /* = FileSelectorFilters() */,683std::string_view initial_directory /* = std::string_view() */)684{685callback(std::string());686}687688void Host::AddFixedInputBindings(const SettingsInterface& si)689{690// noop691}692693void Host::OnInputDeviceConnected(InputBindingKey key, std::string_view identifier, std::string_view device_name)694{695// noop696}697698void Host::OnInputDeviceDisconnected(InputBindingKey key, std::string_view identifier)699{700// noop701}702703std::optional<WindowInfo> Host::GetTopLevelWindowInfo()704{705return std::nullopt;706}707708void Host::RefreshGameListAsync(bool invalidate_cache)709{710// noop711}712713void Host::CancelGameListRefresh()714{715// noop716}717718void Host::OnGameListEntriesChanged(std::span<const u32> changed_indices)719{720// noop721}722723BEGIN_HOTKEY_LIST(g_host_hotkeys)724END_HOTKEY_LIST()725726static void SignalHandler(int signal)727{728std::signal(signal, SIG_DFL);729730// MacOS is missing std::quick_exit() despite it being C++11...731#ifndef __APPLE__732std::quick_exit(1);733#else734_Exit(1);735#endif736}737738void RegTestHost::HookSignals()739{740std::signal(SIGINT, SignalHandler);741std::signal(SIGTERM, SignalHandler);742743#ifndef _WIN32744// Ignore SIGCHLD by default on Linux, since we kick off aplay asynchronously.745struct sigaction sa_chld = {};746sigemptyset(&sa_chld.sa_mask);747sa_chld.sa_handler = SIG_IGN;748sa_chld.sa_flags = SA_RESTART | SA_NOCLDSTOP | SA_NOCLDWAIT;749sigaction(SIGCHLD, &sa_chld, nullptr);750#endif751}752753void RegTestHost::GPUThreadEntryPoint()754{755Threading::SetNameOfCurrentThread("CPU Thread");756GPUThread::Internal::GPUThreadEntryPoint();757}758759void RegTestHost::DumpSystemStateHashes()760{761Error error;762763// don't save full state on gpu dump, it's not going to be complete...764if (!System::IsReplayingGPUDump())765{766DynamicHeapArray<u8> state_data(System::GetMaxSaveStateSize());767size_t state_data_size;768if (!System::SaveStateDataToBuffer(state_data, &state_data_size, &error))769{770ERROR_LOG("Failed to save system state: {}", error.GetDescription());771return;772}773774INFO_LOG("Save State Hash: {}",775SHA256Digest::DigestToString(SHA256Digest::GetDigest(state_data.cspan(0, state_data_size))));776INFO_LOG("RAM Hash: {}",777SHA256Digest::DigestToString(SHA256Digest::GetDigest(std::span<const u8>(Bus::g_ram, Bus::g_ram_size))));778INFO_LOG("SPU RAM Hash: {}", SHA256Digest::DigestToString(SHA256Digest::GetDigest(SPU::GetRAM())));779}780781INFO_LOG("VRAM Hash: {}", SHA256Digest::DigestToString(SHA256Digest::GetDigest(782std::span<const u8>(reinterpret_cast<const u8*>(g_vram), VRAM_SIZE))));783}784785void RegTestHost::InitializeEarlyConsole()786{787const bool was_console_enabled = Log::IsConsoleOutputEnabled();788if (!was_console_enabled)789{790Log::SetConsoleOutputParams(true);791Log::SetLogLevel(Log::Level::Info);792}793}794795void RegTestHost::PrintCommandLineVersion()796{797InitializeEarlyConsole();798std::fprintf(stderr, "DuckStation Regression Test Runner Version %s (%s)\n", g_scm_tag_str, g_scm_branch_str);799std::fprintf(stderr, "https://github.com/stenzek/duckstation\n");800std::fprintf(stderr, "\n");801}802803void RegTestHost::PrintCommandLineHelp(const char* progname)804{805InitializeEarlyConsole();806PrintCommandLineVersion();807std::fprintf(stderr, "Usage: %s [parameters] [--] [boot filename]\n", progname);808std::fprintf(stderr, "\n");809std::fprintf(stderr, " -help: Displays this information and exits.\n");810std::fprintf(stderr, " -version: Displays version information and exits.\n");811std::fprintf(stderr, " -dumpdir: Set frame dump base directory (will be dumped to basedir/gametitle).\n");812std::fprintf(stderr, " -dumpinterval: Dumps every N frames.\n");813std::fprintf(stderr, " -frames: Sets the number of frames to execute.\n");814std::fprintf(stderr, " -log <level>: Sets the log level. Defaults to verbose.\n");815std::fprintf(stderr, " -console: Enables console logging output.\n");816std::fprintf(stderr, " -pgxp: Enables PGXP.\n");817std::fprintf(stderr, " -pgxp-cpu: Forces PGXP CPU mode.\n");818std::fprintf(stderr, " -renderer <renderer>: Sets the graphics renderer. Default to software.\n");819std::fprintf(stderr, " -upscale <multiplier>: Enables upscaled rendering at the specified multiplier.\n");820std::fprintf(stderr, " --: Signals that no more arguments will follow and the remaining\n"821" parameters make up the filename. Use when the filename contains\n"822" spaces or starts with a dash.\n");823std::fprintf(stderr, "\n");824}825826static std::optional<SystemBootParameters>& AutoBoot(std::optional<SystemBootParameters>& autoboot)827{828if (!autoboot)829autoboot.emplace();830831return autoboot;832}833834bool RegTestHost::ParseCommandLineParameters(int argc, char* argv[], std::optional<SystemBootParameters>& autoboot)835{836bool no_more_args = false;837for (int i = 1; i < argc; i++)838{839if (!no_more_args)840{841#define CHECK_ARG(str) !std::strcmp(argv[i], str)842#define CHECK_ARG_PARAM(str) (!std::strcmp(argv[i], str) && ((i + 1) < argc))843844if (CHECK_ARG("-help"))845{846PrintCommandLineHelp(argv[0]);847return false;848}849else if (CHECK_ARG("-version"))850{851PrintCommandLineVersion();852return false;853}854else if (CHECK_ARG_PARAM("-dumpdir"))855{856s_dump_base_directory = argv[++i];857if (s_dump_base_directory.empty())858{859ERROR_LOG("Invalid dump directory specified.");860return false;861}862863continue;864}865else if (CHECK_ARG_PARAM("-dumpinterval"))866{867s_frame_dump_interval = StringUtil::FromChars<u32>(argv[++i]).value_or(0);868if (s_frame_dump_interval <= 0)869{870ERROR_LOG("Invalid dump interval specified: {}", argv[i]);871return false;872}873874continue;875}876else if (CHECK_ARG_PARAM("-frames"))877{878s_frames_to_run = StringUtil::FromChars<u32>(argv[++i]).value_or(0);879if (s_frames_to_run == 0)880{881ERROR_LOG("Invalid frame count specified: {}", argv[i]);882return false;883}884885continue;886}887else if (CHECK_ARG_PARAM("-log"))888{889std::optional<Log::Level> level = Settings::ParseLogLevelName(argv[++i]);890if (!level.has_value())891{892ERROR_LOG("Invalid log level specified.");893return false;894}895896Log::SetLogLevel(level.value());897s_base_settings_interface.SetStringValue("Logging", "LogLevel", Settings::GetLogLevelName(level.value()));898continue;899}900else if (CHECK_ARG("-console"))901{902Log::SetConsoleOutputParams(true);903s_base_settings_interface.SetBoolValue("Logging", "LogToConsole", true);904continue;905}906else if (CHECK_ARG_PARAM("-renderer"))907{908std::optional<GPURenderer> renderer = Settings::ParseRendererName(argv[++i]);909if (!renderer.has_value())910{911ERROR_LOG("Invalid renderer specified.");912return false;913}914915s_base_settings_interface.SetStringValue("GPU", "Renderer", Settings::GetRendererName(renderer.value()));916continue;917}918else if (CHECK_ARG_PARAM("-upscale"))919{920const u32 upscale = StringUtil::FromChars<u32>(argv[++i]).value_or(0);921if (upscale == 0)922{923ERROR_LOG("Invalid upscale value.");924return false;925}926927INFO_LOG("Setting upscale to {}.", upscale);928s_base_settings_interface.SetIntValue("GPU", "ResolutionScale", static_cast<s32>(upscale));929continue;930}931else if (CHECK_ARG_PARAM("-cpu"))932{933const std::optional<CPUExecutionMode> cpu = Settings::ParseCPUExecutionMode(argv[++i]);934if (!cpu.has_value())935{936ERROR_LOG("Invalid CPU execution mode.");937return false;938}939940INFO_LOG("Setting CPU execution mode to {}.", Settings::GetCPUExecutionModeName(cpu.value()));941s_base_settings_interface.SetStringValue("CPU", "ExecutionMode",942Settings::GetCPUExecutionModeName(cpu.value()));943continue;944}945else if (CHECK_ARG("-pgxp"))946{947INFO_LOG("Enabling PGXP.");948s_base_settings_interface.SetBoolValue("GPU", "PGXPEnable", true);949continue;950}951else if (CHECK_ARG("-pgxp-cpu"))952{953INFO_LOG("Enabling PGXP CPU mode.");954s_base_settings_interface.SetBoolValue("GPU", "PGXPEnable", true);955s_base_settings_interface.SetBoolValue("GPU", "PGXPCPU", true);956continue;957}958else if (CHECK_ARG("--"))959{960no_more_args = true;961continue;962}963else if (argv[i][0] == '-')964{965ERROR_LOG("Unknown parameter: '{}'", argv[i]);966return false;967}968969#undef CHECK_ARG970#undef CHECK_ARG_PARAM971}972973if (autoboot && !autoboot->path.empty())974autoboot->path += ' ';975AutoBoot(autoboot)->path += argv[i];976}977978return true;979}980981bool RegTestHost::SetNewDataRoot(const std::string& filename)982{983if (!s_dump_base_directory.empty())984{985std::string game_subdir = Path::SanitizeFileName(Path::GetFileTitle(filename));986INFO_LOG("Writing to subdirectory '{}'", game_subdir);987988std::string dump_directory = Path::Combine(s_dump_base_directory, game_subdir);989if (!FileSystem::DirectoryExists(dump_directory.c_str()))990{991INFO_LOG("Creating directory '{}'...", dump_directory);992if (!FileSystem::CreateDirectory(dump_directory.c_str(), false))993Panic("Failed to create dump directory.");994}995996// Switch to file logging.997INFO_LOG("Dumping frames to '{}'...", dump_directory);998EmuFolders::DataRoot = std::move(dump_directory);999s_base_settings_interface.SetBoolValue("Logging", "LogToFile", true);1000s_base_settings_interface.SetStringValue("Logging", "LogLevel", Settings::GetLogLevelName(Log::Level::Dev));1001Settings::UpdateLogConfig(s_base_settings_interface);1002}10031004return true;1005}10061007std::string RegTestHost::GetFrameDumpPath(u32 frame)1008{1009return Path::Combine(EmuFolders::DataRoot, fmt::format("frame_{:05d}.png", frame));1010}10111012int main(int argc, char* argv[])1013{1014CrashHandler::Install(&Bus::CleanupMemoryMap);10151016Error startup_error;1017if (!System::PerformEarlyHardwareChecks(&startup_error) || !System::ProcessStartup(&startup_error))1018{1019ERROR_LOG("CPUThreadInitialize() failed: {}", startup_error.GetDescription());1020return EXIT_FAILURE;1021}10221023RegTestHost::InitializeEarlyConsole();10241025if (!RegTestHost::InitializeConfig())1026return EXIT_FAILURE;10271028std::optional<SystemBootParameters> autoboot;1029if (!RegTestHost::ParseCommandLineParameters(argc, argv, autoboot))1030return EXIT_FAILURE;10311032if (!autoboot || autoboot->path.empty())1033{1034ERROR_LOG("No boot path specified.");1035return EXIT_FAILURE;1036}10371038if (!RegTestHost::SetNewDataRoot(autoboot->path))1039return EXIT_FAILURE;10401041// Only one async worker.1042if (!System::CPUThreadInitialize(&startup_error, 1))1043{1044ERROR_LOG("CPUThreadInitialize() failed: {}", startup_error.GetDescription());1045return EXIT_FAILURE;1046}10471048RegTestHost::HookSignals();1049s_gpu_thread.Start(&RegTestHost::GPUThreadEntryPoint);10501051Error error;1052int result = -1;1053INFO_LOG("Trying to boot '{}'...", autoboot->path);1054if (!System::BootSystem(std::move(autoboot.value()), &error))1055{1056ERROR_LOG("Failed to boot system: {}", error.GetDescription());1057goto cleanup;1058}10591060if (System::IsReplayingGPUDump() && !s_dump_base_directory.empty())1061{1062INFO_LOG("Replaying GPU dump, dumping all frames.");1063s_frame_dump_interval = 1;1064s_frames_to_run = static_cast<u32>(System::GetGPUDumpFrameCount());1065}10661067if (s_frame_dump_interval > 0)1068{1069if (s_dump_base_directory.empty())1070{1071ERROR_LOG("Dump directory not specified.");1072goto cleanup;1073}10741075INFO_LOG("Dumping every {}th frame to '{}'.", s_frame_dump_interval, s_dump_base_directory);1076}10771078INFO_LOG("Running for {} frames...", s_frames_to_run);1079s_frames_remaining = s_frames_to_run;10801081{1082const Timer::Value start_time = Timer::GetCurrentValue();10831084System::Execute();10851086const Timer::Value elapsed_time = Timer::GetCurrentValue() - start_time;1087const double elapsed_time_ms = Timer::ConvertValueToMilliseconds(elapsed_time);1088INFO_LOG("Total execution time: {:.2f}ms, average frame time {:.2f}ms, {:.2f} FPS", elapsed_time_ms,1089elapsed_time_ms / static_cast<double>(s_frames_to_run),1090static_cast<double>(s_frames_to_run) / elapsed_time_ms * 1000.0);1091}10921093INFO_LOG("Exiting with success.");1094result = 0;10951096cleanup:1097if (s_gpu_thread.Joinable())1098{1099GPUThread::Internal::RequestShutdown();1100s_gpu_thread.Join();1101}11021103RegTestHost::ProcessCPUThreadEvents();1104System::CPUThreadShutdown();1105System::ProcessShutdown();1106return result;1107}110811091110