Path: blob/master/src/duckstation-regtest/regtest_host.cpp
7408 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/core_private.h"7#include "core/cpu_core.h"8#include "core/fullscreenui.h"9#include "core/fullscreenui_widgets.h"10#include "core/game_list.h"11#include "core/gpu.h"12#include "core/gpu_backend.h"13#include "core/host.h"14#include "core/spu.h"15#include "core/system.h"16#include "core/system_private.h"17#include "core/video_presenter.h"18#include "core/video_thread.h"1920#include "scmversion/scmversion.h"2122#include "util/cd_image.h"23#include "util/gpu_device.h"24#include "util/imgui_manager.h"25#include "util/input_manager.h"26#include "util/translation.h"2728#include "common/assert.h"29#include "common/crash_handler.h"30#include "common/error.h"31#include "common/file_system.h"32#include "common/log.h"33#include "common/path.h"34#include "common/sha256_digest.h"35#include "common/string_util.h"36#include "common/task_queue.h"37#include "common/threading.h"38#include "common/time_helpers.h"39#include "common/timer.h"4041#include "fmt/format.h"4243#include <csignal>44#include <cstdio>45#include <ctime>4647LOG_CHANNEL(Host);4849namespace RegTestHost {5051static bool ParseCommandLineParameters(int argc, char* argv[], std::optional<SystemBootParameters>& autoboot);52static void PrintCommandLineVersion();53static void PrintCommandLineHelp(const char* progname);54static bool InitializeFoldersAndConfig(Error* error);55static void InitializeEarlyConsole();56static void HookSignals();57static bool SetNewDataRoot(const std::string& filename);58static void DumpSystemStateHashes();59static std::string GetFrameDumpPath(u32 frame);60static void ProcessCoreThreadEvents();61static void VideoThreadEntryPoint();6263struct RegTestHostState64{65ALIGN_TO_CACHE_LINE std::mutex core_thread_events_mutex;66std::condition_variable core_thread_event_done;67std::deque<std::pair<std::function<void()>, bool>> cpu_thread_events;68u32 blocking_cpu_events_pending = 0;69};7071static RegTestHostState s_state;72ALIGN_TO_CACHE_LINE static TaskQueue s_async_task_queue;7374} // namespace RegTestHost7576static Threading::Thread s_video_thread;7778static u32 s_frames_to_run = 60 * 60;79static u32 s_frames_remaining = 0;80static u32 s_frame_dump_interval = 0;81static std::string s_dump_base_directory;8283bool RegTestHost::InitializeFoldersAndConfig(Error* error)84{85if (!Core::SetCriticalFolders("resources", error))86return false;8788if (!Core::InitializeBaseSettingsLayer({}, error))89return false;9091// default settings for runner92const auto lock = Core::GetSettingsLock();93SettingsInterface& si = *Core::GetBaseSettingsLayer();94si.SetStringValue("GPU", "Renderer", Settings::GetRendererName(GPURenderer::Software));95si.SetBoolValue("GPU", "DisableShaderCache", true);96si.SetStringValue("Pad1", "Type", Controller::GetControllerInfo(ControllerType::AnalogController).name);97si.SetStringValue("Pad2", "Type", Controller::GetControllerInfo(ControllerType::None).name);98si.SetStringValue("MemoryCards", "Card1Type", Settings::GetMemoryCardTypeName(MemoryCardType::NonPersistent));99si.SetStringValue("MemoryCards", "Card2Type", Settings::GetMemoryCardTypeName(MemoryCardType::None));100si.SetStringValue("ControllerPorts", "MultitapMode", Settings::GetMultitapModeName(MultitapMode::Disabled));101si.SetStringValue("Audio", "Backend", AudioStream::GetBackendName(AudioBackend::Null));102si.SetBoolValue("Logging", "LogToConsole", false);103si.SetBoolValue("Logging", "LogToFile", false);104si.SetStringValue("Logging", "LogLevel", Settings::GetLogLevelName(Log::Level::Info));105si.SetBoolValue("Main", "ApplyGameSettings", false); // don't want game settings interfering106si.SetBoolValue("BIOS", "PatchFastBoot", true); // no point validating the bios intro..107si.SetFloatValue("Main", "EmulationSpeed", 0.0f);108109// disable all sources110for (u32 i = 0; i < static_cast<u32>(InputSourceType::Count); i++)111si.SetBoolValue("InputSources", InputManager::InputSourceToString(static_cast<InputSourceType>(i)), false);112113return true;114}115116void Host::ReportFatalError(std::string_view title, std::string_view message)117{118ERROR_LOG("ReportFatalError: {}", message);119abort();120}121122void Host::ReportErrorAsync(std::string_view title, std::string_view message)123{124if (!title.empty() && !message.empty())125ERROR_LOG("ReportErrorAsync: {}: {}", title, message);126else if (!message.empty())127ERROR_LOG("ReportErrorAsync: {}", message);128}129130void Host::ReportStatusMessage(std::string_view message)131{132INFO_LOG("ReportStatusMessage: {}", message);133}134135void Host::ConfirmMessageAsync(std::string_view title, std::string_view message, ConfirmMessageAsyncCallback callback,136std::string_view yes_text, std::string_view no_text)137{138if (!title.empty() && !message.empty())139ERROR_LOG("ConfirmMessage: {}: {}", title, message);140else if (!message.empty())141ERROR_LOG("ConfirmMessage: {}", message);142143callback(true);144}145146void Host::ReportDebuggerEvent(CPU::DebuggerEvent event, std::string_view message)147{148ERROR_LOG("ReportDebuggerEvent: {}", message);149}150151std::span<const std::pair<const char*, const char*>> Host::GetAvailableLanguageList()152{153return {};154}155156const char* Host::GetLanguageName(std::string_view language_code)157{158return "";159}160161bool Host::ChangeLanguage(const char* new_language)162{163return false;164}165166s32 Host::Internal::GetTranslatedStringImpl(std::string_view context, std::string_view msg,167std::string_view disambiguation, char* tbuf, size_t tbuf_space)168{169if (msg.size() > tbuf_space)170return -1;171else if (msg.empty())172return 0;173174std::memcpy(tbuf, msg.data(), msg.size());175return static_cast<s32>(msg.size());176}177178std::string Host::TranslatePluralToString(const char* context, const char* msg, const char* disambiguation, int count)179{180TinyString count_str = TinyString::from_format("{}", count);181182std::string ret(msg);183for (;;)184{185std::string::size_type pos = ret.find("%n");186if (pos == std::string::npos)187break;188189ret.replace(pos, pos + 2, count_str.view());190}191192return ret;193}194195SmallString Host::TranslatePluralToSmallString(const char* context, const char* msg, const char* disambiguation,196int count)197{198SmallString ret(msg);199ret.replace("%n", TinyString::from_format("{}", count));200return ret;201}202203void Host::LoadSettings(const SettingsInterface& si, std::unique_lock<std::mutex>& lock)204{205}206207void Host::CheckForSettingsChanges(const Settings& old_settings)208{209}210211void Host::CommitBaseSettingChanges()212{213// noop, in memory214}215216bool Host::ResourceFileExists(std::string_view filename, bool allow_override)217{218const std::string path(Path::Combine(EmuFolders::Resources, filename));219return FileSystem::FileExists(path.c_str());220}221222std::optional<DynamicHeapArray<u8>> Host::ReadResourceFile(std::string_view filename, bool allow_override, Error* error)223{224const std::string path(Path::Combine(EmuFolders::Resources, filename));225return FileSystem::ReadBinaryFile(path.c_str(), error);226}227228std::optional<std::string> Host::ReadResourceFileToString(std::string_view filename, bool allow_override, Error* error)229{230const std::string path(Path::Combine(EmuFolders::Resources, filename));231return FileSystem::ReadFileToString(path.c_str(), error);232}233234std::optional<std::time_t> Host::GetResourceFileTimestamp(std::string_view filename, bool allow_override)235{236const std::string path(Path::Combine(EmuFolders::Resources, filename));237FILESYSTEM_STAT_DATA sd;238if (!FileSystem::StatFile(path.c_str(), &sd))239{240ERROR_LOG("Failed to stat resource file '{}'", filename);241return std::nullopt;242}243244return sd.ModificationTime;245}246247void Host::OnSystemStarting()248{249//250}251252void Host::OnSystemStarted()253{254//255}256257void Host::OnSystemStopping()258{259//260}261262void Host::OnSystemDestroyed()263{264//265}266267void Host::OnSystemPaused()268{269//270}271272void Host::OnSystemResumed()273{274//275}276277void Host::OnSystemAbnormalShutdown(const std::string_view reason)278{279// Already logged in core.280}281282void Host::OnVideoThreadRunIdleChanged(bool is_active)283{284//285}286287bool Host::SetScreensaverInhibit(bool inhibit, Error* error)288{289Error::SetStringView(error, "Not implemented");290return false;291}292293void Host::OnPerformanceCountersUpdated(const GPUBackend* gpu_backend)294{295//296}297298void Host::OnSystemGameChanged(const std::string& disc_path, const std::string& game_serial,299const std::string& game_name, GameHash hash)300{301INFO_LOG("Disc Path: {}", disc_path);302INFO_LOG("Game Serial: {}", game_serial);303INFO_LOG("Game Name: {}", game_name);304}305306void Host::OnSystemUndoStateAvailabilityChanged(bool available, u64 timestamp)307{308//309}310311void Host::OnMediaCaptureStarted()312{313//314}315316void Host::OnMediaCaptureStopped()317{318//319}320321void Host::PumpMessagesOnCoreThread()322{323RegTestHost::ProcessCoreThreadEvents();324325s_frames_remaining--;326if (s_frames_remaining == 0)327{328RegTestHost::DumpSystemStateHashes();329System::ShutdownSystem(false);330}331}332333void Host::RunOnCoreThread(std::function<void()> function, bool block /* = false */)334{335using namespace RegTestHost;336337std::unique_lock lock(s_state.core_thread_events_mutex);338s_state.cpu_thread_events.emplace_back(std::move(function), block);339s_state.blocking_cpu_events_pending += BoolToUInt32(block);340if (block)341s_state.core_thread_event_done.wait(lock, []() { return s_state.blocking_cpu_events_pending == 0; });342}343344void RegTestHost::ProcessCoreThreadEvents()345{346std::unique_lock lock(s_state.core_thread_events_mutex);347348for (;;)349{350if (s_state.cpu_thread_events.empty())351break;352353auto event = std::move(s_state.cpu_thread_events.front());354s_state.cpu_thread_events.pop_front();355lock.unlock();356event.first();357lock.lock();358359if (event.second)360{361s_state.blocking_cpu_events_pending--;362s_state.core_thread_event_done.notify_one();363}364}365}366367void Host::RunOnUIThread(std::function<void()> function, bool block /* = false */)368{369RunOnCoreThread(std::move(function), block);370}371372void Host::QueueAsyncTask(std::function<void()> function)373{374RegTestHost::s_async_task_queue.SubmitTask(std::move(function));375}376377void Host::WaitForAllAsyncTasks()378{379RegTestHost::s_async_task_queue.WaitForAll();380}381382void Host::RequestResizeHostDisplay(s32 width, s32 height)383{384//385}386387void Host::SetDefaultSettings(SettingsInterface& si)388{389//390}391392void Host::OnSettingsResetToDefault(bool host, bool system, bool controller)393{394//395}396397void Host::RequestExitApplication(bool save_state_if_running)398{399//400}401402void Host::RequestExitBigPicture()403{404//405}406407void Host::RequestSystemShutdown(bool allow_confirm, bool save_state, bool check_memcard_busy)408{409//410}411412std::optional<WindowInfo> Host::AcquireRenderWindow(RenderAPI render_api, bool fullscreen, bool exclusive_fullscreen,413Error* error)414{415return WindowInfo();416}417418WindowInfoType Host::GetRenderWindowInfoType()419{420return WindowInfoType::Surfaceless;421}422423void Host::ReleaseRenderWindow()424{425//426}427428bool Host::CanChangeFullscreenMode(bool new_fullscreen_state)429{430return false;431}432433void Host::BeginTextInput()434{435//436}437438void Host::EndTextInput()439{440//441}442443bool Host::CreateAuxiliaryRenderWindow(s32 x, s32 y, u32 width, u32 height, std::string_view title,444std::string_view icon_name, AuxiliaryRenderWindowUserData userdata,445AuxiliaryRenderWindowHandle* handle, WindowInfo* wi, Error* error)446{447return false;448}449450void Host::DestroyAuxiliaryRenderWindow(AuxiliaryRenderWindowHandle handle, s32* pos_x /* = nullptr */,451s32* pos_y /* = nullptr */, u32* width /* = nullptr */,452u32* height /* = nullptr */)453{454}455456void Host::FrameDoneOnVideoThread(GPUBackend* gpu_backend, u32 frame_number)457{458if (s_frame_dump_interval == 0 || (frame_number % s_frame_dump_interval) != 0 || !VideoPresenter::HasDisplayTexture())459return;460461// Need to take a copy of the display texture.462GPUTexture* const read_texture = VideoPresenter::GetDisplayTexture();463const GSVector4i read_rect = VideoPresenter::GetDisplayTextureRect();464const u32 read_x = static_cast<u32>(read_rect.x);465const u32 read_y = static_cast<u32>(read_rect.y);466const u32 read_width = static_cast<u32>(read_rect.width());467const u32 read_height = static_cast<u32>(read_rect.height());468const ImageFormat read_format = GPUTexture::GetImageFormatForTextureFormat(read_texture->GetFormat());469if (read_format == ImageFormat::None)470return;471472Image image(read_width, read_height, read_format);473std::unique_ptr<GPUDownloadTexture> dltex;474if (g_gpu_device->GetFeatures().memory_import)475{476dltex = g_gpu_device->CreateDownloadTexture(read_width, read_height, read_texture->GetFormat(), image.GetPixels(),477image.GetStorageSize(), image.GetPitch());478}479if (!dltex)480{481if (!(dltex = g_gpu_device->CreateDownloadTexture(read_width, read_height, read_texture->GetFormat())))482{483ERROR_LOG("Failed to create {}x{} {} download texture", read_width, read_height,484GPUTexture::GetFormatName(read_texture->GetFormat()));485return;486}487}488489dltex->CopyFromTexture(0, 0, read_texture, read_x, read_y, read_width, read_height, 0, 0, !dltex->IsImported());490if (!dltex->ReadTexels(0, 0, read_width, read_height, image.GetPixels(), image.GetPitch()))491{492ERROR_LOG("Failed to read {}x{} download texture", read_width, read_height);493gpu_backend->RestoreDeviceContext();494return;495}496497// no more GPU calls498gpu_backend->RestoreDeviceContext();499500Error error;501const std::string path = RegTestHost::GetFrameDumpPath(frame_number);502auto fp = FileSystem::OpenManagedCFile(path.c_str(), "wb", &error);503if (!fp)504{505ERROR_LOG("Can't open file '{}': {}", Path::GetFileName(path), error.GetDescription());506return;507}508509Host::QueueAsyncTask([path = std::move(path), fp = fp.release(), image = std::move(image)]() mutable {510Error error;511512if (image.GetFormat() != ImageFormat::RGBA8)513{514std::optional<Image> convert_image = image.ConvertToRGBA8(&error);515if (!convert_image.has_value())516{517ERROR_LOG("Failed to convert {} screenshot to RGBA8: {}", Image::GetFormatName(image.GetFormat()),518error.GetDescription());519image.Invalidate();520}521else522{523image = std::move(convert_image.value());524}525}526527bool result = false;528if (image.IsValid())529{530image.SetAllPixelsOpaque();531532result = image.SaveToFile(path.c_str(), fp, Image::DEFAULT_SAVE_QUALITY, &error);533if (!result)534ERROR_LOG("Failed to save screenshot to '{}': '{}'", Path::GetFileName(path), error.GetDescription());535}536537std::fclose(fp);538return result;539});540}541542void Host::OpenURL(std::string_view url)543{544//545}546547std::string Host::GetClipboardText()548{549return std::string();550}551552bool Host::CopyTextToClipboard(std::string_view text)553{554return false;555}556557std::string Host::FormatNumber(NumberFormatType type, s64 value)558{559std::string ret;560561if (type >= NumberFormatType::ShortDate && type <= NumberFormatType::LongDateTime)562{563const char* format;564switch (type)565{566case NumberFormatType::ShortDate:567format = "%x";568break;569570case NumberFormatType::LongDate:571format = "%A %B %e %Y";572break;573574case NumberFormatType::ShortTime:575case NumberFormatType::LongTime:576format = "%X";577break;578579case NumberFormatType::ShortDateTime:580format = "%X %x";581break;582583case NumberFormatType::LongDateTime:584format = "%c";585break;586587DefaultCaseIsUnreachable();588}589590ret.resize(128);591592if (const std::optional<std::tm> ltime = Common::LocalTime(static_cast<std::time_t>(value)))593ret.resize(std::strftime(ret.data(), ret.size(), format, <ime.value()));594else595ret = "Invalid";596}597else598{599ret = fmt::format("{}", value);600}601602return ret;603}604605std::string Host::FormatNumber(NumberFormatType type, double value)606{607return fmt::format("{}", value);608}609610void Host::SetMouseMode(bool relative, bool hide_cursor)611{612//613}614615void Host::OnAchievementsLoginRequested(Achievements::LoginRequestReason reason)616{617// noop618}619620void Host::OnAchievementsLoginSuccess(const char* username, u32 points, u32 sc_points, u32 unread_messages)621{622// noop623}624625void Host::OnAchievementsActiveChanged(bool active)626{627// noop628}629630void Host::OnAchievementsHardcoreModeChanged(bool enabled)631{632// noop633}634635#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION636637void Host::OnRAIntegrationMenuChanged()638{639// noop640}641642#endif643644const char* Host::GetDefaultFullscreenUITheme()645{646return "";647}648649void Host::AddFixedInputBindings(const SettingsInterface& si)650{651// noop652}653654void Host::OnInputDeviceConnected(InputBindingKey key, std::string_view identifier, std::string_view device_name)655{656// noop657}658659void Host::OnInputDeviceDisconnected(InputBindingKey key, std::string_view identifier)660{661// noop662}663664std::optional<WindowInfo> Host::GetTopLevelWindowInfo()665{666return std::nullopt;667}668669void Host::RefreshGameListAsync(bool invalidate_cache)670{671// noop672}673674void Host::CancelGameListRefresh()675{676// noop677}678679void Host::OnGameListEntriesChanged(std::span<const u32> changed_indices)680{681// noop682}683684static void SignalHandler(int signal)685{686std::signal(signal, SIG_DFL);687688// MacOS is missing std::quick_exit() despite it being C++11...689#ifndef __APPLE__690std::quick_exit(1);691#else692_Exit(1);693#endif694}695696void RegTestHost::HookSignals()697{698std::signal(SIGINT, SignalHandler);699std::signal(SIGTERM, SignalHandler);700701#ifndef _WIN32702// Ignore SIGCHLD by default on Linux, since we kick off aplay asynchronously.703struct sigaction sa_chld = {};704sigemptyset(&sa_chld.sa_mask);705sa_chld.sa_handler = SIG_IGN;706sa_chld.sa_flags = SA_RESTART | SA_NOCLDSTOP | SA_NOCLDWAIT;707sigaction(SIGCHLD, &sa_chld, nullptr);708#endif709}710711void RegTestHost::VideoThreadEntryPoint()712{713Threading::SetNameOfCurrentThread("Video Thread");714VideoThread::Internal::VideoThreadEntryPoint();715}716717void RegTestHost::DumpSystemStateHashes()718{719Error error;720721// don't save full state on gpu dump, it's not going to be complete...722if (!System::IsReplayingGPUDump())723{724DynamicHeapArray<u8> state_data(System::GetMaxSaveStateSize(g_settings.cpu_enable_8mb_ram));725size_t state_data_size;726if (!System::SaveStateDataToBuffer(state_data, &state_data_size, &error))727{728ERROR_LOG("Failed to save system state: {}", error.GetDescription());729return;730}731732INFO_LOG("Save State Hash: {}",733SHA256Digest::DigestToString(SHA256Digest::GetDigest(state_data.cspan(0, state_data_size))));734INFO_LOG("RAM Hash: {}",735SHA256Digest::DigestToString(SHA256Digest::GetDigest(std::span<const u8>(Bus::g_ram, Bus::g_ram_size))));736INFO_LOG("SPU RAM Hash: {}", SHA256Digest::DigestToString(SHA256Digest::GetDigest(SPU::GetRAM())));737}738739INFO_LOG("VRAM Hash: {}", SHA256Digest::DigestToString(SHA256Digest::GetDigest(740std::span<const u8>(reinterpret_cast<const u8*>(g_vram), VRAM_SIZE))));741}742743void RegTestHost::InitializeEarlyConsole()744{745const bool was_console_enabled = Log::IsConsoleOutputEnabled();746if (!was_console_enabled)747{748Log::SetConsoleOutputParams(true);749Log::SetLogLevel(Log::Level::Info);750}751}752753void RegTestHost::PrintCommandLineVersion()754{755InitializeEarlyConsole();756std::fprintf(stderr, "DuckStation Regression Test Runner Version %s (%s)\n", g_scm_tag_str, g_scm_branch_str);757std::fprintf(stderr, "https://github.com/stenzek/duckstation\n");758std::fprintf(stderr, "\n");759}760761void RegTestHost::PrintCommandLineHelp(const char* progname)762{763InitializeEarlyConsole();764PrintCommandLineVersion();765std::fprintf(stderr, "Usage: %s [parameters] [--] [boot filename]\n", progname);766std::fprintf(stderr, "\n");767std::fprintf(stderr, " -help: Displays this information and exits.\n");768std::fprintf(stderr, " -version: Displays version information and exits.\n");769std::fprintf(stderr, " -dumpdir: Set frame dump base directory (will be dumped to basedir/gametitle).\n");770std::fprintf(stderr, " -dumpinterval: Dumps every N frames.\n");771std::fprintf(stderr, " -frames: Sets the number of frames to execute.\n");772std::fprintf(stderr, " -log <level>: Sets the log level. Defaults to verbose.\n");773std::fprintf(stderr, " -console: Enables console logging output.\n");774std::fprintf(stderr, " -pgxp: Enables PGXP.\n");775std::fprintf(stderr, " -pgxp-cpu: Forces PGXP CPU mode.\n");776std::fprintf(stderr, " -renderer <renderer>: Sets the graphics renderer. Default to software.\n");777std::fprintf(stderr, " -upscale <multiplier>: Enables upscaled rendering at the specified multiplier.\n");778std::fprintf(stderr, " --: Signals that no more arguments will follow and the remaining\n"779" parameters make up the filename. Use when the filename contains\n"780" spaces or starts with a dash.\n");781std::fprintf(stderr, "\n");782}783784static std::optional<SystemBootParameters>& AutoBoot(std::optional<SystemBootParameters>& autoboot)785{786if (!autoboot)787autoboot.emplace();788789return autoboot;790}791792bool RegTestHost::ParseCommandLineParameters(int argc, char* argv[], std::optional<SystemBootParameters>& autoboot)793{794bool no_more_args = false;795for (int i = 1; i < argc; i++)796{797if (!no_more_args)798{799#define CHECK_ARG(str) !std::strcmp(argv[i], str)800#define CHECK_ARG_PARAM(str) (!std::strcmp(argv[i], str) && ((i + 1) < argc))801802if (CHECK_ARG("-help"))803{804PrintCommandLineHelp(argv[0]);805return false;806}807else if (CHECK_ARG("-version"))808{809PrintCommandLineVersion();810return false;811}812else if (CHECK_ARG_PARAM("-dumpdir"))813{814s_dump_base_directory = argv[++i];815if (s_dump_base_directory.empty())816{817ERROR_LOG("Invalid dump directory specified.");818return false;819}820821continue;822}823else if (CHECK_ARG_PARAM("-dumpinterval"))824{825s_frame_dump_interval = StringUtil::FromChars<u32>(argv[++i]).value_or(0);826if (s_frame_dump_interval <= 0)827{828ERROR_LOG("Invalid dump interval specified: {}", argv[i]);829return false;830}831832continue;833}834else if (CHECK_ARG_PARAM("-frames"))835{836s_frames_to_run = StringUtil::FromChars<u32>(argv[++i]).value_or(0);837if (s_frames_to_run == 0)838{839ERROR_LOG("Invalid frame count specified: {}", argv[i]);840return false;841}842843continue;844}845else if (CHECK_ARG_PARAM("-log"))846{847std::optional<Log::Level> level = Settings::ParseLogLevelName(argv[++i]);848if (!level.has_value())849{850ERROR_LOG("Invalid log level specified.");851return false;852}853854Log::SetLogLevel(level.value());855Core::SetBaseStringSettingValue("Logging", "LogLevel", Settings::GetLogLevelName(level.value()));856continue;857}858else if (CHECK_ARG("-console"))859{860Log::SetConsoleOutputParams(true);861Core::SetBaseBoolSettingValue("Logging", "LogToConsole", true);862continue;863}864else if (CHECK_ARG_PARAM("-renderer"))865{866std::optional<GPURenderer> renderer = Settings::ParseRendererName(argv[++i]);867if (!renderer.has_value())868{869ERROR_LOG("Invalid renderer specified.");870return false;871}872873Core::SetBaseStringSettingValue("GPU", "Renderer", Settings::GetRendererName(renderer.value()));874continue;875}876else if (CHECK_ARG_PARAM("-upscale"))877{878const u32 upscale = StringUtil::FromChars<u32>(argv[++i]).value_or(0);879if (upscale == 0)880{881ERROR_LOG("Invalid upscale value.");882return false;883}884885INFO_LOG("Setting upscale to {}.", upscale);886Core::SetBaseIntSettingValue("GPU", "ResolutionScale", static_cast<s32>(upscale));887continue;888}889else if (CHECK_ARG_PARAM("-cpu"))890{891const std::optional<CPUExecutionMode> cpu = Settings::ParseCPUExecutionMode(argv[++i]);892if (!cpu.has_value())893{894ERROR_LOG("Invalid CPU execution mode.");895return false;896}897898INFO_LOG("Setting CPU execution mode to {}.", Settings::GetCPUExecutionModeName(cpu.value()));899Core::SetBaseStringSettingValue("CPU", "ExecutionMode", Settings::GetCPUExecutionModeName(cpu.value()));900continue;901}902else if (CHECK_ARG("-pgxp"))903{904INFO_LOG("Enabling PGXP.");905Core::SetBaseBoolSettingValue("GPU", "PGXPEnable", true);906continue;907}908else if (CHECK_ARG("-pgxp-cpu"))909{910INFO_LOG("Enabling PGXP CPU mode.");911Core::SetBaseBoolSettingValue("GPU", "PGXPEnable", true);912Core::SetBaseBoolSettingValue("GPU", "PGXPCPU", true);913continue;914}915else if (CHECK_ARG("--"))916{917no_more_args = true;918continue;919}920else if (argv[i][0] == '-')921{922ERROR_LOG("Unknown parameter: '{}'", argv[i]);923return false;924}925926#undef CHECK_ARG927#undef CHECK_ARG_PARAM928}929930if (autoboot && !autoboot->path.empty())931autoboot->path += ' ';932AutoBoot(autoboot)->path += argv[i];933}934935return true;936}937938bool RegTestHost::SetNewDataRoot(const std::string& filename)939{940if (!s_dump_base_directory.empty())941{942std::string game_subdir = Path::SanitizeFileName(Path::GetFileTitle(filename));943INFO_LOG("Writing to subdirectory '{}'", game_subdir);944945std::string dump_directory = Path::Combine(s_dump_base_directory, game_subdir);946if (!FileSystem::DirectoryExists(dump_directory.c_str()))947{948INFO_LOG("Creating directory '{}'...", dump_directory);949if (!FileSystem::CreateDirectory(dump_directory.c_str(), false))950Panic("Failed to create dump directory.");951}952953// Switch to file logging.954INFO_LOG("Dumping frames to '{}'...", dump_directory);955956const auto lock = Core::GetSettingsLock();957EmuFolders::DataRoot = std::move(dump_directory);958SettingsInterface& si = *Core::GetBaseSettingsLayer();959si.SetBoolValue("Logging", "LogToFile", true);960si.SetStringValue("Logging", "LogLevel", Settings::GetLogLevelName(Log::Level::Dev));961Settings::UpdateLogConfig(si);962}963964return true;965}966967std::string RegTestHost::GetFrameDumpPath(u32 frame)968{969return Path::Combine(EmuFolders::DataRoot, fmt::format("frame_{:05d}.png", frame));970}971972int main(int argc, char* argv[])973{974CrashHandler::Install(&Bus::CleanupMemoryMap);975976Error error;977if (!System::PerformEarlyHardwareChecks(&error) || !System::ProcessStartup(&error))978{979std::fprintf(stderr, "ERROR: ProcessStartup() failed: %s\n", error.GetDescription().c_str());980return EXIT_FAILURE;981}982983if (!RegTestHost::InitializeFoldersAndConfig(&error))984{985std::fprintf(stderr, "ERROR: Failed to initialize config: %s\n", error.GetDescription().c_str());986return EXIT_FAILURE;987}988989std::optional<SystemBootParameters> autoboot;990if (!RegTestHost::ParseCommandLineParameters(argc, argv, autoboot))991return EXIT_FAILURE;992993if (!autoboot || autoboot->path.empty())994{995ERROR_LOG("No boot path specified.");996return EXIT_FAILURE;997}998999if (!RegTestHost::SetNewDataRoot(autoboot->path))1000return EXIT_FAILURE;10011002if (!System::CoreThreadInitialize(&error))1003{1004ERROR_LOG("CoreThreadInitialize() failed: {}", error.GetDescription());1005return EXIT_FAILURE;1006}10071008// Only one async worker, keep the CPU usage down so we can parallelize execution of regtest itself.1009RegTestHost::s_async_task_queue.SetWorkerCount(1);10101011RegTestHost::HookSignals();1012s_video_thread.Start(&RegTestHost::VideoThreadEntryPoint);10131014int result = -1;1015INFO_LOG("Trying to boot '{}'...", autoboot->path);1016if (!System::BootSystem(std::move(autoboot.value()), &error))1017{1018ERROR_LOG("Failed to boot system: {}", error.GetDescription());1019goto cleanup;1020}10211022if (System::IsReplayingGPUDump() && !s_dump_base_directory.empty())1023{1024INFO_LOG("Replaying GPU dump, dumping all frames.");1025s_frame_dump_interval = 1;1026s_frames_to_run = static_cast<u32>(System::GetGPUDumpFrameCount());1027}10281029if (s_frame_dump_interval > 0)1030{1031if (s_dump_base_directory.empty())1032{1033ERROR_LOG("Dump directory not specified.");1034goto cleanup;1035}10361037INFO_LOG("Dumping every {}th frame to '{}'.", s_frame_dump_interval, s_dump_base_directory);1038}10391040INFO_LOG("Running for {} frames...", s_frames_to_run);1041s_frames_remaining = s_frames_to_run;10421043{1044const Timer::Value start_time = Timer::GetCurrentValue();10451046System::Execute();10471048const Timer::Value elapsed_time = Timer::GetCurrentValue() - start_time;1049const double elapsed_time_ms = Timer::ConvertValueToMilliseconds(elapsed_time);1050INFO_LOG("Total execution time: {:.2f}ms, average frame time {:.2f}ms, {:.2f} FPS", elapsed_time_ms,1051elapsed_time_ms / static_cast<double>(s_frames_to_run),1052static_cast<double>(s_frames_to_run) / elapsed_time_ms * 1000.0);1053}10541055INFO_LOG("Exiting with success.");1056result = 0;10571058cleanup:1059if (s_video_thread.Joinable())1060{1061VideoThread::Internal::RequestShutdown();1062s_video_thread.Join();1063}10641065RegTestHost::s_async_task_queue.SetWorkerCount(0);10661067RegTestHost::ProcessCoreThreadEvents();1068System::CoreThreadShutdown();1069System::ProcessShutdown();1070return result;1071}107210731074