// SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team // SPDX-License-Identifier: LGPL-3.0+ #include #include #include #include #include #include #include #ifdef _WIN32 #include "common/RedtapeWindows.h" #endif #include "fmt/core.h" #include "common/Assertions.h" #include "common/Console.h" #include "common/FileSystem.h" #include "common/MemorySettingsInterface.h" #include "common/Path.h" #include "common/SettingsWrapper.h" #include "common/StringUtil.h" #include "pcsx2/PrecompiledHeader.h" #include "pcsx2/Achievements.h" #include "pcsx2/CDVD/CDVD.h" #include "pcsx2/GS.h" #include "pcsx2/GS/GSPerfMon.h" #include "pcsx2/GSDumpReplayer.h" #include "pcsx2/GameList.h" #include "pcsx2/Host.h" #include "pcsx2/INISettingsInterface.h" #include "pcsx2/ImGui/ImGuiManager.h" #include "pcsx2/Input/InputManager.h" #include "pcsx2/LogSink.h" #include "pcsx2/MTGS.h" #include "pcsx2/SIO/Pad/Pad.h" #include "pcsx2/PerformanceMetrics.h" #include "pcsx2/VMManager.h" #include "svnrev.h" namespace GSRunner { static void InitializeConsole(); static bool InitializeConfig(); static bool ParseCommandLineArgs(int argc, char* argv[], VMBootParameters& params); static void DumpStats(); static bool CreatePlatformWindow(); static void DestroyPlatformWindow(); static std::optional GetPlatformWindowInfo(); static void PumpPlatformMessages(); } // namespace GSRunner static constexpr u32 WINDOW_WIDTH = 640; static constexpr u32 WINDOW_HEIGHT = 480; static MemorySettingsInterface s_settings_interface; static std::string s_output_prefix; static s32 s_loop_count = 1; static std::optional s_use_window; static bool s_no_console = false; // Owned by the GS thread. static u32 s_dump_frame_number = 0; static u32 s_loop_number = s_loop_count; static double s_last_internal_draws = 0; static double s_last_draws = 0; static double s_last_render_passes = 0; static double s_last_barriers = 0; static double s_last_copies = 0; static double s_last_uploads = 0; static double s_last_readbacks = 0; static u64 s_total_internal_draws = 0; static u64 s_total_draws = 0; static u64 s_total_render_passes = 0; static u64 s_total_barriers = 0; static u64 s_total_copies = 0; static u64 s_total_uploads = 0; static u64 s_total_readbacks = 0; static u32 s_total_frames = 0; static u32 s_total_drawn_frames = 0; bool GSRunner::InitializeConfig() { if (!EmuFolders::InitializeCriticalFolders()) return false; const char* error; if (!VMManager::PerformEarlyHardwareChecks(&error)) return false; // don't provide an ini path, or bother loading. we'll store everything in memory. MemorySettingsInterface& si = s_settings_interface; Host::Internal::SetBaseSettingsLayer(&si); VMManager::SetDefaultSettings(si, true, true, true, true, true); // complete as quickly as possible si.SetBoolValue("EmuCore/GS", "FrameLimitEnable", false); si.SetIntValue("EmuCore/GS", "VsyncEnable", static_cast(VsyncMode::Off)); // ensure all input sources are disabled, we're not using them si.SetBoolValue("InputSources", "SDL", false); si.SetBoolValue("InputSources", "XInput", false); // we don't need any sound output si.SetStringValue("SPU2/Output", "OutputModule", "nullout"); // none of the bindings are going to resolve to anything Pad::ClearPortBindings(si, 0); si.ClearSection("Hotkeys"); // make sure any gamesettings inis in your tree don't get loaded si.SetBoolValue("EmuCore", "EnablePerGameSettings", false); // force logging si.SetBoolValue("Logging", "EnableSystemConsole", !s_no_console); si.SetBoolValue("Logging", "EnableTimestamps", true); si.SetBoolValue("Logging", "EnableVerbose", true); // and show some stats :) si.SetBoolValue("EmuCore/GS", "OsdShowFPS", true); si.SetBoolValue("EmuCore/GS", "OsdShowResolution", true); si.SetBoolValue("EmuCore/GS", "OsdShowGSStats", true); // remove memory cards, so we don't have sharing violations for (u32 i = 0; i < 2; i++) { si.SetBoolValue("MemoryCards", fmt::format("Slot{}_Enable", i + 1).c_str(), false); si.SetStringValue("MemoryCards", fmt::format("Slot{}_Filename", i + 1).c_str(), ""); } VMManager::Internal::LoadStartupSettings(); return true; } void Host::CommitBaseSettingChanges() { // nothing to save, we're all in memory } void Host::LoadSettings(SettingsInterface& si, std::unique_lock& lock) { } void Host::CheckForSettingsChanges(const Pcsx2Config& old_config) { } bool Host::RequestResetSettings(bool folders, bool core, bool controllers, bool hotkeys, bool ui) { // not running any UI, so no settings requests will come in return false; } void Host::SetDefaultUISettings(SettingsInterface& si) { // nothing } void Host::ReportErrorAsync(const std::string_view& title, const std::string_view& message) { if (!title.empty() && !message.empty()) { Console.Error( "ReportErrorAsync: %.*s: %.*s", static_cast(title.size()), title.data(), static_cast(message.size()), message.data()); } else if (!message.empty()) { Console.Error("ReportErrorAsync: %.*s", static_cast(message.size()), message.data()); } } bool Host::ConfirmMessage(const std::string_view& title, const std::string_view& message) { if (!title.empty() && !message.empty()) { Console.Error( "ConfirmMessage: %.*s: %.*s", static_cast(title.size()), title.data(), static_cast(message.size()), message.data()); } else if (!message.empty()) { Console.Error("ConfirmMessage: %.*s", static_cast(message.size()), message.data()); } return true; } void Host::OpenURL(const std::string_view& url) { // noop } bool Host::CopyTextToClipboard(const std::string_view& text) { return false; } void Host::BeginTextInput() { // noop } void Host::EndTextInput() { // noop } std::optional Host::GetTopLevelWindowInfo() { return GSRunner::GetPlatformWindowInfo(); } void Host::OnInputDeviceConnected(const std::string_view& identifier, const std::string_view& device_name) { } void Host::OnInputDeviceDisconnected(const std::string_view& identifier) { } void Host::SetMouseMode(bool relative_mode, bool hide_cursor) { } std::optional Host::AcquireRenderWindow(bool recreate_window) { return GSRunner::GetPlatformWindowInfo(); } void Host::ReleaseRenderWindow() { } void Host::BeginPresentFrame() { if (s_loop_number == 0 && !s_output_prefix.empty()) { // when we wrap around, don't race other files GSJoinSnapshotThreads(); // queue dumping of this frame std::string dump_path(fmt::format("{}_frame{}.png", s_output_prefix, s_dump_frame_number)); GSQueueSnapshot(dump_path); } if (GSIsHardwareRenderer()) { const u32 last_draws = s_total_internal_draws; const u32 last_uploads = s_total_uploads; static constexpr auto update_stat = [](GSPerfMon::counter_t counter, u64& dst, double& last) { // perfmon resets every 30 frames to zero const double val = g_perfmon.GetCounter(counter); dst += static_cast((val < last) ? val : (val - last)); last = val; }; update_stat(GSPerfMon::Draw, s_total_internal_draws, s_last_internal_draws); update_stat(GSPerfMon::DrawCalls, s_total_draws, s_last_draws); update_stat(GSPerfMon::RenderPasses, s_total_render_passes, s_last_render_passes); update_stat(GSPerfMon::Barriers, s_total_barriers, s_last_barriers); update_stat(GSPerfMon::TextureCopies, s_total_copies, s_last_copies); update_stat(GSPerfMon::TextureUploads, s_total_uploads, s_last_uploads); update_stat(GSPerfMon::Readbacks, s_total_readbacks, s_last_readbacks); const bool idle_frame = s_total_frames && (last_draws == s_total_internal_draws && last_uploads == s_total_uploads); if(!idle_frame) s_total_drawn_frames++; s_total_frames++; std::atomic_thread_fence(std::memory_order_release); } } void Host::RequestResizeHostDisplay(s32 width, s32 height) { } void Host::OnVMStarting() { } void Host::OnVMStarted() { } void Host::OnVMDestroyed() { } void Host::OnVMPaused() { } void Host::OnVMResumed() { } void Host::OnGameChanged(const std::string& title, const std::string& elf_override, const std::string& disc_path, const std::string& disc_serial, u32 disc_crc, u32 current_crc) { } void Host::OnPerformanceMetricsUpdated() { } void Host::OnSaveStateLoading(const std::string_view& filename) { } void Host::OnSaveStateLoaded(const std::string_view& filename, bool was_successful) { } void Host::OnSaveStateSaved(const std::string_view& filename) { } void Host::RunOnCPUThread(std::function function, bool block /* = false */) { pxFailRel("Not implemented"); } void Host::RefreshGameListAsync(bool invalidate_cache) { } void Host::CancelGameListRefresh() { } bool Host::IsFullscreen() { return false; } void Host::SetFullscreen(bool enabled) { } void Host::OnCaptureStarted(const std::string& filename) { } void Host::OnCaptureStopped() { } void Host::RequestExit(bool allow_confirm) { } void Host::RequestVMShutdown(bool allow_confirm, bool allow_save_state, bool default_save_state) { VMManager::SetState(VMState::Stopping); } void Host::OnAchievementsLoginSuccess(const char* username, u32 points, u32 sc_points, u32 unread_messages) { // noop } void Host::OnAchievementsLoginRequested(Achievements::LoginRequestReason reason) { // noop } void Host::OnAchievementsHardcoreModeChanged(bool enabled) { // noop } void Host::OnAchievementsRefreshed() { // noop } void Host::OnCoverDownloaderOpenRequested() { // noop } std::optional InputManager::ConvertHostKeyboardStringToCode(const std::string_view& str) { return std::nullopt; } std::optional InputManager::ConvertHostKeyboardCodeToString(u32 code) { return std::nullopt; } const char* InputManager::ConvertHostKeyboardCodeToIcon(u32 code) { return nullptr; } BEGIN_HOTKEY_LIST(g_host_hotkeys) END_HOTKEY_LIST() static void PrintCommandLineVersion() { std::fprintf(stderr, "PCSX2 GS Runner Version %s\n", GIT_REV); std::fprintf(stderr, "https://pcsx2.net/\n"); std::fprintf(stderr, "\n"); } static void PrintCommandLineHelp(const char* progname) { PrintCommandLineVersion(); std::fprintf(stderr, "Usage: %s [parameters] [--] [filename]\n", progname); std::fprintf(stderr, "\n"); std::fprintf(stderr, " -help: Displays this information and exits.\n"); std::fprintf(stderr, " -version: Displays version information and exits.\n"); std::fprintf(stderr, " -dumpdir : Frame dump directory (will be dumped as filename_frameN.png).\n"); std::fprintf(stderr, " -loop : Loops dump playback N times. Defaults to 1. 0 will loop infinitely.\n"); std::fprintf(stderr, " -renderer : Sets the graphics renderer. Defaults to Auto.\n"); std::fprintf(stderr, " -window: Forces a window to be displayed.\n"); std::fprintf(stderr, " -surfaceless: Disables showing a window.\n"); std::fprintf(stderr, " -logfile : Writes emu log to filename.\n"); std::fprintf(stderr, " -noshadercache: Disables the shader cache (useful for parallel runs).\n"); std::fprintf(stderr, " --: Signals that no more arguments will follow and the remaining\n" " parameters make up the filename. Use when the filename contains\n" " spaces or starts with a dash.\n"); std::fprintf(stderr, "\n"); } void GSRunner::InitializeConsole() { const char* var = std::getenv("PCSX2_NOCONSOLE"); s_no_console = (var && StringUtil::FromChars(var).value_or(false)); if (!s_no_console) LogSink::InitializeEarlyConsole(); } bool GSRunner::ParseCommandLineArgs(int argc, char* argv[], VMBootParameters& params) { bool no_more_args = false; for (int i = 1; i < argc; i++) { if (!no_more_args) { #define CHECK_ARG(str) !std::strcmp(argv[i], str) #define CHECK_ARG_PARAM(str) (!std::strcmp(argv[i], str) && ((i + 1) < argc)) if (CHECK_ARG("-help")) { PrintCommandLineHelp(argv[0]); return false; } else if (CHECK_ARG("-version")) { PrintCommandLineVersion(); return false; } else if (CHECK_ARG_PARAM("-dumpdir")) { s_output_prefix = StringUtil::StripWhitespace(argv[++i]); if (s_output_prefix.empty()) { Console.Error("Invalid dump directory specified."); return false; } if (!FileSystem::DirectoryExists(s_output_prefix.c_str()) && !FileSystem::CreateDirectoryPath(s_output_prefix.c_str(), false)) { Console.Error("Failed to create output directory"); return false; } continue; } else if (CHECK_ARG_PARAM("-loop")) { s_loop_count = StringUtil::FromChars(argv[++i]).value_or(0); Console.WriteLn("Looping dump playback %d times.", s_loop_count); continue; } else if (CHECK_ARG_PARAM("-renderer")) { const char* rname = argv[++i]; GSRendererType type = GSRendererType::Auto; if (StringUtil::Strcasecmp(rname, "Auto") == 0) type = GSRendererType::Auto; #ifdef _WIN32 else if (StringUtil::Strcasecmp(rname, "dx11") == 0) type = GSRendererType::DX11; else if (StringUtil::Strcasecmp(rname, "dx12") == 0) type = GSRendererType::DX12; #endif #ifdef ENABLE_OPENGL else if (StringUtil::Strcasecmp(rname, "gl") == 0) type = GSRendererType::OGL; #endif #ifdef ENABLE_VULKAN else if (StringUtil::Strcasecmp(rname, "vulkan") == 0) type = GSRendererType::VK; #endif #ifdef __APPLE__ else if (StringUtil::Strcasecmp(rname, "metal") == 0) type = GSRendererType::Metal; #endif else if (StringUtil::Strcasecmp(rname, "sw") == 0) type = GSRendererType::SW; else { Console.Error("Unknown renderer '%s'", rname); return false; } Console.WriteLn("Using %s renderer.", Pcsx2Config::GSOptions::GetRendererName(type)); s_settings_interface.SetIntValue("EmuCore/GS", "Renderer", static_cast(type)); continue; } else if (CHECK_ARG_PARAM("-renderhacks")) { std::string str(argv[++i]); s_settings_interface.SetBoolValue("EmuCore/GS", "UserHacks", true); if(str.find("af") != std::string::npos) s_settings_interface.SetBoolValue("EmuCore/GS", "UserHacks_AutoFlush", true); if (str.find("cpufb") != std::string::npos) s_settings_interface.SetBoolValue("EmuCore/GS", "UserHacks_CPU_FB_Conversion", true); if (str.find("dds") != std::string::npos) s_settings_interface.SetBoolValue("EmuCore/GS", "UserHacks_DisableDepthSupport", true); if (str.find("dpi") != std::string::npos) s_settings_interface.SetBoolValue("EmuCore/GS", "UserHacks_DisablePartialInvalidation", true); if (str.find("dsf") != std::string::npos) s_settings_interface.SetBoolValue("EmuCore/GS", "UserHacks_DisableSafeFeatures", true); if (str.find("tinrt") != std::string::npos) s_settings_interface.SetBoolValue("EmuCore/GS", "UserHacks_TextureInsideRt", true); if (str.find("plf") != std::string::npos) s_settings_interface.SetBoolValue("EmuCore/GS", "preload_frame_with_gs_data", true); continue; } else if (CHECK_ARG_PARAM("-upscale")) { const float upscale = StringUtil::FromChars(argv[++i]).value_or(0.0f); if (upscale < 0.5f) { Console.WriteLn("Invalid upscale multiplier"); return false; } Console.WriteLn(fmt::format("Setting upscale multiplier to {}", upscale)); s_settings_interface.SetFloatValue("EmuCore/GS", "upscale_multiplier", upscale); continue; } else if (CHECK_ARG_PARAM("-logfile")) { const char* logfile = argv[++i]; if (std::strlen(logfile) > 0) { // disable timestamps, since we want to be able to diff the logs Console.WriteLn("Logging to %s...", logfile); LogSink::SetFileLogPath(logfile); s_settings_interface.SetBoolValue("Logging", "EnableFileLogging", true); s_settings_interface.SetBoolValue("Logging", "EnableTimestamps", false); } continue; } else if (CHECK_ARG("-noshadercache")) { Console.WriteLn("Disabling shader cache"); s_settings_interface.SetBoolValue("EmuCore/GS", "disable_shader_cache", true); continue; } else if (CHECK_ARG("-window")) { Console.WriteLn("Creating window"); s_use_window = true; continue; } else if (CHECK_ARG("-surfaceless")) { Console.WriteLn("Running surfaceless"); s_use_window = false; continue; } else if (CHECK_ARG("--")) { no_more_args = true; continue; } else if (argv[i][0] == '-') { Console.Error("Unknown parameter: '%s'", argv[i]); return false; } #undef CHECK_ARG #undef CHECK_ARG_PARAM } if (!params.filename.empty()) params.filename += ' '; params.filename += argv[i]; } if (params.filename.empty()) { Console.Error("No dump filename provided."); return false; } if (!VMManager::IsGSDumpFileName(params.filename)) { Console.Error("Provided filename is not a GS dump."); return false; } // set up the frame dump directory if (!s_output_prefix.empty()) { // strip off all extensions std::string_view title(Path::GetFileTitle(params.filename)); if (StringUtil::EndsWithNoCase(title, ".gs")) title = Path::GetFileTitle(title); s_output_prefix = Path::Combine(s_output_prefix, StringUtil::StripWhitespace(title)); Console.WriteLn(fmt::format("Saving dumps as {}_frameN.png", s_output_prefix)); } return true; } void GSRunner::DumpStats() { std::atomic_thread_fence(std::memory_order_acquire); Console.WriteLn(fmt::format("======= HW STATISTICS FOR {} ({}) FRAMES ========", s_total_frames, s_total_drawn_frames)); Console.WriteLn(fmt::format("@HWSTAT@ Draw Calls: {} (avg {})", s_total_draws, static_cast(std::ceil(s_total_draws / static_cast(s_total_drawn_frames))))); Console.WriteLn(fmt::format("@HWSTAT@ Render Passes: {} (avg {})", s_total_render_passes, static_cast(std::ceil(s_total_render_passes / static_cast(s_total_drawn_frames))))); Console.WriteLn(fmt::format("@HWSTAT@ Barriers: {} (avg {})", s_total_barriers, static_cast(std::ceil(s_total_barriers / static_cast(s_total_drawn_frames))))); Console.WriteLn(fmt::format("@HWSTAT@ Copies: {} (avg {})", s_total_copies, static_cast(std::ceil(s_total_copies / static_cast(s_total_drawn_frames))))); Console.WriteLn(fmt::format("@HWSTAT@ Uploads: {} (avg {})", s_total_uploads, static_cast(std::ceil(s_total_uploads / static_cast(s_total_drawn_frames))))); Console.WriteLn(fmt::format("@HWSTAT@ Readbacks: {} (avg {})", s_total_readbacks, static_cast(std::ceil(s_total_readbacks / static_cast(s_total_drawn_frames))))); Console.WriteLn("============================================"); } #ifdef _WIN32 // We can't handle unicode in filenames if we don't use wmain on Win32. #define main real_main #endif int main(int argc, char* argv[]) { GSRunner::InitializeConsole(); if (!GSRunner::InitializeConfig()) { Console.Error("Failed to initialize config."); return EXIT_FAILURE; } VMBootParameters params; if (!GSRunner::ParseCommandLineArgs(argc, argv, params)) return EXIT_FAILURE; if (!VMManager::Internal::CPUThreadInitialize()) return EXIT_FAILURE; if (s_use_window.value_or(true) && !GSRunner::CreatePlatformWindow()) { Console.Error("Failed to create window."); return EXIT_FAILURE; } // apply new settings (e.g. pick up renderer change) VMManager::ApplySettings(); GSDumpReplayer::SetIsDumpRunner(true); if (VMManager::Initialize(params)) { // run until end GSDumpReplayer::SetLoopCount(s_loop_count); VMManager::SetState(VMState::Running); while (VMManager::GetState() == VMState::Running) VMManager::Execute(); VMManager::Shutdown(false); GSRunner::DumpStats(); } VMManager::Internal::CPUThreadShutdown(); GSRunner::DestroyPlatformWindow(); LogSink::CloseFileLog(); return EXIT_SUCCESS; } void Host::VSyncOnCPUThread() { // update GS thread copy of frame number MTGS::RunOnGSThread([frame_number = GSDumpReplayer::GetFrameNumber()]() { s_dump_frame_number = frame_number; }); MTGS::RunOnGSThread([loop_number = GSDumpReplayer::GetLoopCount()]() { s_loop_number = loop_number; }); // process any window messages (but we shouldn't really have any) GSRunner::PumpPlatformMessages(); } s32 Host::Internal::GetTranslatedStringImpl( const std::string_view& context, const std::string_view& msg, char* tbuf, size_t tbuf_space) { if (msg.size() > tbuf_space) return -1; else if (msg.empty()) return 0; std::memcpy(tbuf, msg.data(), msg.size()); return static_cast(msg.size()); } ////////////////////////////////////////////////////////////////////////// // Platform specific code ////////////////////////////////////////////////////////////////////////// #ifdef _WIN32 static constexpr LPCWSTR WINDOW_CLASS_NAME = L"PCSX2GSRunner"; static HWND s_hwnd = NULL; static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); bool GSRunner::CreatePlatformWindow() { WNDCLASSEXW wc = {}; wc.cbSize = sizeof(WNDCLASSEXW); wc.style = 0; wc.lpfnWndProc = WndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = GetModuleHandle(nullptr); wc.hIcon = NULL; wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); wc.lpszMenuName = NULL; wc.lpszClassName = WINDOW_CLASS_NAME; wc.hIconSm = NULL; if (!RegisterClassExW(&wc)) { Console.Error("Window registration failed."); return false; } s_hwnd = CreateWindowExW(WS_EX_CLIENTEDGE, WINDOW_CLASS_NAME, L"PCSX2 GS Runner", WS_OVERLAPPEDWINDOW | WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU | WS_SIZEBOX, CW_USEDEFAULT, CW_USEDEFAULT, WINDOW_WIDTH, WINDOW_HEIGHT, nullptr, nullptr, GetModuleHandleW(nullptr), nullptr); if (!s_hwnd) { Console.Error("CreateWindowEx failed."); return false; } ShowWindow(s_hwnd, SW_SHOW); UpdateWindow(s_hwnd); // make sure all messages are processed before returning PumpPlatformMessages(); return true; } void GSRunner::DestroyPlatformWindow() { if (!s_hwnd) return; PumpPlatformMessages(); DestroyWindow(s_hwnd); s_hwnd = {}; } std::optional GSRunner::GetPlatformWindowInfo() { WindowInfo wi; if (s_hwnd) { RECT rc = {}; GetWindowRect(s_hwnd, &rc); wi.surface_width = static_cast(rc.right - rc.left); wi.surface_height = static_cast(rc.bottom - rc.top); wi.surface_scale = 1.0f; wi.type = WindowInfo::Type::Win32; wi.window_handle = s_hwnd; } else { wi.type = WindowInfo::Type::Surfaceless; } return wi; } void GSRunner::PumpPlatformMessages() { MSG msg; while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessageW(&msg); } } LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { return DefWindowProcW(hwnd, msg, wParam, lParam); } int wmain(int argc, wchar_t** argv) { std::vector u8_args; u8_args.reserve(static_cast(argc)); for (int i = 0; i < argc; i++) u8_args.push_back(StringUtil::WideStringToUTF8String(argv[i])); std::vector u8_argptrs; u8_argptrs.reserve(u8_args.size()); for (int i = 0; i < argc; i++) u8_argptrs.push_back(u8_args[i].data()); u8_argptrs.push_back(nullptr); return real_main(argc, u8_argptrs.data()); } #endif // _WIN32