diff --git a/src/core/host_interface.cpp b/src/core/host_interface.cpp index 648f3ae0b..9e1943ada 100644 --- a/src/core/host_interface.cpp +++ b/src/core/host_interface.cpp @@ -61,6 +61,14 @@ void HostInterface::CreateAudioStream() bool HostInterface::BootSystem(const SystemBootParameters& parameters) { + if (parameters.filename.empty()) + Log_InfoPrintf("Boot Filename: "); + else + Log_InfoPrintf("Boot Filename: %s", parameters.filename.c_str()); + + if (!parameters.state_filename.empty()) + Log_InfoPrintf("Save State Filename: %s", parameters.filename.c_str()); + if (!AcquireHostDisplay()) { ReportFormattedError("Failed to acquire host display"); @@ -81,6 +89,9 @@ bool HostInterface::BootSystem(const SystemBootParameters& parameters) return false; } + if (!parameters.state_filename.empty()) + LoadState(parameters.state_filename.c_str()); + OnSystemCreated(); m_paused = m_settings.start_paused; @@ -611,6 +622,9 @@ void HostInterface::OnControllerTypeChanged(u32 slot) {} void HostInterface::SetUserDirectory() { + if (!m_user_directory.empty()) + return; + const std::string program_path = FileSystem::GetProgramPath(); const std::string program_directory = FileSystem::GetPathDirectory(program_path.c_str()); Log_InfoPrintf("Program path: \"%s\" (directory \"%s\")", program_path.c_str(), program_directory.c_str()); @@ -653,6 +667,13 @@ void HostInterface::SetUserDirectory() } } +void HostInterface::SetUserDirectoryToProgramDirectory() +{ + const std::string program_path = FileSystem::GetProgramPath(); + const std::string program_directory = FileSystem::GetPathDirectory(program_path.c_str()); + m_user_directory = program_directory; +} + void HostInterface::InitializeUserDirectory() { Log_InfoPrintf("User directory: \"%s\"", m_user_directory.c_str()); diff --git a/src/core/host_interface.h b/src/core/host_interface.h index 0eb7276a5..6050db5c9 100644 --- a/src/core/host_interface.h +++ b/src/core/host_interface.h @@ -50,10 +50,11 @@ public: /// Shuts down the emulator frontend. virtual void Shutdown(); - bool BootSystem(const SystemBootParameters& parameters); + virtual bool BootSystem(const SystemBootParameters& parameters); + virtual void PowerOffSystem(); + void PauseSystem(bool paused); void ResetSystem(); - void PowerOffSystem(); void DestroySystem(); /// Loads state from the specified filename. @@ -158,6 +159,9 @@ protected: /// Sets the base path for the user directory. Can be overridden by platform/frontend/command line. virtual void SetUserDirectory(); + /// Sets the user directory to the program directory, i.e. "portable mode". + void SetUserDirectoryToProgramDirectory(); + /// Performs the initial load of settings. Should call CheckSettings() and m_settings.Load(). virtual void LoadSettings() = 0; @@ -229,14 +233,14 @@ protected: Settings m_settings; std::string m_user_directory; + std::deque m_osd_messages; + std::mutex m_osd_messages_lock; + bool m_paused = false; bool m_speed_limiter_temp_disabled = false; bool m_speed_limiter_enabled = false; bool m_timer_resolution_increased = false; - std::deque m_osd_messages; - std::mutex m_osd_messages_lock; - private: void InitializeUserDirectory(); void CreateAudioStream(); diff --git a/src/core/system.h b/src/core/system.h index f6988b715..4e0ed4408 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -35,7 +35,9 @@ struct SystemBootParameters ~SystemBootParameters(); std::string filename; + std::string state_filename; std::optional override_fast_boot; + std::optional override_fullscreen; }; class System diff --git a/src/duckstation-qt/main.cpp b/src/duckstation-qt/main.cpp index 3b8053ab2..7786decae 100644 --- a/src/duckstation-qt/main.cpp +++ b/src/duckstation-qt/main.cpp @@ -3,12 +3,13 @@ #include "qthostinterface.h" #include #include +#include #include static void InitLogging() { // set log flags -#ifdef Y_BUILD_CONFIG_DEBUG +#ifdef _DEBUG Log::SetConsoleOutputParams(true, nullptr, LOGLEVEL_DEBUG); Log::SetFilterLevel(LOGLEVEL_DEBUG); #else @@ -35,18 +36,30 @@ int main(int argc, char* argv[]) #endif std::unique_ptr host_interface = std::make_unique(); + std::unique_ptr boot_params; + if (!host_interface->parseCommandLineParameters(argc, argv, &boot_params)) + return EXIT_FAILURE; + if (!host_interface->Initialize()) { host_interface->Shutdown(); QMessageBox::critical(nullptr, QObject::tr("DuckStation Error"), QObject::tr("Failed to initialize host interface. Cannot continue."), QMessageBox::Ok); - return -1; + return EXIT_FAILURE; } std::unique_ptr window = std::make_unique(host_interface.get()); window->show(); - host_interface->refreshGameList(); + // if we're in batch mode, don't bother refreshing the game list as it won't be used + if (!host_interface->inBatchMode()) + host_interface->refreshGameList(); + + if (boot_params) + { + host_interface->bootSystem(*boot_params); + boot_params.reset(); + } int result = app.exec(); diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index 53b01187d..bf2c9aec8 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -528,6 +528,7 @@ void MainWindow::connectSignals() connect(m_host_interface, &QtHostInterface::systemPerformanceCountersUpdated, this, &MainWindow::onSystemPerformanceCountersUpdated); connect(m_host_interface, &QtHostInterface::runningGameChanged, this, &MainWindow::onRunningGameChanged); + connect(m_host_interface, &QtHostInterface::exitRequested, this, &MainWindow::close); // These need to be queued connections to stop crashing due to menus opening/closing and switching focus. connect(m_game_list_widget, &GameListWidget::entrySelected, this, &MainWindow::onGameListEntrySelected, diff --git a/src/duckstation-qt/qthostinterface.cpp b/src/duckstation-qt/qthostinterface.cpp index 7c29bf3e8..f1dfe9bd8 100644 --- a/src/duckstation-qt/qthostinterface.cpp +++ b/src/duckstation-qt/qthostinterface.cpp @@ -39,6 +39,11 @@ QtHostInterface::~QtHostInterface() Assert(!m_display_widget); } +const char* QtHostInterface::GetFrontendName() const +{ + return "DuckStation Qt Frontend"; +} + bool QtHostInterface::Initialize() { createThread(); @@ -104,6 +109,12 @@ bool QtHostInterface::ConfirmMessage(const char* message) return result; } +bool QtHostInterface::parseCommandLineParameters(int argc, char* argv[], + std::unique_ptr* out_boot_params) +{ + return CommonHostInterface::ParseCommandLineParameters(argc, argv, out_boot_params); +} + QVariant QtHostInterface::getSettingValue(const QString& name, const QVariant& default_value) { std::lock_guard guard(m_qsettings_mutex); @@ -320,6 +331,11 @@ bool QtHostInterface::SetFullscreen(bool enabled) return true; } +void QtHostInterface::RequestExit() +{ + emit exitRequested(); +} + std::optional QtHostInterface::GetHostKeyCode(const std::string_view key_code) const { const std::optional code = diff --git a/src/duckstation-qt/qthostinterface.h b/src/duckstation-qt/qthostinterface.h index 4d15939c8..bc80f073d 100644 --- a/src/duckstation-qt/qthostinterface.h +++ b/src/duckstation-qt/qthostinterface.h @@ -1,7 +1,7 @@ #pragma once +#include "common/event.h" #include "core/host_interface.h" #include "core/system.h" -#include "common/event.h" #include "frontend-common/common_host_interface.h" #include #include @@ -37,6 +37,8 @@ public: explicit QtHostInterface(QObject* parent = nullptr); ~QtHostInterface(); + const char* GetFrontendName() const override; + bool Initialize() override; void Shutdown() override; @@ -44,6 +46,8 @@ public: void ReportMessage(const char* message) override; bool ConfirmMessage(const char* message) override; + bool parseCommandLineParameters(int argc, char* argv[], std::unique_ptr* out_boot_params); + /// Thread-safe QSettings access. QVariant getSettingValue(const QString& name, const QVariant& default_value = QVariant()); void putSettingValue(const QString& name, const QVariant& value); @@ -55,6 +59,7 @@ public: ALWAYS_INLINE const HotkeyInfoList& getHotkeyInfoList() const { return GetHotkeyInfoList(); } ALWAYS_INLINE ControllerInterface* getControllerInterface() const { return GetControllerInterface(); } + ALWAYS_INLINE bool inBatchMode() const { return InBatchMode(); } ALWAYS_INLINE bool isOnWorkerThread() const { return QThread::currentThread() == m_worker_thread; } @@ -84,6 +89,7 @@ Q_SIGNALS: void systemPerformanceCountersUpdated(float speed, float fps, float vps, float avg_frame_time, float worst_frame_time); void runningGameChanged(const QString& filename, const QString& game_code, const QString& game_title); + void exitRequested(); public Q_SLOTS: void setDefaultSettings(); @@ -124,6 +130,7 @@ protected: bool IsFullscreen() const override; bool SetFullscreen(bool enabled) override; + void RequestExit() override; std::optional GetHostKeyCode(const std::string_view key_code) const override; void OnSystemCreated() override; @@ -161,7 +168,7 @@ private: private: QtHostInterface* m_parent; - std::atomic_bool m_init_result{ false }; + std::atomic_bool m_init_result{false}; Common::Event m_init_event; }; diff --git a/src/duckstation-sdl/main.cpp b/src/duckstation-sdl/main.cpp index d5f81d75c..5cee36f6a 100644 --- a/src/duckstation-sdl/main.cpp +++ b/src/duckstation-sdl/main.cpp @@ -5,70 +5,8 @@ #include "sdl_host_interface.h" #include #include +#include -static int Run(int argc, char* argv[]) -{ - // parameters - std::optional state_index; - bool state_is_global = false; - const char* boot_filename = nullptr; - for (int i = 1; i < argc; i++) - { -#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_PARAM("-state") || CHECK_ARG_PARAM("-gstate")) - { - state_is_global = argv[i][1] == 'g'; - state_index = std::atoi(argv[++i]); - } - else if (CHECK_ARG_PARAM("-resume")) - { - state_index = -1; - } - else - { - boot_filename = argv[i]; - } - -#undef CHECK_ARG -#undef CHECK_ARG_PARAM - } - - // create display and host interface - std::unique_ptr host_interface = SDLHostInterface::Create(); - if (!host_interface->Initialize()) - { - host_interface->Shutdown(); - Panic("Failed to initialize host interface"); - SDL_Quit(); - return -1; - } - - // boot/load state - if (boot_filename) - { - SystemBootParameters boot_params; - boot_params.filename = boot_filename; - if (host_interface->BootSystem(boot_params) && state_index.has_value()) - host_interface->LoadState(state_is_global, state_index.value()); - } - else if (state_index.has_value()) - { - host_interface->LoadState(true, state_index.value()); - } - - // run - host_interface->Run(); - - // done - host_interface->Shutdown(); - host_interface.reset(); - SDL_Quit(); - return 0; -} - -// SDL requires the entry point declared without c++ decoration #undef main int main(int argc, char* argv[]) { @@ -91,6 +29,38 @@ int main(int argc, char* argv[]) FrontendCommon::EnsureSDLInitialized(); - // return NoGUITest(); - return Run(argc, argv); + std::unique_ptr host_interface = SDLHostInterface::Create(); + std::unique_ptr boot_params; + if (!host_interface->ParseCommandLineParameters(argc, argv, &boot_params)) + { + SDL_Quit(); + return EXIT_FAILURE; + } + + if (!host_interface->Initialize()) + { + host_interface->Shutdown(); + SDL_Quit(); + return EXIT_FAILURE; + } + + if (boot_params) + { + if (!host_interface->BootSystem(*boot_params) && host_interface->InBatchMode()) + { + host_interface->Shutdown(); + host_interface.reset(); + SDL_Quit(); + return EXIT_FAILURE; + } + + boot_params.reset(); + } + + host_interface->Run(); + host_interface->Shutdown(); + host_interface.reset(); + + SDL_Quit(); + return EXIT_SUCCESS; } diff --git a/src/duckstation-sdl/sdl_host_interface.cpp b/src/duckstation-sdl/sdl_host_interface.cpp index 5d0b904f8..52a08c69b 100644 --- a/src/duckstation-sdl/sdl_host_interface.cpp +++ b/src/duckstation-sdl/sdl_host_interface.cpp @@ -34,6 +34,11 @@ SDLHostInterface::SDLHostInterface() SDLHostInterface::~SDLHostInterface() = default; +const char* SDLHostInterface::GetFrontendName() const +{ + return "DuckStation SDL/ImGui Frontend"; +} + float SDLHostInterface::GetDPIScaleFactor(SDL_Window* window) { #ifdef __APPLE__ @@ -191,10 +196,6 @@ bool SDLHostInterface::AcquireHostDisplay() } #endif - // Switch to fullscreen if requested. - if (m_settings.start_fullscreen) - SetFullscreen(true); - return true; } @@ -274,6 +275,11 @@ void SDLHostInterface::OnRunningGameChanged() SDL_SetWindowTitle(m_window, "DuckStation"); } +void SDLHostInterface::RequestExit() +{ + m_quit_request = true; +} + void SDLHostInterface::RunLater(std::function callback) { SDL_Event ev = {}; diff --git a/src/duckstation-sdl/sdl_host_interface.h b/src/duckstation-sdl/sdl_host_interface.h index 59c3634fe..df60ebcaf 100644 --- a/src/duckstation-sdl/sdl_host_interface.h +++ b/src/duckstation-sdl/sdl_host_interface.h @@ -25,6 +25,8 @@ public: static std::unique_ptr Create(); + const char* GetFrontendName() const override; + void ReportError(const char* message) override; void ReportMessage(const char* message) override; bool ConfirmMessage(const char* message) override; @@ -47,6 +49,8 @@ protected: void OnSystemDestroyed() override; void OnRunningGameChanged() override; + void RequestExit() override; + std::optional GetHostKeyCode(const std::string_view key_code) const override; void UpdateInputMap() override; diff --git a/src/frontend-common/common_host_interface.cpp b/src/frontend-common/common_host_interface.cpp index ab6b6445b..bf44bcef3 100644 --- a/src/frontend-common/common_host_interface.cpp +++ b/src/frontend-common/common_host_interface.cpp @@ -13,6 +13,7 @@ #include "sdl_audio_stream.h" #include "sdl_controller_interface.h" #endif +#include #include Log_SetChannel(CommonHostInterface); @@ -63,6 +64,228 @@ void CommonHostInterface::Shutdown() } } +bool CommonHostInterface::BootSystem(const SystemBootParameters& parameters) +{ + if (!HostInterface::BootSystem(parameters)) + { + // if in batch mode, exit immediately if booting failed + if (m_batch_mode) + RequestExit(); + + return false; + } + + // enter fullscreen if requested in the parameters + if ((parameters.override_fullscreen.has_value() && *parameters.override_fullscreen) || + (!parameters.override_fullscreen.has_value() && m_settings.start_fullscreen)) + { + SetFullscreen(true); + } + + return true; +} + +void CommonHostInterface::PowerOffSystem() +{ + HostInterface::PowerOffSystem(); + + // TODO: Do we want to move the resume state saving here? + + if (m_batch_mode) + RequestExit(); +} + +static void PrintCommandLineVersion(const char* frontend_name) +{ + std::fprintf(stderr, "%s version \n", frontend_name); + std::fprintf(stderr, "https://github.com/stenzek/duckstation\n"); + std::fprintf(stderr, "\n"); +} + +static void PrintCommandLineHelp(const char* progname, const char* frontend_name) +{ + PrintCommandLineVersion(frontend_name); + std::fprintf(stderr, "Usage: %s [parameters] [--] [boot 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, " -batch: Enables batch mode (exits after powering off)\n"); + std::fprintf(stderr, " -fastboot: Force fast boot for provided filename\n"); + std::fprintf(stderr, " -slowboot: Force slow boot for provided filename\n"); + std::fprintf(stderr, " -resume: Load resume save state. If a boot filename is provided,\n" + " that game's resume state will be loaded, otherwise the most\n" + " recent resume save state will be loaded.\n"); + std::fprintf(stderr, " -state : Loads specified save state by index. If a boot\n" + " filename is provided, a per-game state will be loaded, otherwise\n" + " a global state will be loaded.\n"); + std::fprintf(stderr, " -statefile : Loads state from the specified filename.\n" + " No boot filename is required with this option.\n"); + std::fprintf(stderr, " -fullscreen: Enters fullscreen mode immediately after starting.\n"); + std::fprintf(stderr, " -nofullscreen: Prevents fullscreen mode from triggering if enabled.\n"); + std::fprintf(stderr, " -portable: Forces \"portable mode\", data in same directory.\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"); +} + +bool CommonHostInterface::ParseCommandLineParameters(int argc, char* argv[], + std::unique_ptr* out_boot_params) +{ + std::optional force_fast_boot; + std::optional force_fullscreen; + std::optional state_index; + std::string state_filename; + std::string boot_filename; + 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], GetFrontendName()); + return false; + } + else if (CHECK_ARG("-version")) + { + PrintCommandLineVersion(GetFrontendName()); + return false; + } + else if (CHECK_ARG("-batch")) + { + Log_InfoPrintf("Enabling batch mode.\n"); + m_batch_mode = true; + continue; + } + else if (CHECK_ARG("-fastboot")) + { + Log_InfoPrintf("Forcing fast boot.\n"); + force_fast_boot = true; + continue; + } + else if (CHECK_ARG("-slowboot")) + { + Log_InfoPrintf("Forcing slow boot.\n"); + force_fast_boot = false; + continue; + } + else if (CHECK_ARG("-resume")) + { + state_index = -1; + continue; + } + else if (CHECK_ARG_PARAM("-state")) + { + state_index = std::atoi(argv[++i]); + continue; + } + else if (CHECK_ARG_PARAM("-statefile")) + { + state_filename = argv[++i]; + continue; + } + else if (CHECK_ARG("-fullscreen")) + { + Log_InfoPrintf("Going fullscreen after booting.\n"); + force_fullscreen = true; + continue; + } + else if (CHECK_ARG("-nofullscreen")) + { + Log_InfoPrintf("Preventing fullscreen after booting.\n"); + force_fullscreen = false; + continue; + } + else if (CHECK_ARG("-portable")) + { + Log_InfoPrintf("Using portable mode.\n"); + SetUserDirectoryToProgramDirectory(); + continue; + } + else if (CHECK_ARG_PARAM("-resume")) + { + state_index = -1; + continue; + } + else if (CHECK_ARG("--")) + { + no_more_args = true; + continue; + } + else if (argv[i][0] == '-') + { + Log_ErrorPrintf("Unknown parameter: '%s'", argv[i]); + return false; + } + +#undef CHECK_ARG +#undef CHECK_ARG_PARAM + } + + if (!boot_filename.empty()) + boot_filename += ' '; + boot_filename += argv[i]; + } + + if (state_index.has_value() || !boot_filename.empty() || !state_filename.empty()) + { + // init user directory early since we need it for save states + SetUserDirectory(); + + if (state_index.has_value() && !state_filename.empty()) + { + // if a save state is provided, whether a boot filename was provided determines per-game/local + if (boot_filename.empty()) + { + // loading a global state. if this is -1, we're loading the most recent resume state + if (*state_index < 0) + state_filename = GetMostRecentResumeSaveStatePath(); + else + state_filename = GetGlobalSaveStateFileName(*state_index); + + if (state_filename.empty() || !FileSystem::FileExists(state_filename.c_str())) + { + Log_ErrorPrintf("Could not find file for global save state %d", *state_index); + return false; + } + } + else + { + // find the game id, and get its save state path + std::string game_code = m_game_list->GetGameCodeForPath(boot_filename.c_str()); + if (game_code.empty()) + { + Log_WarningPrintf("Could not identify game code for '%s', cannot load save state %d.", boot_filename.c_str(), + *state_index); + } + else + { + state_filename = GetGameSaveStateFileName(game_code.c_str(), *state_index); + if (state_filename.empty() || !FileSystem::FileExists(state_filename.c_str())) + { + Log_ErrorPrintf("Could not find file for game '%s' save state %d", game_code.c_str(), *state_index); + return false; + } + } + } + } + + std::unique_ptr boot_params = std::make_unique(); + boot_params->filename = std::move(boot_filename); + boot_params->state_filename = std::move(state_filename); + boot_params->override_fast_boot = std::move(force_fast_boot); + boot_params->override_fullscreen = std::move(force_fullscreen); + *out_boot_params = std::move(boot_params); + } + + return true; +} + bool CommonHostInterface::IsFullscreen() const { return false; @@ -401,7 +624,7 @@ void CommonHostInterface::RegisterGeneralHotkeys() [this](bool pressed) { if (!pressed && m_system) { - if (m_settings.confim_power_off) + if (m_settings.confim_power_off && !m_batch_mode) { SmallString confirmation_message("Are you sure you want to stop emulation?"); if (m_settings.save_state_on_exit) diff --git a/src/frontend-common/common_host_interface.h b/src/frontend-common/common_host_interface.h index 618fd3807..4b4ea51bc 100644 --- a/src/frontend-common/common_host_interface.h +++ b/src/frontend-common/common_host_interface.h @@ -33,19 +33,34 @@ public: using HotkeyInfoList = std::vector; + /// Returns the name of the frontend. + virtual const char* GetFrontendName() const = 0; + virtual bool Initialize() override; virtual void Shutdown() override; + virtual bool BootSystem(const SystemBootParameters& parameters) override; + virtual void PowerOffSystem() override; + /// Returns a list of all available hotkeys. ALWAYS_INLINE const HotkeyInfoList& GetHotkeyInfoList() const { return m_hotkeys; } /// Access to current controller interface. ALWAYS_INLINE ControllerInterface* GetControllerInterface() const { return m_controller_interface.get(); } + /// Returns true if running in batch mode, i.e. exit after emulation. + ALWAYS_INLINE bool InBatchMode() const { return m_batch_mode; } + + /// Parses command line parameters for all frontends. + bool ParseCommandLineParameters(int argc, char* argv[], std::unique_ptr* out_boot_params); + protected: CommonHostInterface(); ~CommonHostInterface(); + /// Request the frontend to exit. + virtual void RequestExit() = 0; + virtual bool IsFullscreen() const; virtual bool SetFullscreen(bool enabled); @@ -85,4 +100,7 @@ private: // input key maps std::map m_keyboard_input_handlers; + + // running in batch mode? i.e. exit after stopping emulation + bool m_batch_mode = false; };