From 6003afafc790f20d78f884cde36b8cfa78980fe6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Aug 2022 14:52:47 +0900 Subject: [PATCH] Use `FramedBeatmapClock` in `GameplayClockContainer` --- osu.Game/Beatmaps/FramedBeatmapClock.cs | 98 +++++++++++++------ .../Screens/Play/GameplayClockContainer.cs | 86 +++++++--------- .../Play/MasterGameplayClockContainer.cs | 78 +++------------ 3 files changed, 113 insertions(+), 149 deletions(-) diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index 925cc88fbd..f3219b972e 100644 --- a/osu.Game/Beatmaps/FramedBeatmapClock.cs +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio.Track; @@ -16,6 +17,8 @@ namespace osu.Game.Beatmaps { public class FramedBeatmapClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock { + private readonly bool applyOffsets; + /// /// The length of the underlying beatmap track. Will default to 60 seconds if unavailable. /// @@ -26,9 +29,16 @@ namespace osu.Game.Beatmaps /// public Track? Track { get; private set; } // TODO: virtual rather than null? - private readonly OffsetCorrectionClock userGlobalOffsetClock; - private readonly OffsetCorrectionClock platformOffsetClock; - private readonly OffsetCorrectionClock finalOffsetClock; + /// + /// The total frequency adjustment from pause transforms. Should eventually be handled in a better way. + /// + public readonly BindableDouble ExternalPauseFrequencyAdjust = new BindableDouble(1); + + private readonly OffsetCorrectionClock? userGlobalOffsetClock; + private readonly OffsetCorrectionClock? platformOffsetClock; + private readonly OffsetCorrectionClock? userBeatmapOffsetClock; + + private readonly IFrameBasedClock finalClockSource; private Bindable userAudioOffset = null!; @@ -36,7 +46,20 @@ namespace osu.Game.Beatmaps private readonly DecoupleableInterpolatingFramedClock decoupledClock; - private double totalAppliedOffset => userGlobalOffsetClock.RateAdjustedOffset + finalOffsetClock.RateAdjustedOffset + platformOffsetClock.RateAdjustedOffset; + private double totalAppliedOffset + { + get + { + if (!applyOffsets) + return 0; + + Debug.Assert(userGlobalOffsetClock != null); + Debug.Assert(userBeatmapOffsetClock != null); + Debug.Assert(platformOffsetClock != null); + + return userGlobalOffsetClock.RateAdjustedOffset + userBeatmapOffsetClock.RateAdjustedOffset + platformOffsetClock.RateAdjustedOffset; + } + } [Resolved] private OsuConfigManager config { get; set; } = null!; @@ -53,44 +76,59 @@ namespace osu.Game.Beatmaps set => decoupledClock.IsCoupled = value; } - public FramedBeatmapClock(IClock? sourceClock = null) + public FramedBeatmapClock(IClock? sourceClock = null, bool applyOffsets = false) { - // TODO: Unused for now? - var pauseFreqAdjust = new BindableDouble(1); + this.applyOffsets = applyOffsets; // A decoupled clock is used to ensure precise time values even when the host audio subsystem is not reporting // high precision times (on windows there's generally only 5-10ms reporting intervals, as an example). decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true }; decoupledClock.ChangeSource(sourceClock); - // Audio timings in general with newer BASS versions don't match stable. - // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. - platformOffsetClock = new OffsetCorrectionClock(decoupledClock, pauseFreqAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; + if (applyOffsets) + { + // Audio timings in general with newer BASS versions don't match stable. + // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. + platformOffsetClock = new OffsetCorrectionClock(decoupledClock, ExternalPauseFrequencyAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; - // User global offset (set in settings) should also be applied. - userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock, pauseFreqAdjust); + // User global offset (set in settings) should also be applied. + userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock, ExternalPauseFrequencyAdjust); - // User per-beatmap offset will be applied to this final clock. - finalOffsetClock = new OffsetCorrectionClock(userGlobalOffsetClock, pauseFreqAdjust); + // User per-beatmap offset will be applied to this final clock. + finalClockSource = userBeatmapOffsetClock = new OffsetCorrectionClock(userGlobalOffsetClock, ExternalPauseFrequencyAdjust); + } + else + { + finalClockSource = decoupledClock; + } } protected override void LoadComplete() { base.LoadComplete(); - userAudioOffset = config.GetBindable(OsuSetting.AudioOffset); - userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true); + if (applyOffsets) + { + Debug.Assert(userBeatmapOffsetClock != null); + Debug.Assert(userGlobalOffsetClock != null); - beatmapOffsetSubscription = realm.SubscribeToPropertyChanged( - r => r.Find(beatmap.Value.BeatmapInfo.ID)?.UserSettings, - settings => settings.Offset, - val => finalOffsetClock.Offset = val); + userAudioOffset = config.GetBindable(OsuSetting.AudioOffset); + userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true); + + beatmapOffsetSubscription = realm.SubscribeToPropertyChanged( + r => r.Find(beatmap.Value.BeatmapInfo.ID)?.UserSettings, + settings => settings.Offset, + val => + { + userBeatmapOffsetClock.Offset = val; + }); + } } protected override void Update() { base.Update(); - finalOffsetClock.ProcessFrame(); + finalClockSource.ProcessFrame(); } protected override void Dispose(bool isDisposing) @@ -112,25 +150,25 @@ namespace osu.Game.Beatmaps public void Reset() { decoupledClock.Reset(); - finalOffsetClock.ProcessFrame(); + finalClockSource.ProcessFrame(); } public void Start() { decoupledClock.Start(); - finalOffsetClock.ProcessFrame(); + finalClockSource.ProcessFrame(); } public void Stop() { decoupledClock.Stop(); - finalOffsetClock.ProcessFrame(); + finalClockSource.ProcessFrame(); } public bool Seek(double position) { bool success = decoupledClock.Seek(position - totalAppliedOffset); - finalOffsetClock.ProcessFrame(); + finalClockSource.ProcessFrame(); return success; } @@ -147,20 +185,20 @@ namespace osu.Game.Beatmaps #region Delegation of IFrameBasedClock to clock with all offsets applied - public double CurrentTime => finalOffsetClock.CurrentTime; + public double CurrentTime => finalClockSource.CurrentTime; - public bool IsRunning => finalOffsetClock.IsRunning; + public bool IsRunning => finalClockSource.IsRunning; public void ProcessFrame() { // Noop to ensure an external consumer doesn't process the internal clock an extra time. } - public double ElapsedFrameTime => finalOffsetClock.ElapsedFrameTime; + public double ElapsedFrameTime => finalClockSource.ElapsedFrameTime; - public double FramesPerSecond => finalOffsetClock.FramesPerSecond; + public double FramesPerSecond => finalClockSource.FramesPerSecond; - public FrameTimeInfo TimeInfo => finalOffsetClock.TimeInfo; + public FrameTimeInfo TimeInfo => finalClockSource.TimeInfo; #endregion } diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index ac846b45c4..de28f86824 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -11,12 +11,14 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Framework.Timing; using osu.Framework.Utils; +using osu.Game.Beatmaps; namespace osu.Game.Screens.Play { /// /// Encapsulates gameplay timing logic and provides a via DI for gameplay components to use. /// + [Cached(typeof(IGameplayClock))] public class GameplayClockContainer : Container, IAdjustableClock, IGameplayClock { /// @@ -45,44 +47,34 @@ namespace osu.Game.Screens.Play public virtual IEnumerable NonGameplayAdjustments => Enumerable.Empty(); - /// - /// The final clock which is exposed to gameplay components. - /// - protected IFrameBasedClock FramedClock { get; private set; } - private readonly BindableBool isPaused = new BindableBool(true); /// /// The adjustable source clock used for gameplay. Should be used for seeks and clock control. + /// This is the final clock exposed to gameplay components as an . /// - private readonly DecoupleableInterpolatingFramedClock decoupledClock; + protected readonly FramedBeatmapClock GameplayClock; + + protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; /// /// Creates a new . /// /// The source used for timing. - public GameplayClockContainer(IClock sourceClock) + /// Whether to apply platform, user and beatmap offsets to the mix. + public GameplayClockContainer(IClock sourceClock, bool applyOffsets = false) { SourceClock = sourceClock; RelativeSizeAxes = Axes.Both; - decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; + InternalChildren = new Drawable[] + { + GameplayClock = new FramedBeatmapClock(sourceClock, applyOffsets) { IsCoupled = false }, + Content + }; + IsPaused.BindValueChanged(OnIsPausedChanged); - - // this will be replaced during load, but non-null for tests which don't add this component to the hierarchy. - FramedClock = new FramedClock(); - } - - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - - FramedClock = CreateGameplayClock(decoupledClock); - - dependencies.CacheAs(this); - - return dependencies; } /// @@ -92,13 +84,13 @@ namespace osu.Game.Screens.Play { ensureSourceClockSet(); - if (!decoupledClock.IsRunning) + if (!GameplayClock.IsRunning) { // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time // This accounts for the clock source potentially taking time to enter a completely stopped state - Seek(FramedClock.CurrentTime); + Seek(GameplayClock.CurrentTime); - decoupledClock.Start(); + GameplayClock.Start(); } isPaused.Value = false; @@ -112,10 +104,10 @@ namespace osu.Game.Screens.Play { Logger.Log($"{nameof(GameplayClockContainer)} seeking to {time}"); - decoupledClock.Seek(time); + GameplayClock.Seek(time); // Manually process to make sure the gameplay clock is correctly updated after a seek. - FramedClock.ProcessFrame(); + GameplayClock.ProcessFrame(); OnSeek?.Invoke(); } @@ -132,7 +124,7 @@ namespace osu.Game.Screens.Play public void Reset(bool startClock = false) { // Manually stop the source in order to not affect the IsPaused state. - decoupledClock.Stop(); + GameplayClock.Stop(); if (!IsPaused.Value || startClock) Start(); @@ -145,10 +137,10 @@ namespace osu.Game.Screens.Play /// Changes the source clock. /// /// The new source. - protected void ChangeSource(IClock sourceClock) => decoupledClock.ChangeSource(SourceClock = sourceClock); + protected void ChangeSource(IClock sourceClock) => GameplayClock.ChangeSource(SourceClock = sourceClock); /// - /// Ensures that the is set to , if it hasn't been given a source yet. + /// Ensures that the is set to , if it hasn't been given a source yet. /// This is usually done before a seek to avoid accidentally seeking only the adjustable source in decoupled mode, /// but not the actual source clock. /// That will pretty much only happen on the very first call of this method, as the source clock is passed in the constructor, @@ -156,40 +148,30 @@ namespace osu.Game.Screens.Play /// private void ensureSourceClockSet() { - if (decoupledClock.Source == null) + if (GameplayClock.Source == null) ChangeSource(SourceClock); } protected override void Update() { if (!IsPaused.Value) - FramedClock.ProcessFrame(); + GameplayClock.ProcessFrame(); base.Update(); } /// - /// Invoked when the value of is changed to start or stop the clock. + /// Invoked when the value of is changed to start or stop the clock. /// /// Whether the clock should now be paused. protected virtual void OnIsPausedChanged(ValueChangedEvent isPaused) { if (isPaused.NewValue) - decoupledClock.Stop(); + GameplayClock.Stop(); else - decoupledClock.Start(); + GameplayClock.Start(); } - /// - /// Creates the final which is exposed via DI to be used by gameplay components. - /// - /// - /// Any intermediate clocks such as platform offsets should be applied here. - /// - /// The providing the source time. - /// The final . - protected virtual IFrameBasedClock CreateGameplayClock(IFrameBasedClock source) => source; - #region IAdjustableClock bool IAdjustableClock.Seek(double position) @@ -204,15 +186,15 @@ namespace osu.Game.Screens.Play double IAdjustableClock.Rate { - get => FramedClock.Rate; + get => GameplayClock.Rate; set => throw new NotSupportedException(); } - public double Rate => FramedClock.Rate; + public double Rate => GameplayClock.Rate; - public double CurrentTime => FramedClock.CurrentTime; + public double CurrentTime => GameplayClock.CurrentTime; - public bool IsRunning => FramedClock.IsRunning; + public bool IsRunning => GameplayClock.IsRunning; #endregion @@ -221,11 +203,11 @@ namespace osu.Game.Screens.Play // Handled via update. Don't process here to safeguard from external usages potentially processing frames additional times. } - public double ElapsedFrameTime => FramedClock.ElapsedFrameTime; + public double ElapsedFrameTime => GameplayClock.ElapsedFrameTime; - public double FramesPerSecond => FramedClock.FramesPerSecond; + public double FramesPerSecond => GameplayClock.FramesPerSecond; - public FrameTimeInfo TimeInfo => FramedClock.TimeInfo; + public FrameTimeInfo TimeInfo => GameplayClock.TimeInfo; public double TrueGameplayRate { diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index d26f0c6311..51f57f27b8 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -4,8 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework; -using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; @@ -13,8 +11,6 @@ using osu.Framework.Graphics; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Configuration; -using osu.Game.Database; namespace osu.Game.Screens.Play { @@ -43,28 +39,10 @@ namespace osu.Game.Screens.Play Precision = 0.1, }; - private double totalAppliedOffset => userBeatmapOffsetClock.RateAdjustedOffset + userGlobalOffsetClock.RateAdjustedOffset + platformOffsetClock.RateAdjustedOffset; - - private readonly BindableDouble pauseFreqAdjust = new BindableDouble(); // Important that this starts at zero, matching the paused state of the clock. - private readonly WorkingBeatmap beatmap; - private OffsetCorrectionClock userGlobalOffsetClock = null!; - private OffsetCorrectionClock userBeatmapOffsetClock = null!; - private OffsetCorrectionClock platformOffsetClock = null!; - - private Bindable userAudioOffset = null!; - - private IDisposable? beatmapOffsetSubscription; - private readonly double skipTargetTime; - [Resolved] - private RealmAccess realm { get; set; } = null!; - - [Resolved] - private OsuConfigManager config { get; set; } = null!; - private readonly List> nonGameplayAdjustments = new List>(); public override IEnumerable NonGameplayAdjustments => nonGameplayAdjustments.Select(b => b.Value); @@ -75,7 +53,7 @@ namespace osu.Game.Screens.Play /// The beatmap to be used for time and metadata references. /// The latest time which should be used when introducing gameplay. Will be used when skipping forward. public MasterGameplayClockContainer(WorkingBeatmap beatmap, double skipTargetTime) - : base(beatmap.Track) + : base(beatmap.Track, true) { this.beatmap = beatmap; this.skipTargetTime = skipTargetTime; @@ -85,14 +63,6 @@ namespace osu.Game.Screens.Play { base.LoadComplete(); - userAudioOffset = config.GetBindable(OsuSetting.AudioOffset); - userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true); - - beatmapOffsetSubscription = realm.SubscribeToPropertyChanged( - r => r.Find(beatmap.BeatmapInfo.ID)?.UserSettings, - settings => settings.Offset, - val => userBeatmapOffsetClock.Offset = val); - // Reset may have been called externally before LoadComplete. // If it was, and the clock is in a playing state, we want to ensure that it isn't stopped here. bool isStarted = !IsPaused.Value; @@ -133,14 +103,14 @@ namespace osu.Game.Screens.Play // During normal operation, the source is stopped after performing a frequency ramp. if (isPaused.NewValue) { - this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ => + this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 0, 200, Easing.Out).OnComplete(_ => { if (IsPaused.Value == isPaused.NewValue) base.OnIsPausedChanged(isPaused); }); } else - this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In); + this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 1, 200, Easing.In); } else { @@ -148,11 +118,11 @@ namespace osu.Game.Screens.Play base.OnIsPausedChanged(isPaused); // If not yet loaded, we still want to ensure relevant state is correct, as it is used for offset calculations. - pauseFreqAdjust.Value = isPaused.NewValue ? 0 : 1; + GameplayClock.ExternalPauseFrequencyAdjust.Value = isPaused.NewValue ? 0 : 1; // We must also process underlying gameplay clocks to update rate-adjusted offsets with the new frequency adjustment. // Without doing this, an initial seek may be performed with the wrong offset. - FramedClock.ProcessFrame(); + GameplayClock.ProcessFrame(); } } @@ -162,48 +132,23 @@ namespace osu.Game.Screens.Play base.Start(); } - /// - /// Seek to a specific time in gameplay. - /// - /// - /// Adjusts for any offsets which have been applied (so the seek may not be the expected point in time on the underlying audio track). - /// - /// The destination time to seek to. - public override void Seek(double time) - { - // remove the offset component here because most of the time we want the seek to be aligned to gameplay, not the audio track. - // we may want to consider reversing the application of offsets in the future as it may feel more correct. - base.Seek(time - totalAppliedOffset); - } - /// /// Skip forward to the next valid skip point. /// public void Skip() { - if (FramedClock.CurrentTime > skipTargetTime - MINIMUM_SKIP_TIME) + if (GameplayClock.CurrentTime > skipTargetTime - MINIMUM_SKIP_TIME) return; double skipTarget = skipTargetTime - MINIMUM_SKIP_TIME; - if (FramedClock.CurrentTime < 0 && skipTarget > 6000) + if (GameplayClock.CurrentTime < 0 && skipTarget > 6000) // double skip exception for storyboards with very long intros skipTarget = 0; Seek(skipTarget); } - protected override IFrameBasedClock CreateGameplayClock(IFrameBasedClock source) - { - // Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited. - // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. - platformOffsetClock = new OffsetCorrectionClock(source, pauseFreqAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; - - // the final usable gameplay clock with user-set offsets applied. - userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock, pauseFreqAdjust); - return userBeatmapOffsetClock = new OffsetCorrectionClock(userGlobalOffsetClock, pauseFreqAdjust); - } - /// /// Changes the backing clock to avoid using the originally provided track. /// @@ -224,10 +169,10 @@ namespace osu.Game.Screens.Play if (SourceClock is not Track track) return; - track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); + track.AddAdjustment(AdjustableProperty.Frequency, GameplayClock.ExternalPauseFrequencyAdjust); track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); - nonGameplayAdjustments.Add(pauseFreqAdjust); + nonGameplayAdjustments.Add(GameplayClock.ExternalPauseFrequencyAdjust); nonGameplayAdjustments.Add(UserPlaybackRate); speedAdjustmentsApplied = true; @@ -241,10 +186,10 @@ namespace osu.Game.Screens.Play if (SourceClock is not Track track) return; - track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); + track.RemoveAdjustment(AdjustableProperty.Frequency, GameplayClock.ExternalPauseFrequencyAdjust); track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); - nonGameplayAdjustments.Remove(pauseFreqAdjust); + nonGameplayAdjustments.Remove(GameplayClock.ExternalPauseFrequencyAdjust); nonGameplayAdjustments.Remove(UserPlaybackRate); speedAdjustmentsApplied = false; @@ -253,7 +198,6 @@ namespace osu.Game.Screens.Play protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - beatmapOffsetSubscription?.Dispose(); removeSourceClockAdjustments(); }