From 84758d0ddc4418ff520d9f18a7b64781d1acc187 Mon Sep 17 00:00:00 2001 From: Mahmoud Al-Qudsi Date: Tue, 10 Jun 2025 13:49:18 -0500 Subject: [PATCH 1/2] Exclude time spent with emulator paused from play time With this change, the play time reported internally by a game's gameplay timer should match (or be much closer to matching) what Ryujinx displays in the application library. Aside from this being closer to the natural expectation of what "hours played" would take into account (as by definition time spent paused is time spent not playing), this also brings us closer to the behavior of other emulators and game libraries. --- src/Ryujinx/Systems/AppHost.cs | 7 ++++++- src/Ryujinx/Systems/AppLibrary/ApplicationMetadata.cs | 7 +++++-- src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs | 4 ++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs index 74005400c..94c2f2900 100644 --- a/src/Ryujinx/Systems/AppHost.cs +++ b/src/Ryujinx/Systems/AppHost.cs @@ -75,6 +75,7 @@ namespace Ryujinx.Ava.Systems private readonly long _ticksPerFrame; private readonly Stopwatch _chrono; + private readonly Stopwatch _pauseTimer; private long _ticks; private readonly AccountManager _accountManager; @@ -175,6 +176,7 @@ namespace Ryujinx.Ava.Systems _chrono = new Stopwatch(); _ticksPerFrame = Stopwatch.Frequency / TargetFps; + _pauseTimer = new Stopwatch(); if (ApplicationPath.StartsWith("@SystemContent")) { @@ -616,7 +618,7 @@ namespace Ryujinx.Ava.Systems private void Dispose() { if (Device.Processes != null) - MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication.ProgramIdText); + MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication.ProgramIdText, _pauseTimer.Elapsed); ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState; ConfigurationState.Instance.Graphics.AspectRatio.Event -= UpdateAspectRatioState; @@ -635,6 +637,7 @@ namespace Ryujinx.Ava.Systems _gpuCancellationTokenSource.Dispose(); _chrono.Stop(); + _pauseTimer.Stop(); } public void DisposeGpu() @@ -877,6 +880,7 @@ namespace Ryujinx.Ava.Systems Device?.System.TogglePauseEmulation(false); _viewModel.IsPaused = false; + _pauseTimer.Stop(); _viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI); Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed"); } @@ -886,6 +890,7 @@ namespace Ryujinx.Ava.Systems Device?.System.TogglePauseEmulation(true); _viewModel.IsPaused = true; + _pauseTimer.Start(); _viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI, LocaleManager.Instance[LocaleKeys.Paused]); Logger.Info?.Print(LogClass.Emulation, "Emulation was paused"); } diff --git a/src/Ryujinx/Systems/AppLibrary/ApplicationMetadata.cs b/src/Ryujinx/Systems/AppLibrary/ApplicationMetadata.cs index 9d8488aeb..e80fae009 100644 --- a/src/Ryujinx/Systems/AppLibrary/ApplicationMetadata.cs +++ b/src/Ryujinx/Systems/AppLibrary/ApplicationMetadata.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Text.Json.Serialization; namespace Ryujinx.Ava.Systems.AppLibrary @@ -33,7 +34,8 @@ namespace Ryujinx.Ava.Systems.AppLibrary /// /// Updates and . Call this after a game ends. /// - public void UpdatePostGame() + /// The amount of time emulation was paused while playing. + public void UpdatePostGame(TimeSpan pauseTime) { DateTime? prevLastPlayed = LastPlayed; UpdatePreGame(); @@ -44,7 +46,8 @@ namespace Ryujinx.Ava.Systems.AppLibrary } TimeSpan diff = DateTime.UtcNow - prevLastPlayed.Value; - double newTotalSeconds = TimePlayed.Add(diff).TotalSeconds; + Debug.Assert(pauseTime <= diff); + double newTotalSeconds = TimePlayed.Add(diff - pauseTime).TotalSeconds; TimePlayed = TimeSpan.FromSeconds(Math.Round(newTotalSeconds, MidpointRounding.AwayFromZero)); } } diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 8b9b04511..1ea9d009d 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -1688,8 +1688,8 @@ namespace Ryujinx.Ava.UI.ViewModels RendererHostControl.Focus(); }); - public static void UpdateGameMetadata(string titleId) - => ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame()); + public static void UpdateGameMetadata(string titleId, TimeSpan pauseTime) + => ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame(pauseTime)); public void RefreshFirmwareStatus() { From a151ae5b8aa08f8969d5c40713e15dc79ff7baac Mon Sep 17 00:00:00 2001 From: Mahmoud Al-Qudsi Date: Wed, 11 Jun 2025 15:37:56 -0500 Subject: [PATCH 2/2] Rework updates of application metadata gameplay hours Rely on `AppHost` to more accurately track active gameplay time instead of doing naive DateTime.UtcNow subtraction in UpdatePre/UpdatePostGame() calls. --- src/Ryujinx/Systems/AppHost.cs | 14 ++++++++------ .../Systems/AppLibrary/ApplicationMetadata.cs | 17 +++-------------- .../UI/ViewModels/MainWindowViewModel.cs | 4 ++-- 3 files changed, 13 insertions(+), 22 deletions(-) diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs index 94c2f2900..1c5f64309 100644 --- a/src/Ryujinx/Systems/AppHost.cs +++ b/src/Ryujinx/Systems/AppHost.cs @@ -75,7 +75,7 @@ namespace Ryujinx.Ava.Systems private readonly long _ticksPerFrame; private readonly Stopwatch _chrono; - private readonly Stopwatch _pauseTimer; + private readonly Stopwatch _playTimer; private long _ticks; private readonly AccountManager _accountManager; @@ -176,7 +176,7 @@ namespace Ryujinx.Ava.Systems _chrono = new Stopwatch(); _ticksPerFrame = Stopwatch.Frequency / TargetFps; - _pauseTimer = new Stopwatch(); + _playTimer = new Stopwatch(); if (ApplicationPath.StartsWith("@SystemContent")) { @@ -567,6 +567,7 @@ namespace Ryujinx.Ava.Systems public void Stop() { _isActive = false; + _playTimer.Stop(); } private void Exit() @@ -618,7 +619,7 @@ namespace Ryujinx.Ava.Systems private void Dispose() { if (Device.Processes != null) - MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication.ProgramIdText, _pauseTimer.Elapsed); + MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication.ProgramIdText, _playTimer.Elapsed); ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState; ConfigurationState.Instance.Graphics.AspectRatio.Event -= UpdateAspectRatioState; @@ -637,7 +638,7 @@ namespace Ryujinx.Ava.Systems _gpuCancellationTokenSource.Dispose(); _chrono.Stop(); - _pauseTimer.Stop(); + _playTimer.Stop(); } public void DisposeGpu() @@ -871,6 +872,7 @@ namespace Ryujinx.Ava.Systems ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, appMetadata => appMetadata.UpdatePreGame() ); + _playTimer.Start(); return true; } @@ -880,7 +882,7 @@ namespace Ryujinx.Ava.Systems Device?.System.TogglePauseEmulation(false); _viewModel.IsPaused = false; - _pauseTimer.Stop(); + _playTimer.Start(); _viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI); Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed"); } @@ -890,7 +892,7 @@ namespace Ryujinx.Ava.Systems Device?.System.TogglePauseEmulation(true); _viewModel.IsPaused = true; - _pauseTimer.Start(); + _playTimer.Stop(); _viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI, LocaleManager.Instance[LocaleKeys.Paused]); Logger.Info?.Print(LogClass.Emulation, "Emulation was paused"); } diff --git a/src/Ryujinx/Systems/AppLibrary/ApplicationMetadata.cs b/src/Ryujinx/Systems/AppLibrary/ApplicationMetadata.cs index e80fae009..8940657b0 100644 --- a/src/Ryujinx/Systems/AppLibrary/ApplicationMetadata.cs +++ b/src/Ryujinx/Systems/AppLibrary/ApplicationMetadata.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; using System.Text.Json.Serialization; namespace Ryujinx.Ava.Systems.AppLibrary @@ -34,21 +33,11 @@ namespace Ryujinx.Ava.Systems.AppLibrary /// /// Updates and . Call this after a game ends. /// - /// The amount of time emulation was paused while playing. - public void UpdatePostGame(TimeSpan pauseTime) + /// The active gameplay time this past session. + public void UpdatePostGame(TimeSpan playTime) { - DateTime? prevLastPlayed = LastPlayed; UpdatePreGame(); - - if (!prevLastPlayed.HasValue) - { - return; - } - - TimeSpan diff = DateTime.UtcNow - prevLastPlayed.Value; - Debug.Assert(pauseTime <= diff); - double newTotalSeconds = TimePlayed.Add(diff - pauseTime).TotalSeconds; - TimePlayed = TimeSpan.FromSeconds(Math.Round(newTotalSeconds, MidpointRounding.AwayFromZero)); + TimePlayed += playTime; } } } diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 1ea9d009d..5e7df4d62 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -1688,8 +1688,8 @@ namespace Ryujinx.Ava.UI.ViewModels RendererHostControl.Focus(); }); - public static void UpdateGameMetadata(string titleId, TimeSpan pauseTime) - => ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame(pauseTime)); + public static void UpdateGameMetadata(string titleId, TimeSpan playTime) + => ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame(playTime)); public void RefreshFirmwareStatus() {