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.
This commit is contained in:
Mahmoud Al-Qudsi 2025-06-10 13:49:18 -05:00 committed by GreemDev
parent 6bb2af0091
commit 84758d0ddc
3 changed files with 13 additions and 5 deletions

View file

@ -75,6 +75,7 @@ namespace Ryujinx.Ava.Systems
private readonly long _ticksPerFrame; private readonly long _ticksPerFrame;
private readonly Stopwatch _chrono; private readonly Stopwatch _chrono;
private readonly Stopwatch _pauseTimer;
private long _ticks; private long _ticks;
private readonly AccountManager _accountManager; private readonly AccountManager _accountManager;
@ -175,6 +176,7 @@ namespace Ryujinx.Ava.Systems
_chrono = new Stopwatch(); _chrono = new Stopwatch();
_ticksPerFrame = Stopwatch.Frequency / TargetFps; _ticksPerFrame = Stopwatch.Frequency / TargetFps;
_pauseTimer = new Stopwatch();
if (ApplicationPath.StartsWith("@SystemContent")) if (ApplicationPath.StartsWith("@SystemContent"))
{ {
@ -616,7 +618,7 @@ namespace Ryujinx.Ava.Systems
private void Dispose() private void Dispose()
{ {
if (Device.Processes != null) 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.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState;
ConfigurationState.Instance.Graphics.AspectRatio.Event -= UpdateAspectRatioState; ConfigurationState.Instance.Graphics.AspectRatio.Event -= UpdateAspectRatioState;
@ -635,6 +637,7 @@ namespace Ryujinx.Ava.Systems
_gpuCancellationTokenSource.Dispose(); _gpuCancellationTokenSource.Dispose();
_chrono.Stop(); _chrono.Stop();
_pauseTimer.Stop();
} }
public void DisposeGpu() public void DisposeGpu()
@ -877,6 +880,7 @@ namespace Ryujinx.Ava.Systems
Device?.System.TogglePauseEmulation(false); Device?.System.TogglePauseEmulation(false);
_viewModel.IsPaused = false; _viewModel.IsPaused = false;
_pauseTimer.Stop();
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI); _viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI);
Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed"); Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed");
} }
@ -886,6 +890,7 @@ namespace Ryujinx.Ava.Systems
Device?.System.TogglePauseEmulation(true); Device?.System.TogglePauseEmulation(true);
_viewModel.IsPaused = true; _viewModel.IsPaused = true;
_pauseTimer.Start();
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI, LocaleManager.Instance[LocaleKeys.Paused]); _viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI, LocaleManager.Instance[LocaleKeys.Paused]);
Logger.Info?.Print(LogClass.Emulation, "Emulation was paused"); Logger.Info?.Print(LogClass.Emulation, "Emulation was paused");
} }

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Diagnostics;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Systems.AppLibrary namespace Ryujinx.Ava.Systems.AppLibrary
@ -33,7 +34,8 @@ namespace Ryujinx.Ava.Systems.AppLibrary
/// <summary> /// <summary>
/// Updates <see cref="LastPlayed"/> and <see cref="TimePlayed"/>. Call this after a game ends. /// Updates <see cref="LastPlayed"/> and <see cref="TimePlayed"/>. Call this after a game ends.
/// </summary> /// </summary>
public void UpdatePostGame() /// <param name="pauseTime">The amount of time emulation was paused while playing.</param>
public void UpdatePostGame(TimeSpan pauseTime)
{ {
DateTime? prevLastPlayed = LastPlayed; DateTime? prevLastPlayed = LastPlayed;
UpdatePreGame(); UpdatePreGame();
@ -44,7 +46,8 @@ namespace Ryujinx.Ava.Systems.AppLibrary
} }
TimeSpan diff = DateTime.UtcNow - prevLastPlayed.Value; 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)); TimePlayed = TimeSpan.FromSeconds(Math.Round(newTotalSeconds, MidpointRounding.AwayFromZero));
} }
} }

View file

@ -1688,8 +1688,8 @@ namespace Ryujinx.Ava.UI.ViewModels
RendererHostControl.Focus(); RendererHostControl.Focus();
}); });
public static void UpdateGameMetadata(string titleId) public static void UpdateGameMetadata(string titleId, TimeSpan pauseTime)
=> ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame()); => ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame(pauseTime));
public void RefreshFirmwareStatus() public void RefreshFirmwareStatus()
{ {