Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new DecouplingClock and obsolete DecoupleableInterpolatingFramedClock #6001

Merged
merged 44 commits into from
Sep 26, 2023

Conversation

peppy
Copy link
Member

@peppy peppy commented Sep 22, 2023

Clocks, explained

Clocks have been painful, but they were never intended to be. The system should conceptually be simple:

  • All clocks have a CurrentTime.
  • A framed clock (IFrameBasedClock) is processed (via ProcessFrame) by the consumer every frame and can therefore provide elapsed time.
  • An adjustable clock (IAdjustableClock) exposes the ability to be Started, Stopped, Seeked and Reset. This is most commonly used to bring audio tracks into the clock system.
  • A clock may contain a source clock (via ISourceChangeableClock), to allow nesting clocks inside each other. This can be used to add more complex behaviours to existing clocks via composition.

These are the basic governing rules. Let's look at the two main cases for nesting clocks:

  • Interpolation via InterpolatingFramedClock (aka allowing CurrentTime to be more precise than the underlying audio subsystem via interpolation). This one is pretty simple, and has been recently cleaned up via Tidy up InterpolatingFramedClock #5992.
  • Decoupling (aka allowing a track to play in negative time, or beyond the end of the track, for lead-in / lead-out purposes), historically via DecoupleableInterpolatingFramedClock. This is the thorn in the clock system.

What was wrong with DecoupleableInterpolatingFramedClock?

  • It was inheriting InterpolatingFramedClock, overriding some pieces but also adding its own complexity on top. It was nigh impossible to follow the cross-interactions between these two implementations. It should have been implemented as its own stand-alone implementation which only decoupled
  • It was doing random shit. Like literally, wild west in terms of what behaviours it decided to apply. These were "well tested", but also meant workarounds all over the place osu! side because attempting to fix anything may results in the rest of the house of cards falling down.

So really, the only thing that needs to be fixed (from the framework perspective) when I've said "clocks are fucked" many times over the years is this one implementation. I've rewritten it from scratch with everything I've learned over the years and with the above mistakes in mind. The new class is called 'DecouplingClock'.

Behaviour differences

Is now a stand-alone implementation

DecouplingClock no longer inherits from InterpolatingClock. This reduces about half of the original complexity.

Existing usages of DecoupleableInterpolatingFramedClock will need to nest a DecouplingClock inside of an InterpolatingClock. Note the order here – DecouplingClock should directly encapsulate the ITrack or similar source for best behaviour. Introducing decoupling in the middle of a framed hierarchy will likely not work well.

Not frame based

Update: now is

DecouplingClock is intentionally re-implemented as a non-frame-based clock for simplicity. My main reason for this was to ensure that we are exposing "instantaneous" values to keep complexity to a minimum. ie. we don't need to unnecessarily track and compare time values.

To make this work, time tracking for the case where time is decoupled is done when CurrentTime is called. I can understand if this may be seen as a bit weird, but I do think it's better than making it an IFrameBasedClock – at least until we can better define how things like Seek (should a seek on a framed clock immediately update CurrentTime or only after a ProcessFrame?) and IsRunnning (should running state of the nested clock immediately update or wait for next frame?) work.

In practice, this seems to work quite well. It also hints to consumers to put the DecoupledClock inside any further adjustments like interpolation.

A potential caveat here is that if a framed clock is provided as the decoupling source (as is done in some osu!-side usages such as tests and spectator), the source will need to be externally processed.

Less mutations

When AllowDecoupling is set to false, the new implementation will operate in a completely transparent way. No extra operations will be performed on the Source.

When AllowDecoupling is set to true, there are only two cases where DecouplingClock will perform operations on the Source:

  • If a DecouplingClock.Seek is requested by the user and the source returns false (aka "cannot seek"), Stop will be called on the source to ensure it stops. This is to cover the scenario where calling Seek(-1000) on a bass track will seek it to 0 and continue playing.
  • After a negative seek which failed on the Source, at the boundary of CurrentTime==0, the source will be Started once, hinting that it is likely able to take over.

Touching on the CurrentTime==0 special case

To keep things simple, the implementation assumes that if the Source could not seek to a negative time value, it will likely be able to again after reaching zero. This is the only time a DecouplingClock will Start the underlying source.

