From 803241c64bf7d227bf203d1ea89f46a8f864c08b Mon Sep 17 00:00:00 2001 From: Aneesh Maganti <28660350+aminoa@users.noreply.github.com> Date: Mon, 10 Feb 2025 15:28:57 -0500 Subject: [PATCH 1/6] Core: Add TimePlayed class to track game playtime Creates TimePlayed class and implemented constructors, AddTime, GetTimePlayed, and Reload methods. Updates CMakeLists.txt and DolphinLib.props as appropriate. --- Source/Core/Core/CMakeLists.txt | 2 + Source/Core/Core/TimePlayed.cpp | 66 +++++++++++++++++++++++++++++++++ Source/Core/Core/TimePlayed.h | 40 ++++++++++++++++++++ Source/Core/DolphinLib.props | 2 + 4 files changed, 110 insertions(+) create mode 100644 Source/Core/Core/TimePlayed.cpp create mode 100644 Source/Core/Core/TimePlayed.h diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index 33a97d05e7..b4437747e8 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -539,6 +539,8 @@ add_library(core SysConf.h System.cpp System.h + TimePlayed.cpp + TimePlayed.h TitleDatabase.cpp TitleDatabase.h WC24PatchEngine.cpp diff --git a/Source/Core/Core/TimePlayed.cpp b/Source/Core/Core/TimePlayed.cpp new file mode 100644 index 0000000000..e20a0c014e --- /dev/null +++ b/Source/Core/Core/TimePlayed.cpp @@ -0,0 +1,66 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/TimePlayed.h" + +#include +#include + +#include "Common/CommonTypes.h" +#include "Common/FileUtil.h" +#include "Common/IniFile.h" +#include "Common/NandPaths.h" + +TimePlayed::TimePlayed() + : m_game_id(""), m_ini_path(File::GetUserPath(D_CONFIG_IDX) + "TimePlayed.ini") +{ + Reload(); +} + +TimePlayed::TimePlayed(std::string game_id) + : m_game_id(Common::EscapeFileName(game_id)), // filter for unsafe characters + m_ini_path(File::GetUserPath(D_CONFIG_IDX) + "TimePlayed.ini") +{ + Reload(); +} + +TimePlayed::~TimePlayed() = default; + +void TimePlayed::AddTime(std::chrono::milliseconds time_emulated) +{ + if (m_game_id == "") + { + return; + } + + u64 previous_time; + m_time_list->Get(m_game_id, &previous_time); + m_time_list->Set(m_game_id, previous_time + static_cast(time_emulated.count())); + m_ini.Save(m_ini_path); +} + +std::chrono::milliseconds TimePlayed::GetTimePlayed() const +{ + if (m_game_id == "") + { + return std::chrono::milliseconds(0); + } + + u64 previous_time; + m_time_list->Get(m_game_id, &previous_time); + return std::chrono::milliseconds(previous_time); +} + +std::chrono::milliseconds TimePlayed::GetTimePlayed(std::string game_id) const +{ + std::string filtered_game_id = Common::EscapeFileName(game_id); + u64 previous_time; + m_time_list->Get(filtered_game_id, &previous_time); + return std::chrono::milliseconds(previous_time); +} + +void TimePlayed::Reload() +{ + m_ini.Load(m_ini_path); + m_time_list = m_ini.GetOrCreateSection("TimePlayed"); +} diff --git a/Source/Core/Core/TimePlayed.h b/Source/Core/Core/TimePlayed.h new file mode 100644 index 0000000000..306438b778 --- /dev/null +++ b/Source/Core/Core/TimePlayed.h @@ -0,0 +1,40 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "Common/CommonTypes.h" +#include "Common/IniFile.h" + +class TimePlayed +{ +public: + // used for QT interface - general access to time played for games + TimePlayed(); + + TimePlayed(std::string game_id); + + // not copyable due to the stored section pointer + TimePlayed(const TimePlayed& other) = delete; + TimePlayed(TimePlayed&& other) = delete; + TimePlayed& operator=(const TimePlayed& other) = delete; + TimePlayed& operator=(TimePlayed&& other) = delete; + + ~TimePlayed(); + + void AddTime(std::chrono::milliseconds time_emulated); + + std::chrono::milliseconds GetTimePlayed() const; + std::chrono::milliseconds GetTimePlayed(std::string game_id) const; + + void Reload(); + +private: + std::string m_game_id; + std::string m_ini_path; + Common::IniFile m_ini; + Common::IniFile::Section* m_time_list; +}; diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index e41e9fc26a..367c347213 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -459,6 +459,7 @@ + @@ -1125,6 +1126,7 @@ + From 892d6e30dfb7ef8b444819f2ca271b8357f77aa9 Mon Sep 17 00:00:00 2001 From: Aneesh Maganti <28660350+aminoa@users.noreply.github.com> Date: Mon, 10 Feb 2025 15:28:09 -0500 Subject: [PATCH 2/6] Core/ConfigManager: Add locks for metadata methods All metadata access methods now acquire a lock on `m_metadata_lock` to prevent race conditions. --- Source/Core/Core/ConfigManager.cpp | 42 ++++++++++++++++++++++++++++++ Source/Core/Core/ConfigManager.h | 15 ++++++----- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/Source/Core/Core/ConfigManager.cpp b/Source/Core/Core/ConfigManager.cpp index 21d722290e..5a123c87cd 100644 --- a/Source/Core/Core/ConfigManager.cpp +++ b/Source/Core/Core/ConfigManager.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -98,14 +99,52 @@ void SConfig::LoadSettings() Config::Load(); } +const std::string SConfig::GetGameID() const +{ + std::lock_guard lock(m_metadata_lock); + return m_game_id; +} + +const std::string SConfig::GetGameTDBID() const +{ + std::lock_guard lock(m_metadata_lock); + return m_gametdb_id; +} + +const std::string SConfig::GetTitleName() const +{ + std::lock_guard lock(m_metadata_lock); + return m_title_name; +} + +const std::string SConfig::GetTitleDescription() const +{ + std::lock_guard lock(m_metadata_lock); + return m_title_description; +} + +u64 SConfig::GetTitleID() const +{ + std::lock_guard lock(m_metadata_lock); + return m_title_id; +} + +u16 SConfig::GetRevision() const +{ + std::lock_guard lock(m_metadata_lock); + return m_revision; +} + void SConfig::ResetRunningGameMetadata() { + std::lock_guard lock(m_metadata_lock); SetRunningGameMetadata("00000000", "", 0, 0, DiscIO::Region::Unknown); } void SConfig::SetRunningGameMetadata(const DiscIO::Volume& volume, const DiscIO::Partition& partition) { + std::lock_guard lock(m_metadata_lock); if (partition == volume.GetGamePartition()) { SetRunningGameMetadata(volume.GetGameID(), volume.GetGameTDBID(), @@ -122,6 +161,7 @@ void SConfig::SetRunningGameMetadata(const DiscIO::Volume& volume, void SConfig::SetRunningGameMetadata(const IOS::ES::TMDReader& tmd, DiscIO::Platform platform) { + std::lock_guard lock(m_metadata_lock); const u64 tmd_title_id = tmd.GetTitleId(); // If we're launching a disc game, we want to read the revision from @@ -139,12 +179,14 @@ void SConfig::SetRunningGameMetadata(const IOS::ES::TMDReader& tmd, DiscIO::Plat void SConfig::SetRunningGameMetadata(const std::string& game_id) { + std::lock_guard lock(m_metadata_lock); SetRunningGameMetadata(game_id, "", 0, 0, DiscIO::Region::Unknown); } void SConfig::SetRunningGameMetadata(const std::string& game_id, const std::string& gametdb_id, u64 title_id, u16 revision, DiscIO::Region region) { + std::lock_guard lock(m_metadata_lock); const bool was_changed = m_game_id != game_id || m_gametdb_id != gametdb_id || m_title_id != title_id || m_revision != revision; m_game_id = game_id; diff --git a/Source/Core/Core/ConfigManager.h b/Source/Core/Core/ConfigManager.h index 999cc9f86c..d21b087a51 100644 --- a/Source/Core/Core/ConfigManager.h +++ b/Source/Core/Core/ConfigManager.h @@ -4,6 +4,7 @@ #pragma once #include +#include #include #include #include @@ -58,15 +59,16 @@ struct SConfig std::string m_strSRAM; std::string m_debugger_game_id; + // TODO: remove this as soon as the ticket view hack in IOS/ES/Views is dropped. bool m_disc_booted_from_game_list = false; - const std::string& GetGameID() const { return m_game_id; } - const std::string& GetGameTDBID() const { return m_gametdb_id; } - const std::string& GetTitleName() const { return m_title_name; } - const std::string& GetTitleDescription() const { return m_title_description; } - u64 GetTitleID() const { return m_title_id; } - u16 GetRevision() const { return m_revision; } + const std::string GetGameID() const; + const std::string GetGameTDBID() const; + const std::string GetTitleName() const; + const std::string GetTitleDescription() const; + u64 GetTitleID() const; + u16 GetRevision() const; void ResetRunningGameMetadata(); void SetRunningGameMetadata(const DiscIO::Volume& volume, const DiscIO::Partition& partition); void SetRunningGameMetadata(const IOS::ES::TMDReader& tmd, DiscIO::Platform platform); @@ -114,6 +116,7 @@ private: u64 title_id, u16 revision, DiscIO::Region region); static SConfig* m_Instance; + mutable std::recursive_mutex m_metadata_lock; std::string m_game_id; std::string m_gametdb_id; From a9ebedbf7dcce6234f4355e4111acf1ce69d6fa6 Mon Sep 17 00:00:00 2001 From: Aneesh Maganti <28660350+aminoa@users.noreply.github.com> Date: Mon, 10 Feb 2025 15:27:45 -0500 Subject: [PATCH 3/6] Core/Config: Add settings for time tracking --- Source/Core/Core/Config/MainSettings.cpp | 3 +++ Source/Core/Core/Config/MainSettings.h | 2 ++ 2 files changed, 5 insertions(+) diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index 0442347fdd..4159042b2f 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -309,6 +309,7 @@ const Info MAIN_GDB_PORT{{System::Main, "General", "GDBPort"}, -1}; const Info MAIN_ISO_PATH_COUNT{{System::Main, "General", "ISOPaths"}, 0}; const Info MAIN_SKYLANDERS_PATH{{System::Main, "General", "SkylandersCollectionPath"}, ""}; +const Info MAIN_TIME_TRACKING{{System::Main, "General", "EnablePlayTimeTracking"}, true}; static Info MakeISOPathConfigInfo(size_t idx) { @@ -459,6 +460,8 @@ const Info MAIN_GAMELIST_COLUMN_BLOCK_SIZE{{System::Main, "GameList", "Col false}; const Info MAIN_GAMELIST_COLUMN_COMPRESSION{{System::Main, "GameList", "ColumnCompression"}, false}; +const Info MAIN_GAMELIST_COLUMN_TIME_PLAYED{{System::Main, "GameList", "ColumnTimePlayed"}, + true}; const Info MAIN_GAMELIST_COLUMN_TAGS{{System::Main, "GameList", "ColumnTags"}, false}; // Main.FifoPlayer diff --git a/Source/Core/Core/Config/MainSettings.h b/Source/Core/Core/Config/MainSettings.h index b6e8f966c7..8764a5d91c 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -186,6 +186,7 @@ extern const Info MAIN_RENDER_WINDOW_HEIGHT; extern const Info MAIN_RENDER_WINDOW_AUTOSIZE; extern const Info MAIN_KEEP_WINDOW_ON_TOP; extern const Info MAIN_DISABLE_SCREENSAVER; +extern const Info MAIN_TIME_TRACKING; // Main.General @@ -295,6 +296,7 @@ extern const Info MAIN_GAMELIST_COLUMN_FILE_SIZE; extern const Info MAIN_GAMELIST_COLUMN_FILE_FORMAT; extern const Info MAIN_GAMELIST_COLUMN_BLOCK_SIZE; extern const Info MAIN_GAMELIST_COLUMN_COMPRESSION; +extern const Info MAIN_GAMELIST_COLUMN_TIME_PLAYED; extern const Info MAIN_GAMELIST_COLUMN_TAGS; // Main.FifoPlayer From 276f043db8f8615b45ca84ebacf84d3bb9bc3aad Mon Sep 17 00:00:00 2001 From: Aneesh Maganti <28660350+aminoa@users.noreply.github.com> Date: Mon, 10 Feb 2025 15:26:07 -0500 Subject: [PATCH 4/6] DolphinQt: Create toggle for enabling/disabling time tracking Introduce a new "Enable Time Tracking" checkbox in the InterfacePane UI. The checkbox is dynamically enabled or disabled based on the emulation state, preventing changes while emulation is active. --- .../Core/DolphinQt/Settings/InterfacePane.cpp | 21 +++++++++++++++++++ .../Core/DolphinQt/Settings/InterfacePane.h | 8 +++++++ 2 files changed, 29 insertions(+) diff --git a/Source/Core/DolphinQt/Settings/InterfacePane.cpp b/Source/Core/DolphinQt/Settings/InterfacePane.cpp index e52fb69200..71a044d658 100644 --- a/Source/Core/DolphinQt/Settings/InterfacePane.cpp +++ b/Source/Core/DolphinQt/Settings/InterfacePane.cpp @@ -22,6 +22,8 @@ #include "Core/AchievementManager.h" #include "Core/Config/MainSettings.h" #include "Core/Config/UISettings.h" +#include "Core/Core.h" +#include "Core/System.h" #include "DolphinQt/Config/ConfigControls/ConfigBool.h" #include "DolphinQt/Config/ConfigControls/ConfigChoice.h" @@ -95,6 +97,10 @@ InterfacePane::InterfacePane(QWidget* parent) : QWidget(parent) connect(&Settings::Instance(), &Settings::EmulationStateChanged, this, &InterfacePane::UpdateShowDebuggingCheckbox); + connect(&Settings::Instance(), &Settings::EmulationStateChanged, this, + &InterfacePane::OnEmulationStateChanged); + + OnEmulationStateChanged(Core::GetState(Core::System::GetInstance())); } void InterfacePane::CreateLayout() @@ -168,12 +174,15 @@ void InterfacePane::CreateUI() new ConfigBool(tr("Hotkeys Require Window Focus"), Config::MAIN_FOCUSED_HOTKEYS); m_checkbox_disable_screensaver = new ConfigBool(tr("Inhibit Screensaver During Emulation"), Config::MAIN_DISABLE_SCREENSAVER); + m_checkbox_time_tracking = + new ConfigBool(tr("Enable Play Time Tracking"), Config::MAIN_TIME_TRACKING); groupbox_layout->addWidget(m_checkbox_use_builtin_title_database); groupbox_layout->addWidget(m_checkbox_use_covers); groupbox_layout->addWidget(m_checkbox_show_debugging_ui); groupbox_layout->addWidget(m_checkbox_focused_hotkeys); groupbox_layout->addWidget(m_checkbox_disable_screensaver); + groupbox_layout->addWidget(m_checkbox_time_tracking); } void InterfacePane::CreateInGame() @@ -313,6 +322,12 @@ void InterfacePane::OnLanguageChanged() tr("You must restart Dolphin in order for the change to take effect.")); } +void InterfacePane::OnEmulationStateChanged(Core::State state) +{ + const bool uninitialized = state == Core::State::Uninitialized; + m_checkbox_time_tracking->setEnabled(uninitialized); +} + void InterfacePane::AddDescriptions() { static constexpr char TR_TITLE_DATABASE_DESCRIPTION[] = QT_TR_NOOP( @@ -341,6 +356,10 @@ void InterfacePane::AddDescriptions() static constexpr char TR_DISABLE_SCREENSAVER_DESCRIPTION[] = QT_TR_NOOP("Disables your screensaver while running a game." "

