From ef763e9bd7f28ab95b3f5b742c08e84cd223eed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Dec 2021 10:59:05 +0100 Subject: [PATCH 1/4] Add failing test for storyboard not tracking current track --- .../TestSceneBackgroundScreenDefault.cs | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs index 844fe7705a..884e74346b 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs @@ -6,18 +6,24 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.Sprites; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Screens; using osu.Game.Screens.Backgrounds; using osu.Game.Skinning; +using osu.Game.Storyboards; +using osu.Game.Storyboards.Drawables; using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual.Background @@ -129,6 +135,46 @@ namespace osu.Game.Tests.Visual.Background AddAssert("top level background reused existing", () => screen.CheckLastLoadChange() == false); } + [Test] + public void TestBeatmapBackgroundWithStoryboardClockAlwaysUsesCurrentTrack() + { + BackgroundScreenBeatmap nestedScreen = null; + WorkingBeatmap originalWorking = null; + + setSupporter(true); + setSourceMode(BackgroundSource.BeatmapWithStoryboard); + + AddStep("change beatmap", () => originalWorking = Beatmap.Value = createTestWorkingBeatmapWithStoryboard()); + AddAssert("background changed", () => screen.CheckLastLoadChange() == true); + AddUntilStep("wait for beatmap background to be loaded", () => getCurrentBackground()?.GetType() == typeof(BeatmapBackgroundWithStoryboard)); + + AddStep("start music", () => MusicController.Play()); + AddUntilStep("storyboard clock running", () => screen.ChildrenOfType().SingleOrDefault()?.Clock.IsRunning == true); + + // of note, this needs to be a type that doesn't match BackgroundScreenDefault else it is silently not pushed by the background stack. + AddStep("push new background to stack", () => stack.Push(nestedScreen = new BackgroundScreenBeatmap(Beatmap.Value))); + AddUntilStep("wait for screen to load", () => nestedScreen.IsLoaded && nestedScreen.IsCurrentScreen()); + + // we're testing a case where scheduling may be used to avoid issues, so ensure the scheduler is no longer running. + AddUntilStep("wait for top level not alive", () => !screen.IsAlive); + + AddStep("stop music", () => MusicController.Stop()); + AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithStoryboard()); + AddStep("change beatmap back", () => Beatmap.Value = originalWorking); + AddStep("restart music", () => MusicController.Play()); + + AddAssert("top level background hasn't changed yet", () => screen.CheckLastLoadChange() == null); + + AddStep("pop screen back to top level", () => screen.MakeCurrent()); + + AddStep("top level screen is current", () => screen.IsCurrentScreen()); + AddAssert("top level background reused existing", () => screen.CheckLastLoadChange() == false); + AddUntilStep("storyboard clock running", () => screen.ChildrenOfType().Single().Clock.IsRunning); + + AddStep("stop music", () => MusicController.Stop()); + AddStep("restore default beatmap", () => Beatmap.SetDefault()); + } + [Test] public void TestBackgroundTypeSwitch() { @@ -198,6 +244,7 @@ namespace osu.Game.Tests.Visual.Background }); private WorkingBeatmap createTestWorkingBeatmapWithUniqueBackground() => new UniqueBackgroundTestWorkingBeatmap(Audio); + private WorkingBeatmap createTestWorkingBeatmapWithStoryboard() => new TestWorkingBeatmapWithStoryboard(Audio); private class TestBackgroundScreenDefault : BackgroundScreenDefault { @@ -233,6 +280,51 @@ namespace osu.Game.Tests.Visual.Background protected override Texture GetBackground() => new Texture(1, 1); } + private class TestWorkingBeatmapWithStoryboard : TestWorkingBeatmap + { + public TestWorkingBeatmapWithStoryboard(AudioManager audioManager) + : base(new Beatmap(), createStoryboard(), audioManager) + { + } + + protected override Track GetBeatmapTrack() => new TrackVirtual(100000); + + private static Storyboard createStoryboard() + { + var storyboard = new Storyboard(); + storyboard.Layers.Last().Add(new TestStoryboardElement()); + return storyboard; + } + + private class TestStoryboardElement : IStoryboardElementWithDuration + { + public string Path => string.Empty; + public bool IsDrawable => true; + public double StartTime => double.MinValue; + public double EndTime => double.MaxValue; + + public Drawable CreateDrawable() => new DrawableTestStoryboardElement(); + } + + private class DrawableTestStoryboardElement : OsuSpriteText + { + public override bool RemoveWhenNotAlive => false; + + public DrawableTestStoryboardElement() + { + Anchor = Origin = Anchor.Centre; + Font = OsuFont.Default.With(size: 32); + Text = "(not started)"; + } + + protected override void Update() + { + base.Update(); + Text = Time.Current.ToString("N2"); + } + } + } + private void setCustomSkin() { // feign a skin switch. this doesn't do anything except force CurrentSkin to become a LegacySkin. From 9f8ad9f833f5057150297c455fee597dc6bc7db1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Dec 2021 11:28:20 +0100 Subject: [PATCH 2/4] Fix menu background storyboard stopping after track reload --- .../BeatmapBackgroundWithStoryboard.cs | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs index 6a42e83305..71b1ef2c18 100644 --- a/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs +++ b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs @@ -1,17 +1,25 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; using osu.Game.Beatmaps; +using osu.Game.Overlays; using osu.Game.Storyboards.Drawables; namespace osu.Game.Graphics.Backgrounds { public class BeatmapBackgroundWithStoryboard : BeatmapBackground { + private InterpolatingFramedClock storyboardClock = null!; + + [Resolved(CanBeNull = true)] + private MusicController? musicController { get; set; } + public BeatmapBackgroundWithStoryboard(WorkingBeatmap beatmap, string fallbackTextureName = "Backgrounds/bg1") : base(beatmap, fallbackTextureName) { @@ -30,8 +38,33 @@ namespace osu.Game.Graphics.Backgrounds { RelativeSizeAxes = Axes.Both, Volume = { Value = 0 }, - Child = new DrawableStoryboard(Beatmap.Storyboard) { Clock = new InterpolatingFramedClock(Beatmap.Track) } + Child = new DrawableStoryboard(Beatmap.Storyboard) { Clock = storyboardClock = new InterpolatingFramedClock(Beatmap.Track) } }, AddInternal); } + + protected override void LoadComplete() + { + base.LoadComplete(); + if (musicController != null) + musicController.TrackChanged += onTrackChanged; + } + + private void onTrackChanged(WorkingBeatmap newBeatmap, TrackChangeDirection _) + { + if (newBeatmap != Beatmap) + return; + + // `MusicController` will sometimes reload the track, even when the working beatmap technically hasn't changed. + // ensure that the storyboard's clock is always using the latest track instance. + storyboardClock.ChangeSource(newBeatmap.Track); + storyboardClock.ProcessFrame(); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + if (musicController != null) + musicController.TrackChanged -= onTrackChanged; + } } } From 568cab68086792a3ed9cf863f63f25331ea13ae4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Dec 2021 21:13:04 +0100 Subject: [PATCH 3/4] Add clarification comment about explicit `ProcessFrame()` call --- .../Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs index 71b1ef2c18..c5c8a25f4a 100644 --- a/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs +++ b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs @@ -57,6 +57,9 @@ namespace osu.Game.Graphics.Backgrounds // `MusicController` will sometimes reload the track, even when the working beatmap technically hasn't changed. // ensure that the storyboard's clock is always using the latest track instance. storyboardClock.ChangeSource(newBeatmap.Track); + // more often than not, the previous source track's time will be in the future relative to the new source track. + // explicitly process a single frame so that `InterpolatingFramedClock`'s interpolation logic is bypassed + // and the storyboard clock is correctly rewound to the source track's time exactly. storyboardClock.ProcessFrame(); } From 76d5225bb9ee9bfcc599bf9ec6f769c3658a4b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Jan 2022 22:00:04 +0100 Subject: [PATCH 4/4] Rewrite storyboard clock management in slightly different way --- .../Backgrounds/BeatmapBackgroundWithStoryboard.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs index c5c8a25f4a..56ef87c1f4 100644 --- a/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs +++ b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs @@ -15,7 +15,7 @@ namespace osu.Game.Graphics.Backgrounds { public class BeatmapBackgroundWithStoryboard : BeatmapBackground { - private InterpolatingFramedClock storyboardClock = null!; + private readonly InterpolatingFramedClock storyboardClock; [Resolved(CanBeNull = true)] private MusicController? musicController { get; set; } @@ -23,6 +23,7 @@ namespace osu.Game.Graphics.Backgrounds public BeatmapBackgroundWithStoryboard(WorkingBeatmap beatmap, string fallbackTextureName = "Backgrounds/bg1") : base(beatmap, fallbackTextureName) { + storyboardClock = new InterpolatingFramedClock(); } [BackgroundDependencyLoader] @@ -38,7 +39,7 @@ namespace osu.Game.Graphics.Backgrounds { RelativeSizeAxes = Axes.Both, Volume = { Value = 0 }, - Child = new DrawableStoryboard(Beatmap.Storyboard) { Clock = storyboardClock = new InterpolatingFramedClock(Beatmap.Track) } + Child = new DrawableStoryboard(Beatmap.Storyboard) { Clock = storyboardClock } }, AddInternal); } @@ -47,9 +48,13 @@ namespace osu.Game.Graphics.Backgrounds base.LoadComplete(); if (musicController != null) musicController.TrackChanged += onTrackChanged; + + updateStoryboardClockSource(Beatmap); } - private void onTrackChanged(WorkingBeatmap newBeatmap, TrackChangeDirection _) + private void onTrackChanged(WorkingBeatmap newBeatmap, TrackChangeDirection _) => updateStoryboardClockSource(newBeatmap); + + private void updateStoryboardClockSource(WorkingBeatmap newBeatmap) { if (newBeatmap != Beatmap) return;