This is a very simplistic approach to handle the main use case of decoupling that we have. If there a new use case where the Source can say, only track time from 1000..6000 and we perform a seek to 0, the Source will not be hinted to start when reaching 1000.

Fixing this would require some extra thought from a performance perspective (we likely don't want to call Start() on the Source every time CurrentTime is retrieved, for instance) without further hinting at what range a clock can actually handle.

A further refinement could be doing precisely this – DecouplingClock can only operation on a source which implements a new interface IRangeLimitedClock { (double, double) ValidRange { get ; } }.

Source must be IAdjustableClock

This is another arbitrary limitation to keep complexity down. DecouplingClock needs to be IAdjustableClock in order to track operations performed against the source. Without this we would have a very hard time knowing what state we are in.

If necessary in the future, we could allow sources to be non-adjustable, but methods like Start/Stop/Seek would need to throw as we rationally cannot make them work.

All adjustable operations should be performed on DecouplingClock

As touched upon, all IAdjustableClock operations should be performed directly on the DecouplingClock, not the source. Operations performed directly on the source could lead to incorrect behaviour, such as Seeks being undone when in a decoupled state and stopped.

Current state

I'm publishing this as a draft to get eyes on it and make sure the overall direction is agreeable. I'd estimate there's another day's work to get this ready for prime time, and I'll probably push ahead even if this doesn't get any eyes on it immediately.

I've left my full commit history intact, which means some of the earlier commits may be taking things in a slightly different direction. I want these around in case I need to backtrack based on feedback. And who knows, maybe it'll explain the direction I took better.

osu! integration

Works against osu! with some changes. Note that there are a few straggler tests that need fixing, but the hard ones are handled. I've tried to explain changes in commit messages, but will continue to refine things.

One cool thing is that it seems like a few hacks we had in place can be removed. Needs some more rigorous testing, though.

Test coverage

There's a few tests missing (see inline TODOs) which I will add before marking this as non-draft. These came up as I adjusted the implementation to pass osu!-side tests, for what it's worth.

Regarding obsoleting the old implementation

I've searched across github and cannot find any consumers using the old implementation (phew). We could potentially just nuke it from existence once osu! has switched across.


Prerequisite for ppy/osu#24811.

Breaking changes

DecoupleableInterpolatingFramedClock is obsoleted. Use DecouplingClock instead

See #6001 for more details on a migration path.

osu.Framework/Timing/InterpolatingFramedClock.cs Outdated Show resolved Hide resolved
osu.Framework/Timing/DecouplingClock.cs Outdated Show resolved Hide resolved
osu.Framework/Timing/DecouplingClock.cs Outdated Show resolved Hide resolved
osu.Framework/Timing/DecouplingClock.cs Outdated Show resolved Hide resolved
osu.Framework/Timing/DecouplingClock.cs Outdated Show resolved Hide resolved
osu.Framework/Timing/DecouplingClock.cs Outdated Show resolved Hide resolved
osu.Framework.Tests/Visual/Clocks/TestSceneClocks.cs Outdated Show resolved Hide resolved
osu.Framework.Tests/Visual/Clocks/TestSceneClocks.cs Outdated Show resolved Hide resolved
osu.Framework.Tests/Clocks/DecouplingClockTest.cs Outdated Show resolved Hide resolved
@bdach
Copy link
Collaborator

bdach commented Sep 22, 2023

Generally this seems okay aside from the above review, which is mostly minor quibbles, but I'll highlight one point in particular:

DecouplingClock is intentionally re-implemented as a non-frame-based clock for simplicity. My main reason for this was to ensure that we are exposing "instantaneous" values to keep complexity to a minimum. ie. we don't need to unnecessarily track and compare time values.

I'm not sure if it's just me, but in a way, to me, reading this implementation, I have this vague feeling that DecouplingClock seems to be essentially a framed clock, without explicitly being one or implementing the interfaces. It's just that the ProcessFrame() calls you would normally issue manually, with the current implementation, are sorta-kinda "implicitly" performed for you when observing the decoupling clock's state in any way - because that's when the clock decides to check the source clock and maybe possibly do something on it or update its own internal state to match it. Which means that the timing of when you observe state now begins to matter, and if you observe the state in a "wrong" way, you may potentially get unexpected results.

I'm not necessarily making value judgements on that yet, but it feels like a potential rake one may step on.

@peppy peppy marked this pull request as ready for review September 22, 2023 16:33
@frenzibyte frenzibyte self-requested a review September 23, 2023 12:09
decouplingClock.ChangeSource(secondSource);
decouplingClock.ProcessFrame();

Assert.That(secondSource.CurrentTime, Is.EqualTo(second_source_time));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was expecting this to be Assert.That(decouplingClock.CurrentTime, ...). Maybe both should be asserted here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, this was a typo. Also regressed at some point in my changes, so I've fixed it by transferring the current time value.

Comment on lines +238 to +247
[Test]
public void TestSeekPositiveWhileDecoupling()
{
decouplingClock.AllowDecoupling = true;
Assert.That(decouplingClock.Seek(1000), Is.True);
decouplingClock.ProcessFrame();

Assert.That(source.CurrentTime, Is.EqualTo(1000));
Assert.That(decouplingClock.CurrentTime, Is.EqualTo(1000));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DecouplingClock states it allows seeking beyond the seekable range, but the source clock in this case has a range (0, +inf) so it isn't testing that case like TestSeekNegativeWhileDecoupling is.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why I didn't notice nothing ever actually sets TestClockWithRange.MaxTime to anything non-default...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 98f832f.

}

[Test]
public void TestForwardPlaybackOverZeroBoundary()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should also be a test that shows the decoupled clock is able to keep running beyond the seekable range - the inverse of TestBackwardPlaybackOverZeroBoundary.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, added in 28d159e.

Comment on lines 37 to 53
public bool IsRunning
{
get
{
// Always use the source clock's running state if it's running.
if (Source.IsRunning)
return isRunning = true;

// If we're allowed to be decoupled, maintain our internal running state.
if (AllowDecoupling)
return isRunning;

// Otherwise we're definitely not running.
Debug.Assert(!Source.IsRunning);
return isRunning = false;
}
}
Copy link
Contributor

@smoogipoo smoogipoo Sep 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just me, but it weirds me out that a getter is mutating the value of isRunning.

Is this code particularly important in that it needs to be referenced every frame for the clock to be in a good state?

For instance, and I'll say ahead of time I haven't tested this, but suppose:

  • Decoupling is not allowed
  • The decoupling clock is .Start()ed (at 0)
  • The clock moves beyond trackLength at which point the source clock stops
  • From here I assume ProcessFrame() becomes a no-op, but isRunning remains true in an internally-decoupled state.
  • A .Seek() to 0 on the decoupling clock would then re-start the track. This operation should (if my theory is correct) change behaviour depending on whether the IsRunning property has been referenced.

Is this expected behaviour?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to change the implementation to match InterpolatingFramedClock for consistency?

diff --git a/osu.Framework.Tests/Clocks/DecouplingFramedClockTest.cs b/osu.Framework.Tests/Clocks/DecouplingFramedClockTest.cs
index a77817f0c..9025b1cd2 100644
--- a/osu.Framework.Tests/Clocks/DecouplingFramedClockTest.cs
+++ b/osu.Framework.Tests/Clocks/DecouplingFramedClockTest.cs
@@ -35,6 +35,7 @@ public void TestStartFromDecoupling(bool allowDecoupling)
             Assert.That(decouplingClock.IsRunning, Is.False);
 
             decouplingClock.Start();
+            decouplingClock.ProcessFrame();
 
             Assert.That(source.IsRunning, Is.True);
             Assert.That(decouplingClock.IsRunning, Is.True);
@@ -50,6 +51,7 @@ public void TestStartFromSource(bool allowDecoupling)
             Assert.That(decouplingClock.IsRunning, Is.False);
 
             source.Start();
+            decouplingClock.ProcessFrame();
 
             Assert.That(source.IsRunning, Is.True);
             Assert.That(decouplingClock.IsRunning, Is.True);
@@ -126,14 +128,17 @@ public void TestChangeSourceUpdatesToCorrectSourceState(bool allowDecoupling)
             decouplingClock.AllowDecoupling = allowDecoupling;
 
             source.Start();
+            decouplingClock.ProcessFrame();
             Assert.That(decouplingClock.IsRunning, Is.True);
 
             var secondSource = new TestClock();
 
             decouplingClock.ChangeSource(secondSource);
+            decouplingClock.ProcessFrame();
             Assert.That(decouplingClock.IsRunning, Is.False);
 
             decouplingClock.ChangeSource(source);
+            decouplingClock.ProcessFrame();
             Assert.That(decouplingClock.IsRunning, Is.True);
         }
 
