From cb2dfabeebae799524c4303f36a5b0772b9deb2c Mon Sep 17 00:00:00 2001 From: Stenzek Date: Sun, 8 Jun 2025 17:08:33 +1000 Subject: [PATCH] Qt: Backport undo load state timestamps --- src/core/system.cpp | 16 ++++++++++--- src/core/system_private.h | 7 ++++-- src/duckstation-mini/mini_host.cpp | 9 ++++++-- src/duckstation-qt/mainwindow.cpp | 29 ++++++++++++++++++------ src/duckstation-qt/mainwindow.h | 6 +++-- src/duckstation-qt/qthost.cpp | 13 +++++++---- src/duckstation-qt/qthost.h | 3 ++- src/duckstation-regtest/regtest_host.cpp | 11 ++++++--- 8 files changed, 70 insertions(+), 24 deletions(-) diff --git a/src/core/system.cpp b/src/core/system.cpp index 0e672c991..be5e8384c 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -137,6 +137,11 @@ struct SaveStateBuffer size_t state_size; }; +struct UndoSaveStateBuffer : public SaveStateBuffer +{ + time_t timestamp; +}; + } // namespace static void CheckCacheLineSize(); @@ -318,7 +323,7 @@ struct ALIGN_TO_CACHE_LINE StateVars Threading::ThreadHandle cpu_thread_handle; // temporary save state, created when loading, used to undo load state - std::optional undo_load_state; + std::optional undo_load_state; // Used to track play time. We use a monotonic timer here, in case of clock changes. u64 session_start_time = 0; @@ -2024,7 +2029,7 @@ void System::ClearRunningGame() s_state.running_game_entry = nullptr; s_state.running_game_hash = 0; - Host::OnGameChanged(s_state.running_game_path, s_state.running_game_serial, s_state.running_game_title, + Host::OnSystemGameChanged(s_state.running_game_path, s_state.running_game_serial, s_state.running_game_title, s_state.running_game_hash); UpdateRichPresence(true); @@ -4212,7 +4217,7 @@ void System::UpdateRunningGame(const std::string& path, CDImage* image, bool boo FullscreenUI::OnRunningGameChanged(s_state.running_game_path, s_state.running_game_serial, s_state.running_game_title, s_state.running_game_hash); - Host::OnGameChanged(s_state.running_game_path, s_state.running_game_serial, s_state.running_game_title, + Host::OnSystemGameChanged(s_state.running_game_path, s_state.running_game_serial, s_state.running_game_title, s_state.running_game_hash); } @@ -5282,12 +5287,14 @@ bool System::UndoLoadState() Host::ReportErrorAsync("Error", fmt::format("Failed to load undo state, resetting system:\n", error.GetDescription())); s_state.undo_load_state.reset(); + Host::OnSystemUndoStateAvailabilityChanged(false, 0); ResetSystem(); return false; } INFO_LOG("Loaded undo save state."); s_state.undo_load_state.reset(); + Host::OnSystemUndoStateAvailabilityChanged(false, 0); return true; } @@ -5303,10 +5310,13 @@ bool System::SaveUndoLoadState() fmt::format(TRANSLATE_FS("OSDMessage", "Failed to save undo load state:\n{}"), error.GetDescription()), Host::OSD_CRITICAL_ERROR_DURATION); s_state.undo_load_state.reset(); + Host::OnSystemUndoStateAvailabilityChanged(false, 0); return false; } INFO_LOG("Saved undo load state: {} bytes", s_state.undo_load_state->state_size); + s_state.undo_load_state->timestamp = std::time(nullptr); + Host::OnSystemUndoStateAvailabilityChanged(true, static_cast(s_state.undo_load_state->timestamp)); return true; } diff --git a/src/core/system_private.h b/src/core/system_private.h index 367fc227f..730720386 100644 --- a/src/core/system_private.h +++ b/src/core/system_private.h @@ -115,8 +115,11 @@ void OnSystemAbnormalShutdown(const std::string_view reason); void OnPerformanceCountersUpdated(const GPUBackend* gpu_backend); /// Provided by the host; called when the running executable changes. -void OnGameChanged(const std::string& disc_path, const std::string& game_serial, const std::string& game_name, - GameHash game_hash); +void OnSystemGameChanged(const std::string& disc_path, const std::string& game_serial, const std::string& game_name, + GameHash game_hash); + +/// Provided by the host; called when the undo save state availability changes. +void OnSystemUndoStateAvailabilityChanged(bool available, u64 timestamp); /// Called when media capture starts/stops. void OnMediaCaptureStarted(); diff --git a/src/duckstation-mini/mini_host.cpp b/src/duckstation-mini/mini_host.cpp index c73e97a03..14bdb47c6 100644 --- a/src/duckstation-mini/mini_host.cpp +++ b/src/duckstation-mini/mini_host.cpp @@ -1185,8 +1185,8 @@ void MiniHost::WarnAboutInterface() Host::AddIconOSDWarning("MiniWarning", ICON_EMOJI_WARNING, message, Host::OSD_INFO_DURATION); } -void Host::OnGameChanged(const std::string& disc_path, const std::string& game_serial, const std::string& game_name, - GameHash game_hash) +void Host::OnSystemGameChanged(const std::string& disc_path, const std::string& game_serial, + const std::string& game_name, GameHash game_hash) { using namespace MiniHost; @@ -1195,6 +1195,11 @@ void Host::OnGameChanged(const std::string& disc_path, const std::string& game_s SDL_SetWindowTitle(s_state.sdl_window, GetWindowTitle(game_name).c_str()); } +void Host::OnSystemUndoStateAvailabilityChanged(bool available, u64 timestamp) +{ + // +} + void Host::RunOnCPUThread(std::function function, bool block /* = false */) { using namespace MiniHost; diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index 043c6d0ce..07a539f77 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -113,6 +113,7 @@ static QString s_current_game_title; static QString s_current_game_serial; static QString s_current_game_path; static QIcon s_current_game_icon; +static std::optional s_undo_state_timestamp; bool QtHost::IsSystemPaused() { @@ -576,6 +577,7 @@ void MainWindow::onSystemDestroyed() s_system_starting = false; s_system_valid = false; s_system_paused = false; + s_undo_state_timestamp.reset(); // If we're closing or in batch mode, quit the whole application now. if (m_is_closing || QtHost::InBatchMode()) @@ -593,7 +595,7 @@ void MainWindow::onSystemDestroyed() switchToGameListView(); } -void MainWindow::onRunningGameChanged(const QString& filename, const QString& game_serial, const QString& game_title) +void MainWindow::onSystemGameChanged(const QString& filename, const QString& game_serial, const QString& game_title) { s_current_game_path = filename; s_current_game_title = game_title; @@ -603,6 +605,14 @@ void MainWindow::onRunningGameChanged(const QString& filename, const QString& ga updateWindowTitle(); } +void MainWindow::onSystemUndoStateAvailabilityChanged(bool available, quint64 timestamp) +{ + if (!available) + s_undo_state_timestamp.reset(); + else + s_undo_state_timestamp = timestamp; +} + void MainWindow::onMediaCaptureStarted() { QSignalBlocker sb(m_ui.actionMediaCapture); @@ -891,7 +901,7 @@ void MainWindow::populateGameListContextMenu(const GameList::Entry* entry, QWidg } } -static QString FormatTimestampForSaveStateMenu(u64 timestamp) +QString MainWindow::formatTimestampForSaveStateMenu(u64 timestamp) { const QDateTime qtime(QDateTime::fromSecsSinceEpoch(static_cast(timestamp))); return qtime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat)); @@ -904,7 +914,7 @@ void MainWindow::populateLoadStateMenu(std::string_view game_serial, QMenu* menu std::optional ssi = System::GetSaveStateInfo(serial, slot); const QString menu_title = - ssi.has_value() ? title.arg(slot).arg(FormatTimestampForSaveStateMenu(ssi->timestamp)) : empty_title.arg(slot); + ssi.has_value() ? title.arg(slot).arg(formatTimestampForSaveStateMenu(ssi->timestamp)) : empty_title.arg(slot); QAction* load_action = menu->addAction(menu_title); load_action->setEnabled(ssi.has_value()); @@ -925,8 +935,11 @@ void MainWindow::populateLoadStateMenu(std::string_view game_serial, QMenu* menu g_emu_thread->loadState(path); }); - QAction* load_from_state = menu->addAction(tr("Undo Load State")); - load_from_state->setEnabled(System::CanUndoLoadState()); + QAction* load_from_state = + menu->addAction(s_undo_state_timestamp.has_value() ? + tr("Undo Load State (%1)").arg(formatTimestampForSaveStateMenu(s_undo_state_timestamp.value())) : + tr("Undo Load State")); + load_from_state->setEnabled(s_undo_state_timestamp.has_value()); connect(load_from_state, &QAction::triggered, g_emu_thread, &EmuThread::undoLoadState); menu->addSeparator(); @@ -949,7 +962,7 @@ void MainWindow::populateSaveStateMenu(std::string_view game_serial, QMenu* menu std::optional ssi = System::GetSaveStateInfo(serial, slot); const QString menu_title = - ssi.has_value() ? title.arg(slot).arg(FormatTimestampForSaveStateMenu(ssi->timestamp)) : empty_title.arg(slot); + ssi.has_value() ? title.arg(slot).arg(formatTimestampForSaveStateMenu(ssi->timestamp)) : empty_title.arg(slot); QAction* save_action = menu->addAction(menu_title); connect(save_action, &QAction::triggered, @@ -2123,7 +2136,9 @@ void MainWindow::connectSignals() connect(g_emu_thread, &EmuThread::systemDestroyed, this, &MainWindow::onSystemDestroyed); connect(g_emu_thread, &EmuThread::systemPaused, this, &MainWindow::onSystemPaused); connect(g_emu_thread, &EmuThread::systemResumed, this, &MainWindow::onSystemResumed); - connect(g_emu_thread, &EmuThread::runningGameChanged, this, &MainWindow::onRunningGameChanged); + connect(g_emu_thread, &EmuThread::systemGameChanged, this, &MainWindow::onSystemGameChanged); + connect(g_emu_thread, &EmuThread::systemUndoStateAvailabilityChanged, this, + &MainWindow::onSystemUndoStateAvailabilityChanged); connect(g_emu_thread, &EmuThread::mediaCaptureStarted, this, &MainWindow::onMediaCaptureStarted); connect(g_emu_thread, &EmuThread::mediaCaptureStopped, this, &MainWindow::onMediaCaptureStopped); connect(g_emu_thread, &EmuThread::mouseModeRequested, this, &MainWindow::onMouseModeRequested); diff --git a/src/duckstation-qt/mainwindow.h b/src/duckstation-qt/mainwindow.h index 28598d38e..e4180f25b 100644 --- a/src/duckstation-qt/mainwindow.h +++ b/src/duckstation-qt/mainwindow.h @@ -146,7 +146,8 @@ private Q_SLOTS: void onSystemDestroyed(); void onSystemPaused(); void onSystemResumed(); - void onRunningGameChanged(const QString& filename, const QString& game_serial, const QString& game_title); + void onSystemGameChanged(const QString& filename, const QString& game_serial, const QString& game_title); + void onSystemUndoStateAvailabilityChanged(bool available, quint64 timestamp); void onMediaCaptureStarted(); void onMediaCaptureStopped(); void onAchievementsLoginRequested(Achievements::LoginRequestReason reason); @@ -215,7 +216,6 @@ private Q_SLOTS: void onDebugLogChannelsMenuAboutToShow(); void openCPUDebugger(); - protected: void showEvent(QShowEvent* event) override; void closeEvent(QCloseEvent* event) override; @@ -296,6 +296,8 @@ private: void startFileOrChangeDisc(const QString& path); void promptForDiscChange(const QString& path); + static QString formatTimestampForSaveStateMenu(u64 timestamp); + Ui::MainWindow m_ui; GameListWidget* m_game_list_widget = nullptr; diff --git a/src/duckstation-qt/qthost.cpp b/src/duckstation-qt/qthost.cpp index 6094986d3..f958a665c 100644 --- a/src/duckstation-qt/qthost.cpp +++ b/src/duckstation-qt/qthost.cpp @@ -2517,11 +2517,16 @@ void Host::OnPerformanceCountersUpdated(const GPUBackend* gpu_backend) g_emu_thread->updatePerformanceCounters(gpu_backend); } -void Host::OnGameChanged(const std::string& disc_path, const std::string& game_serial, const std::string& game_name, - GameHash hash) +void Host::OnSystemGameChanged(const std::string& disc_path, const std::string& game_serial, + const std::string& game_name, GameHash hash) { - emit g_emu_thread->runningGameChanged(QString::fromStdString(disc_path), QString::fromStdString(game_serial), - QString::fromStdString(game_name)); + emit g_emu_thread->systemGameChanged(QString::fromStdString(disc_path), QString::fromStdString(game_serial), + QString::fromStdString(game_name)); +} + +void Host::OnSystemUndoStateAvailabilityChanged(bool available, u64 timestamp) +{ + emit g_emu_thread->systemUndoStateAvailabilityChanged(available, timestamp); } void Host::OnMediaCaptureStarted() diff --git a/src/duckstation-qt/qthost.h b/src/duckstation-qt/qthost.h index ba68210c3..83c00a9ec 100644 --- a/src/duckstation-qt/qthost.h +++ b/src/duckstation-qt/qthost.h @@ -140,6 +140,8 @@ Q_SIGNALS: void systemDestroyed(); void systemPaused(); void systemResumed(); + void systemGameChanged(const QString& filename, const QString& game_serial, const QString& game_title); + void systemUndoStateAvailabilityChanged(bool available, quint64 timestamp); void gameListRefreshed(); void gameListRowsChanged(const QList& rows_changed); std::optional onAcquireRenderWindowRequested(RenderAPI render_api, bool fullscreen, @@ -148,7 +150,6 @@ Q_SIGNALS: void onResizeRenderWindowRequested(qint32 width, qint32 height); void onReleaseRenderWindowRequested(); void focusDisplayWidgetRequested(); - void runningGameChanged(const QString& filename, const QString& game_serial, const QString& game_title); void inputProfileLoaded(); void mouseModeRequested(bool relative, bool hide_cursor); void fullscreenUIStartedOrStopped(bool running); diff --git a/src/duckstation-regtest/regtest_host.cpp b/src/duckstation-regtest/regtest_host.cpp index 01d38095c..e5926090b 100644 --- a/src/duckstation-regtest/regtest_host.cpp +++ b/src/duckstation-regtest/regtest_host.cpp @@ -316,14 +316,19 @@ void Host::OnPerformanceCountersUpdated(const GPUBackend* gpu_backend) // } -void Host::OnGameChanged(const std::string& disc_path, const std::string& game_serial, const std::string& game_name, - GameHash hash) +void Host::OnSystemGameChanged(const std::string& disc_path, const std::string& game_serial, + const std::string& game_name, GameHash hash) { INFO_LOG("Disc Path: {}", disc_path); INFO_LOG("Game Serial: {}", game_serial); INFO_LOG("Game Name: {}", game_name); } +void Host::OnSystemUndoStateAvailabilityChanged(bool available, u64 timestamp) +{ + // +} + void Host::OnMediaCaptureStarted() { // @@ -890,7 +895,7 @@ bool RegTestHost::ParseCommandLineParameters(int argc, char* argv[], std::option INFO_LOG("Setting CPU execution mode to {}.", Settings::GetCPUExecutionModeName(cpu.value())); s_base_settings_interface.SetStringValue("CPU", "ExecutionMode", - Settings::GetCPUExecutionModeName(cpu.value())); + Settings::GetCPUExecutionModeName(cpu.value())); continue; } else if (CHECK_ARG("-pgxp"))