From 80e1568e974f0cfa5efd0e8a4b1c2298c7fffefc Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 24 Apr 2019 13:44:21 +0900
Subject: [PATCH 1/5] Fix FrameStabilityContainer performing frame-stable seeks

---
 osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
index c307520aca..2866ce08ed 100644
--- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
+++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
@@ -68,6 +68,8 @@ namespace osu.Game.Rulesets.UI
 
         private const double sixty_frame_time = 1000.0 / 60;
 
+        private bool firstConsumption = true;
+
         public override bool UpdateSubTree()
         {
             requireMoreUpdateLoops = true;
@@ -103,7 +105,14 @@ namespace osu.Game.Rulesets.UI
 
             try
             {
-                if (Math.Abs(manualClock.CurrentTime - newProposedTime) > sixty_frame_time * 1.2f)
+                if (firstConsumption)
+                {
+                    // On the first update, frame-stability seeking would result in unexpected/unwanted behaviour.
+                    // Instead we perform an initial seek to the proposed time.
+                    manualClock.CurrentTime = newProposedTime;
+                    firstConsumption = false;
+                }
+                else if (Math.Abs(manualClock.CurrentTime - newProposedTime) > sixty_frame_time * 1.2f)
                 {
                     newProposedTime = newProposedTime > manualClock.CurrentTime
                         ? Math.Min(newProposedTime, manualClock.CurrentTime + sixty_frame_time)

From 60328cf1fb840344d3919ee80638ffe01b5010cc Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 24 Apr 2019 15:23:00 +0900
Subject: [PATCH 2/5] Ensure FrameStabilityContainer's ElapsedTime is zero on
 initial seek

---
 osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
index 2866ce08ed..ad15bcf057 100644
--- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
+++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
@@ -110,6 +110,10 @@ namespace osu.Game.Rulesets.UI
                     // On the first update, frame-stability seeking would result in unexpected/unwanted behaviour.
                     // Instead we perform an initial seek to the proposed time.
                     manualClock.CurrentTime = newProposedTime;
+
+                    // do a second process to clear out ElapsedTime
+                    framedClock.ProcessFrame();
+
                     firstConsumption = false;
                 }
                 else if (Math.Abs(manualClock.CurrentTime - newProposedTime) > sixty_frame_time * 1.2f)

From a3e7ec0a14b85acec83e68963bb5d44e99a96300 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 24 Apr 2019 15:23:31 +0900
Subject: [PATCH 3/5] Add tests for FrameStabilityContainer

---
 .../TestCaseFrameStabilityContainer.cs        | 149 ++++++++++++++++++
 1 file changed, 149 insertions(+)
 create mode 100644 osu.Game.Tests/Visual/Gameplay/TestCaseFrameStabilityContainer.cs

diff --git a/osu.Game.Tests/Visual/Gameplay/TestCaseFrameStabilityContainer.cs b/osu.Game.Tests/Visual/Gameplay/TestCaseFrameStabilityContainer.cs
new file mode 100644
index 0000000000..ca0607cd6b
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestCaseFrameStabilityContainer.cs
@@ -0,0 +1,149 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Timing;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+    public class TestCaseFrameStabilityContainer : OsuTestCase
+    {
+        private ManualClock manualClock;
+
+        private Container mainContainer;
+
+        private ClockConsumingChild consumer;
+
+        [SetUp]
+        public void SetUp()
+        {
+            Child = mainContainer = new Container
+            {
+                RelativeSizeAxes = Axes.Both,
+                Clock = new FramedClock(manualClock = new ManualClock()),
+            };
+        }
+
+        [Test]
+        public void TestLargeJumps()
+        {
+            createStabilityContainer();
+            seekManualTo(100000);
+
+            confirmSeek(100000);
+            checkFrameCount(6000);
+
+            seekManualTo(0);
+
+            confirmSeek(0);
+            checkFrameCount(12000);
+        }
+
+        [Test]
+        public void TestSmallJumps()
+        {
+            createStabilityContainer();
+            seekManualTo(40);
+
+            confirmSeek(40);
+            checkFrameCount(3);
+
+            seekManualTo(0);
+
+            confirmSeek(0);
+            checkFrameCount(6);
+        }
+
+        [Test]
+        public void TestSingleFrameJump()
+        {
+            createStabilityContainer();
+            seekManualTo(8);
+            confirmSeek(8);
+            checkFrameCount(1);
+
+            seekManualTo(16);
+            confirmSeek(16);
+            checkFrameCount(2);
+        }
+
+        [Test]
+        public void TestInitialSeek()
+        {
+            seekManualTo(100000);
+
+            createStabilityContainer();
+
+            confirmSeek(100000);
+            checkFrameCount(0);
+        }
+
+        private void createStabilityContainer() => AddStep("create container", () => mainContainer.Child = new FrameStabilityContainer().WithChild(consumer = new ClockConsumingChild()));
+
+        private void seekManualTo(double time) => AddStep($"seek manual clock to {time}", () => manualClock.CurrentTime = time);
+
+        private void confirmSeek(double time) => AddUntilStep($"wait for seek to {time}", () => consumer.Clock.CurrentTime == time);
+
+        private void checkFrameCount(int frames) =>
+            AddAssert($"elapsed frames is {frames}", () => consumer.ElapsedFrames == frames);
+
+        public class ClockConsumingChild : CompositeDrawable
+        {
+            private readonly OsuSpriteText text;
+            private readonly OsuSpriteText text2;
+            private readonly OsuSpriteText text3;
+
+            public ClockConsumingChild()
+            {
+                Anchor = Anchor.Centre;
+                Origin = Anchor.Centre;
+
+                InternalChildren = new Drawable[]
+                {
+                    new FillFlowContainer
+                    {
+                        RelativeSizeAxes = Axes.Both,
+                        Direction = FillDirection.Vertical,
+                        Children = new Drawable[]
+                        {
+                            text = new OsuSpriteText
+                            {
+                                Anchor = Anchor.Centre,
+                                Origin = Anchor.Centre,
+                            },
+                            text2 = new OsuSpriteText
+                            {
+                                Anchor = Anchor.Centre,
+                                Origin = Anchor.Centre,
+                            },
+                            text3 = new OsuSpriteText
+                            {
+                                Anchor = Anchor.Centre,
+                                Origin = Anchor.Centre,
+                            },
+                        }
+                    },
+                };
+            }
+
+            public int ElapsedFrames;
+
+            protected override void Update()
+            {
+                base.Update();
+
+                if (Clock.ElapsedFrameTime != 0)
+                    ElapsedFrames++;
+
+                text.Text = $"current time: {Clock.CurrentTime:F0}";
+                if (Clock.ElapsedFrameTime != 0)
+                    text2.Text = $"last elapsed frame time: {Clock.ElapsedFrameTime:F0}";
+                text3.Text = $"total frames: {ElapsedFrames:F0}";
+            }
+        }
+    }
+}

From 3b36a4982deabef006ccd97909c3f2730abbe51e Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 24 Apr 2019 15:46:21 +0900
Subject: [PATCH 4/5] Fix tests running under nUnit and running multiple times
 in concession

---
 .../Gameplay/TestCaseFrameStabilityContainer.cs      | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/osu.Game.Tests/Visual/Gameplay/TestCaseFrameStabilityContainer.cs b/osu.Game.Tests/Visual/Gameplay/TestCaseFrameStabilityContainer.cs
index ca0607cd6b..bc30648566 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestCaseFrameStabilityContainer.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestCaseFrameStabilityContainer.cs
@@ -4,6 +4,7 @@
 using NUnit.Framework;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
 using osu.Framework.Timing;
 using osu.Game.Graphics.Sprites;
 using osu.Game.Rulesets.UI;
@@ -12,14 +13,13 @@ namespace osu.Game.Tests.Visual.Gameplay
 {
     public class TestCaseFrameStabilityContainer : OsuTestCase
     {
-        private ManualClock manualClock;
+        private readonly ManualClock manualClock;
 
-        private Container mainContainer;
+        private readonly Container mainContainer;
 
         private ClockConsumingChild consumer;
 
-        [SetUp]
-        public void SetUp()
+        public TestCaseFrameStabilityContainer()
         {
             Child = mainContainer = new Container
             {
@@ -31,6 +31,7 @@ namespace osu.Game.Tests.Visual.Gameplay
         [Test]
         public void TestLargeJumps()
         {
+            seekManualTo(0);
             createStabilityContainer();
             seekManualTo(100000);
 
@@ -46,6 +47,7 @@ namespace osu.Game.Tests.Visual.Gameplay
         [Test]
         public void TestSmallJumps()
         {
+            seekManualTo(0);
             createStabilityContainer();
             seekManualTo(40);
 
@@ -61,6 +63,7 @@ namespace osu.Game.Tests.Visual.Gameplay
         [Test]
         public void TestSingleFrameJump()
         {
+            seekManualTo(0);
             createStabilityContainer();
             seekManualTo(8);
             confirmSeek(8);
@@ -75,7 +78,6 @@ namespace osu.Game.Tests.Visual.Gameplay
         public void TestInitialSeek()
         {
             seekManualTo(100000);
-
             createStabilityContainer();
 
             confirmSeek(100000);

From f273f5daae7e6224ead6491e491b391fc261db07 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 24 Apr 2019 15:55:51 +0900
Subject: [PATCH 5/5] Remove unnecessary using statement

---
 .../Visual/Gameplay/TestCaseFrameStabilityContainer.cs           | 1 -
 1 file changed, 1 deletion(-)

diff --git a/osu.Game.Tests/Visual/Gameplay/TestCaseFrameStabilityContainer.cs b/osu.Game.Tests/Visual/Gameplay/TestCaseFrameStabilityContainer.cs
index bc30648566..5cd01fe9a8 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestCaseFrameStabilityContainer.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestCaseFrameStabilityContainer.cs
@@ -4,7 +4,6 @@
 using NUnit.Framework;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
-using osu.Framework.Testing;
 using osu.Framework.Timing;
 using osu.Game.Graphics.Sprites;
 using osu.Game.Rulesets.UI;