@@ -169,11 +174,13 @@ public void TestSourceStoppedWhileNotDecoupling()
         {
             decouplingClock.AllowDecoupling = false;
             decouplingClock.Start();
+            decouplingClock.ProcessFrame();
 
             Assert.That(source.IsRunning, Is.True);
             Assert.That(decouplingClock.IsRunning, Is.True);
 
             source.Stop();
+            decouplingClock.ProcessFrame();
 
             Assert.That(source.IsRunning, Is.False);
             Assert.That(decouplingClock.IsRunning, Is.False);
@@ -210,6 +217,7 @@ public void TestSourceStoppedWhileDecoupling()
         {
             decouplingClock.AllowDecoupling = true;
             decouplingClock.Start();
+            decouplingClock.ProcessFrame();
 
             Assert.That(source.IsRunning, Is.True);
             Assert.That(decouplingClock.IsRunning, Is.True);
diff --git a/osu.Framework/Timing/DecouplingFramedClock.cs b/osu.Framework/Timing/DecouplingFramedClock.cs
index 898866642..5fc61a415 100644
--- a/osu.Framework/Timing/DecouplingFramedClock.cs
+++ b/osu.Framework/Timing/DecouplingFramedClock.cs
@@ -34,23 +34,7 @@ public sealed class DecouplingFramedClock : ISourceChangeableClock, IAdjustableC
         /// </remarks>
         public bool AllowDecoupling { get; set; } = true;
 
-        public bool IsRunning
-        {
-            get
-            {
-                // Always use the source clock's running state if it's running.
-                if (Source.IsRunning)
-                    return isRunning = true;
-
-                // If we're allowed to be decoupled, maintain our internal running state.
-                if (AllowDecoupling)
-                    return isRunning;
-
-                // Otherwise we're definitely not running.
-                Debug.Assert(!Source.IsRunning);
-                return isRunning = false;
-            }
-        }
+        public bool IsRunning { get; private set; }
 
         public double CurrentTime { get; private set; }
 
@@ -107,6 +91,7 @@ public void ProcessFrame()
                 if (Source.IsRunning)
                 {
                     currentTime = Source.CurrentTime;
+                    isRunning = true;
                     return;
                 }
 
@@ -114,6 +99,7 @@ public void ProcessFrame()
                 if (!AllowDecoupling)
                 {
                     currentTime = Source.CurrentTime;
+                    isRunning = false;
                     return;
                 }
 
@@ -141,6 +127,7 @@ public void ProcessFrame()
             }
             finally
             {
+                IsRunning = isRunning;
                 lastReferenceTime = realtimeReferenceClock.CurrentTime;
                 CurrentTime = currentTime;
                 ElapsedFrameTime = CurrentTime - lastTime;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this is sort of a continuation of my previous point about "implicit clock processing". Although I will say that it feels a lot more dicey to require ProcessFrame() calls for running state to propagate from the source clock to the decoupled, which as far as I can tell the diff above is trying to do.

That said, unless it incurs damage to any other pending adjustments game-side, I'd probably agree with applying the diff above.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can say that tests do pass osu! side with this change.

Copy link
Contributor

@smoogipoo smoogipoo Sep 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conceptually I'm fine with that change/the ones committed since this comment, and we can take if it ever goes wrong as a future learning lesson.

The other suggestion I had is that I only observed Seek() being affected by this, so simply changing its conditional to reference the property would also resolve it.

smoogipoo
smoogipoo previously approved these changes Sep 26, 2023
@smoogipoo smoogipoo requested a review from bdach September 26, 2023 02:47
@peppy
Copy link
Member Author

peppy commented Sep 26, 2023

I've pushed a small fix for an intermittent failure I got after several hundred runs.

@bdach
Copy link
Collaborator

bdach commented Sep 26, 2023

Given that we have:

  • TestForwardPlaybackOverZeroBoundary
  • TestForwardPlaybackOverLengthBoundary
  • TestBackwardPlaybackOverZeroBoundary

I wanted to also write TestBackwardPlaybackOverLengthBoundary. Came up with something like this:

diff --git a/osu.Framework.Tests/Clocks/DecouplingFramedClockTest.cs b/osu.Framework.Tests/Clocks/DecouplingFramedClockTest.cs
index 5eb211377..b9079d3a7 100644
--- a/osu.Framework.Tests/Clocks/DecouplingFramedClockTest.cs
+++ b/osu.Framework.Tests/Clocks/DecouplingFramedClockTest.cs
@@ -445,6 +445,53 @@ public void TestForwardPlaybackOverLengthBoundary()
             Assert.That(source.IsRunning, Is.False);
         }
 
+        [Test]
+        public void TestBackwardPlaybackOverLengthBoundary()
+        {
+            source = new TestStopwatchClockWithRangeLimit
+            {
+                MaxTime = 10000
+            };
+
+            decouplingClock.ChangeSource(source);
+            decouplingClock.AllowDecoupling = true;
+
+            decouplingClock.Seek(10200);
+            decouplingClock.Rate = -1;
+            decouplingClock.Start();
+
+            decouplingClock.ProcessFrame();
+
+            double time = decouplingClock.CurrentTime;
+
+            while (decouplingClock.CurrentTime > 10000)
+            {
+                Assert.That(source.IsRunning, Is.False);
+                Assert.That(source.CurrentTime, Is.EqualTo(10000));
+                Assert.That(decouplingClock.CurrentTime, Is.LessThanOrEqualTo(time));
+                time = decouplingClock.CurrentTime;
+
+                decouplingClock.ProcessFrame();
+            }
+
+            while (!source.IsRunning)
+                decouplingClock.ProcessFrame();
+
+            Assert.That(source.IsRunning, Is.True);
+            Assert.That(decouplingClock.CurrentTime, Is.LessThan(9900));
+
+            while (decouplingClock.CurrentTime > 9800)
+            {
+                Assert.That(decouplingClock.IsRunning, Is.True);
+                Assert.That(decouplingClock.CurrentTime, Is.LessThanOrEqualTo(time));
+                time = decouplingClock.CurrentTime;
+
+                decouplingClock.ProcessFrame();
+            }
+
+            Assert.That(source.IsRunning, Is.True);
+        }
+
         #endregion
 
         private class TestClockWithRange : TestClock

but it fails (more precisely, gets stuck at the while loop). Moreover, if I attempt to test manually via the other test scene:

diff --git a/osu.Framework.Tests/Visual/Clocks/TestSceneClocks.cs b/osu.Framework.Tests/Visual/Clocks/TestSceneClocks.cs
index 32735e014..02460a8d8 100644
--- a/osu.Framework.Tests/Visual/Clocks/TestSceneClocks.cs
+++ b/osu.Framework.Tests/Visual/Clocks/TestSceneClocks.cs
@@ -1,6 +1,7 @@
 // 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 System.Linq;
 using NUnit.Framework;
 using osu.Framework.Testing;
 using osu.Framework.Tests.Clocks;
@@ -21,7 +22,7 @@ public void TestInterpolationWithLaggyClock()
         [Test]
         public void TestDecouplingWithRangeLimited()
         {
-            AddStep("add non-negative stopwatch", () => AddClock(new TestStopwatchClockWithRangeLimit()));
+            AddStep("add non-negative stopwatch", () => AddClock(new TestStopwatchClockWithRangeLimit { MaxTime = 15000 }));
             AddStep("add decoupling", () => AddClock(new DecouplingFramedClock { AllowDecoupling = true }));
 
             AddStep("seek decoupling to -10000", () =>
@@ -35,6 +36,17 @@ public void TestDecouplingWithRangeLimited()
                 foreach (var c in this.ChildrenOfType<VisualClock>())
                     (c.TrackingClock as DecouplingFramedClock)?.Seek(10000);
             });
+
+            AddStep("seek decoupling to 20000", () =>
+            {
+                foreach (var c in this.ChildrenOfType<VisualClock>())
+                    (c.TrackingClock as DecouplingFramedClock)?.Seek(20000);
+            });
+
+            AddToggleStep("toggle direction", b =>
+            {
+                (this.ChildrenOfType<VisualClock>().Last().TrackingClock as DecouplingFramedClock)!.Rate = b ? -1 : 1;
+            });
         }
     }
 }