If unsure, leave this checked."); + static constexpr char TR_TIME_TRACKING[] = QT_TR_NOOP( + "Tracks the time you spend playing games and shows it in the List View (as hours/minutes)." + "

This setting cannot be changed while emulation is active." + "

If unsure, leave this checked."); static constexpr char TR_CONFIRM_ON_STOP_DESCRIPTION[] = QT_TR_NOOP("Prompts you to confirm that you want to end emulation when you press Stop." "

If unsure, leave this checked."); @@ -394,6 +413,8 @@ void InterfacePane::AddDescriptions() m_checkbox_disable_screensaver->SetDescription(tr(TR_DISABLE_SCREENSAVER_DESCRIPTION)); + m_checkbox_time_tracking->SetDescription(tr(TR_TIME_TRACKING)); + m_checkbox_confirm_on_stop->SetDescription(tr(TR_CONFIRM_ON_STOP_DESCRIPTION)); m_checkbox_use_panic_handlers->SetDescription(tr(TR_USE_PANIC_HANDLERS_DESCRIPTION)); diff --git a/Source/Core/DolphinQt/Settings/InterfacePane.h b/Source/Core/DolphinQt/Settings/InterfacePane.h index 90a81d2114..ffe1efa087 100644 --- a/Source/Core/DolphinQt/Settings/InterfacePane.h +++ b/Source/Core/DolphinQt/Settings/InterfacePane.h @@ -13,6 +13,11 @@ class QVBoxLayout; class ToolTipCheckBox; class ToolTipComboBox; +namespace Core +{ +enum class State; +} + class InterfacePane final : public QWidget { Q_OBJECT @@ -30,6 +35,8 @@ private: void OnUserStyleChanged(); void OnLanguageChanged(); + void OnEmulationStateChanged(Core::State state); + QVBoxLayout* m_main_layout; ConfigStringChoice* m_combobox_language; @@ -42,6 +49,7 @@ private: ConfigBool* m_checkbox_focused_hotkeys; ConfigBool* m_checkbox_use_covers; ConfigBool* m_checkbox_disable_screensaver; + ConfigBool* m_checkbox_time_tracking; ConfigBool* m_checkbox_confirm_on_stop; ConfigBool* m_checkbox_use_panic_handlers; From 3c44fe592b8db19bf7be5f5e1cb45e5cb0062950 Mon Sep 17 00:00:00 2001 From: Aneesh Maganti <28660350+aminoa@users.noreply.github.com> Date: Mon, 10 Feb 2025 15:27:16 -0500 Subject: [PATCH 5/6] DolphinQt: Add "Time Played" column to game list view Shows minutes/hours in the list view and handles column visibility. --- Source/Core/DolphinQt/GameList/GameList.cpp | 3 ++ .../Core/DolphinQt/GameList/GameListModel.cpp | 33 +++++++++++++++++++ .../Core/DolphinQt/GameList/GameListModel.h | 6 ++++ Source/Core/DolphinQt/MenuBar.cpp | 1 + 4 files changed, 43 insertions(+) diff --git a/Source/Core/DolphinQt/GameList/GameList.cpp b/Source/Core/DolphinQt/GameList/GameList.cpp index 7824925396..a4514a0449 100644 --- a/Source/Core/DolphinQt/GameList/GameList.cpp +++ b/Source/Core/DolphinQt/GameList/GameList.cpp @@ -208,6 +208,7 @@ void GameList::MakeListView() SetResizeMode(Column::FileFormat, Mode::Fixed); SetResizeMode(Column::BlockSize, Mode::Fixed); SetResizeMode(Column::Compression, Mode::Fixed); + SetResizeMode(Column::TimePlayed, Mode::Interactive); SetResizeMode(Column::Tags, Mode::Interactive); // Cells have 3 pixels of padding, so the width of these needs to be image width + 6. Banners @@ -273,6 +274,7 @@ void GameList::UpdateColumnVisibility() SetVisiblity(Column::FileFormat, Config::Get(Config::MAIN_GAMELIST_COLUMN_FILE_FORMAT)); SetVisiblity(Column::BlockSize, Config::Get(Config::MAIN_GAMELIST_COLUMN_BLOCK_SIZE)); SetVisiblity(Column::Compression, Config::Get(Config::MAIN_GAMELIST_COLUMN_COMPRESSION)); + SetVisiblity(Column::TimePlayed, Config::Get(Config::MAIN_GAMELIST_COLUMN_TIME_PLAYED)); SetVisiblity(Column::Tags, Config::Get(Config::MAIN_GAMELIST_COLUMN_TAGS)); } @@ -1005,6 +1007,7 @@ void GameList::OnColumnVisibilityToggled(const QString& row, bool visible) {tr("File Format"), Column::FileFormat}, {tr("Block Size"), Column::BlockSize}, {tr("Compression"), Column::Compression}, + {tr("Time Played"), Column::TimePlayed}, {tr("Tags"), Column::Tags}, }; diff --git a/Source/Core/DolphinQt/GameList/GameListModel.cpp b/Source/Core/DolphinQt/GameList/GameListModel.cpp index c70a870c23..c8c1d8b165 100644 --- a/Source/Core/DolphinQt/GameList/GameListModel.cpp +++ b/Source/Core/DolphinQt/GameList/GameListModel.cpp @@ -9,6 +9,8 @@ #include #include "Core/Config/MainSettings.h" +#include "Core/Core.h" +#include "Core/TimePlayed.h" #include "DiscIO/Enums.h" @@ -32,6 +34,8 @@ GameListModel::GameListModel(QObject* parent) : QAbstractTableModel(parent) &GameTracker::RefreshAll); connect(&Settings::Instance(), &Settings::TitleDBReloadRequested, [this] { m_title_database = Core::TitleDatabase(); }); + connect(&Settings::Instance(), &Settings::EmulationStateChanged, this, + &GameListModel::OnEmulationStateChanged); for (const QString& dir : Settings::Instance().GetPaths()) m_tracker.AddDirectory(dir); @@ -187,6 +191,25 @@ QVariant GameListModel::data(const QModelIndex& index, int role) const return compression.isEmpty() ? tr("No Compression") : compression; } break; + case Column::TimePlayed: + if (role == Qt::DisplayRole) + { + const std::string game_id = game.GetGameID(); + const std::chrono::milliseconds total_time = m_timer.GetTimePlayed(game_id); + const auto total_minutes = std::chrono::duration_cast(total_time); + const auto total_hours = std::chrono::duration_cast(total_time); + + // i18n: A time displayed as hours and minutes + QString formatted_time = + tr("%1h %2m").arg(total_hours.count()).arg(total_minutes.count() % 60); + return formatted_time; + } + if (role == SORT_ROLE) + { + const std::string game_id = game.GetGameID(); + return static_cast(m_timer.GetTimePlayed(game_id).count()); + } + break; case Column::Tags: if (role == Qt::DisplayRole || role == SORT_ROLE) { @@ -232,6 +255,8 @@ QVariant GameListModel::headerData(int section, Qt::Orientation orientation, int return tr("Block Size"); case Column::Compression: return tr("Compression"); + case Column::TimePlayed: + return tr("Time Played"); case Column::Tags: return tr("Tags"); default: @@ -480,3 +505,11 @@ void GameListModel::PurgeCache() { m_tracker.PurgeCache(); } + +void GameListModel::OnEmulationStateChanged(Core::State state) +{ + if (state == Core::State::Uninitialized) + { + m_timer.Reload(); + } +} diff --git a/Source/Core/DolphinQt/GameList/GameListModel.h b/Source/Core/DolphinQt/GameList/GameListModel.h index b76e0a37c2..baac5d507d 100644 --- a/Source/Core/DolphinQt/GameList/GameListModel.h +++ b/Source/Core/DolphinQt/GameList/GameListModel.h @@ -12,6 +12,8 @@ #include #include +#include "Core/Core.h" +#include "Core/TimePlayed.h" #include "Core/TitleDatabase.h" #include "DolphinQt/GameList/GameTracker.h" @@ -58,6 +60,7 @@ public: FileFormat, BlockSize, Compression, + TimePlayed, Tags, Count, }; @@ -87,12 +90,15 @@ private: // Index in m_games, or -1 if it isn't found int FindGameIndex(const std::string& path) const; + void OnEmulationStateChanged(Core::State state); + QStringList m_tag_list; QMap m_game_tags; GameTracker m_tracker; QList> m_games; Core::TitleDatabase m_title_database; + TimePlayed m_timer; QString m_term; float m_scale = 1.0; }; diff --git a/Source/Core/DolphinQt/MenuBar.cpp b/Source/Core/DolphinQt/MenuBar.cpp index fef441c062..24c6cc2d04 100644 --- a/Source/Core/DolphinQt/MenuBar.cpp +++ b/Source/Core/DolphinQt/MenuBar.cpp @@ -701,6 +701,7 @@ void MenuBar::AddListColumnsMenu(QMenu* view_menu) {tr("File Format"), &Config::MAIN_GAMELIST_COLUMN_FILE_FORMAT}, {tr("Block Size"), &Config::MAIN_GAMELIST_COLUMN_BLOCK_SIZE}, {tr("Compression"), &Config::MAIN_GAMELIST_COLUMN_COMPRESSION}, + {tr("Time Played"), &Config::MAIN_GAMELIST_COLUMN_TIME_PLAYED}, {tr("Tags"), &Config::MAIN_GAMELIST_COLUMN_TAGS}}; QActionGroup* column_group = new QActionGroup(this); From fffb499da2fdde53e1ff25e17f2f718cd608167f Mon Sep 17 00:00:00 2001 From: Aneesh Maganti <28660350+aminoa@users.noreply.github.com> Date: Mon, 10 Feb 2025 15:28:32 -0500 Subject: [PATCH 6/6] Core/HW: Add time tracking thread in CPU manager Introduce method to track the time played for a game via time differences and TimePlayed methods. Threads are synchronized via Common::Event. --- Source/Core/Core/HW/CPU.cpp | 54 +++++++++++++++++++++++++++++++++++++ Source/Core/Core/HW/CPU.h | 4 +++ 2 files changed, 58 insertions(+) diff --git a/Source/Core/Core/HW/CPU.cpp b/Source/Core/Core/HW/CPU.cpp index 1eae912476..487a33c6ff 100644 --- a/Source/Core/Core/HW/CPU.cpp +++ b/Source/Core/Core/HW/CPU.cpp @@ -10,12 +10,16 @@ #include "AudioCommon/AudioCommon.h" #include "Common/CommonTypes.h" #include "Common/Event.h" +#include "Common/Timer.h" #include "Core/CPUThreadConfigCallback.h" +#include "Core/Config/MainSettings.h" +#include "Core/ConfigManager.h" #include "Core/Core.h" #include "Core/Host.h" #include "Core/PowerPC/GDBStub.h" #include "Core/PowerPC/PowerPC.h" #include "Core/System.h" +#include "Core/TimePlayed.h" #include "VideoCommon/Fifo.h" namespace CPU @@ -63,6 +67,41 @@ void CPUManager::ExecutePendingJobs(std::unique_lock& state_lock) } } +void CPUManager::StartTimePlayedTimer() +{ + // Steady clock for greater accuracy of timing + std::chrono::steady_clock timer; + auto prev_time = timer.now(); + + while (true) + { + const std::string game_id = SConfig::GetInstance().GetGameID(); + TimePlayed time_played(game_id); + auto curr_time = timer.now(); + + // Check that emulation is not paused + // If the emulation is paused, wait for SetStepping() to reactivate + if (m_state == State::Running) + { + const auto diff_time = + std::chrono::duration_cast(curr_time - prev_time); + time_played.AddTime(diff_time); + } + else if (m_state == State::Stepping) + { + m_time_played_finish_sync.Wait(); + curr_time = timer.now(); + } + + prev_time = curr_time; + + if (m_state == State::PowerDown) + return; + + m_time_played_finish_sync.WaitFor(std::chrono::seconds(30)); + } +} + void CPUManager::Run() { auto& power_pc = m_system.GetPowerPC(); @@ -71,6 +110,13 @@ void CPUManager::Run() // We can't rely on PowerPC::Init doing it, since it's called from EmuThread. PowerPC::RoundingModeUpdated(power_pc.GetPPCState()); + // Start a separate time tracker thread + std::thread timing; + if (Config::Get(Config::MAIN_TIME_TRACKING)) + { + timing = std::thread(&CPUManager::StartTimePlayedTimer, this); + } + std::unique_lock state_lock(m_state_change_lock); while (m_state != State::PowerDown) { @@ -165,6 +211,13 @@ void CPUManager::Run() break; } } + + if (timing.joinable()) + { + m_time_played_finish_sync.Set(); + timing.join(); + } + state_lock.unlock(); Host_UpdateDisasmDialog(); } @@ -266,6 +319,7 @@ void CPUManager::SetStepping(bool stepping) else if (SetStateLocked(State::Running)) { m_state_cpu_cvar.notify_one(); + m_time_played_finish_sync.Set(); RunAdjacentSystems(true); } } diff --git a/Source/Core/Core/HW/CPU.h b/Source/Core/Core/HW/CPU.h index 57e8a31a29..b73471136d 100644 --- a/Source/Core/Core/HW/CPU.h +++ b/Source/Core/Core/HW/CPU.h @@ -8,6 +8,8 @@ #include #include +#include "Common/Event.h" + namespace Common { class Event; @@ -102,6 +104,7 @@ public: private: void FlushStepSyncEventLocked(); void ExecutePendingJobs(std::unique_lock& state_lock); + void StartTimePlayedTimer(); void RunAdjacentSystems(bool running); bool SetStateLocked(State s); @@ -133,6 +136,7 @@ private: bool m_state_cpu_step_instruction = false; Common::Event* m_state_cpu_step_instruction_sync = nullptr; std::queue> m_pending_jobs; + Common::Event m_time_played_finish_sync; Core::System& m_system; };