it also appears that the source clock is not restarted when decoupled clock crosses the length boundary backwards.

Is my testing wrong, or is this a real issue?

@peppy
Copy link
Member Author

peppy commented Sep 26, 2023

Your test is fine I think. There's no logic to support this case because we don't really use it. Moreso, we don't know where the "length" boundary is.

I touch on this in the OP, suggesting we may want to expose the "valid range" of a clock and use that in decoupling.

But without this change, I'd propose documenting this as a caveat and only adding support if we ever require it.

@bdach
Copy link
Collaborator

bdach commented Sep 26, 2023

You could theoretically use the last source time as a stopgap:

diff --git a/osu.Framework/Timing/DecouplingFramedClock.cs b/osu.Framework/Timing/DecouplingFramedClock.cs
index 0d179459d..cdffdb132 100644
--- a/osu.Framework/Timing/DecouplingFramedClock.cs
+++ b/osu.Framework/Timing/DecouplingFramedClock.cs
@@ -116,7 +116,7 @@ public void ProcessFrame()
                 currentTime += elapsedReferenceTime;
 
                 // When crossing the zero time boundary, we should start and use the source clock.
-                if (lastTime < 0 && currentTime >= 0)
+                if ((lastTime < 0 && currentTime >= 0) || (lastTime > Source.CurrentTime && currentTime <= Source.CurrentTime))
                 {
                     adjustableSourceClock.Seek(currentTime);
                     adjustableSourceClock.Start();

but you're right that the correctness of this value is anything but guaranteed. Probably fine to document and move on - shall I grab that so that we can move forward?

@peppy
Copy link
Member Author

peppy commented Sep 26, 2023

You raise a very good point about using the source time. I'd be okay with considering that if it works. Since you've already got the tests and proposal handy, want to give that a try first?

@bdach
Copy link
Collaborator

bdach commented Sep 26, 2023

Yeah sure, I'll run that past both test suites and we'll see how it goes.

@bdach
Copy link
Collaborator

bdach commented Sep 26, 2023

Some failures, unfortunately:

after-mania

The audio track test fails without my change, but the rest is firmly the fault of it. I'll have a look at both, but if I can't get my change to work in like 30 minutes, I'll bail.

bdach added a commit to bdach/osu that referenced this pull request Sep 26, 2023
Came up as a failure when locally running tests for
ppy/osu-framework#6001 - but the test is actually a previously-known
flaky that I couldn't reproduce the failure of until the aforementioned
PR.

This appears to be a simple race; the test scene queries the track
length from update thread, but the length is actually set on the audio
thread. So it's not unreasonable that given unlucky timing, the length
will not be set by `TrackBass` before it is queried.

To fix, switch assert to until step. I'm generally not really willing
to give this more time of day until this change is proven insufficient.
@bdach
Copy link
Collaborator

bdach commented Sep 26, 2023

For the unrelated failure, see PR link above. For the related failures, yeah, my patch breaks things quite badly.

While it is true that lastTime > Source.CurrentTime && currentTime <= Source.CurrentTime looks like the correct condition to apply for the edge case, it is actually trivially true when rewinding. So it breaks rewinding past the left-side boundary, among others.

I'll leave a comment and let this be.

@bdach bdach enabled auto-merge September 26, 2023 15:27
@peppy peppy disabled auto-merge September 26, 2023 15:51
@peppy peppy merged commit ff38299 into ppy:master Sep 26, 2023
@peppy peppy deleted the decoupled-un-fuck branch September 27, 2023 03:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
No open projects
Development

Successfully merging this pull request may close these issues.

3 participants