A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4#nullable enable
5
6using System;
7
8namespace osu.Framework.Timing
9{
10 /// <summary>
11 /// Adds the ability to keep the clock running even when the underlying source has stopped or cannot handle the current time range.
12 /// This is handled by performing seeks on the underlying source and checking whether they were successful or not.
13 /// On failure to seek, we take over with an internal clock until control can be returned to the actual source.
14 ///
15 /// This clock type removes the requirement of having a source set.
16 ///
17 /// If a <see cref="InterpolatingFramedClock.Source"/> is set, it is presumed that we have exclusive control over operations on it.
18 /// This is used to our advantage to allow correct <see cref="IsRunning"/> state tracking in the event of cross-thread communication delays (with an audio thread, for instance).
19 /// </summary>
20 public class DecoupleableInterpolatingFramedClock : InterpolatingFramedClock, IAdjustableClock
21 {
22 /// <summary>
23 /// Specify whether we are coupled 1:1 to SourceClock. If not, we can independently continue operation.
24 /// </summary>
25 public bool IsCoupled = true;
26
27 /// <summary>
28 /// In some cases we should always use the interpolated source.
29 /// </summary>
30 private bool useInterpolatedSourceTime => IsRunning && FramedSourceClock?.IsRunning == true;
31
32 private readonly FramedClock decoupledClock;
33 private readonly StopwatchClock decoupledStopwatch;
34
35 /// <summary>
36 /// We need to be able to pass on adjustments to the source if it supports them.
37 /// </summary>
38 private IAdjustableClock? adjustableSource => Source as IAdjustableClock;
39
40 public override double CurrentTime => currentTime;
41
42 private double currentTime;
43
44 public double ProposedCurrentTime => useInterpolatedSourceTime ? base.CurrentTime : decoupledClock.CurrentTime;
45
46 public double ProposedElapsedTime => useInterpolatedSourceTime ? base.ElapsedFrameTime : decoupledClock.ElapsedFrameTime;
47
48 public override bool IsRunning => decoupledClock.IsRunning; // we always want to use our local IsRunning state, as it is more correct.
49
50 private double elapsedFrameTime;
51
52 public override double ElapsedFrameTime => elapsedFrameTime;
53
54 public override double Rate
55 {
56 get => Source?.Rate ?? 1;
57 set
58 {
59 if (adjustableSource == null)
60 throw new NotSupportedException("Source is not adjustable.");
61
62 adjustableSource.Rate = value;
63 }
64 }
65
66 public void ResetSpeedAdjustments() => Rate = 1;
67
68 public DecoupleableInterpolatingFramedClock()
69 {
70 decoupledClock = new FramedClock(decoupledStopwatch = new StopwatchClock());
71 }
72
73 public override void ProcessFrame()
74 {
75 base.ProcessFrame();
76
77 bool sourceRunning = Source?.IsRunning ?? false;
78
79 decoupledStopwatch.Rate = adjustableSource?.Rate ?? 1;
80
81 // if interpolating based on the source, keep the decoupled clock in sync with the interpolated time.
82 if (IsCoupled && sourceRunning)
83 decoupledStopwatch.Seek(base.CurrentTime);
84
85 // process the decoupled clock to update the current proposed time.
86 decoupledClock.ProcessFrame();
87
88 // if the source clock is started as a result of becoming capable of handling the decoupled time, the proposed time may change to reflect the interpolated source time.
89 // however the interpolated source time that was calculated inside base.ProcessFrame() (above) did not consider the current (post-seek) time of the source.
90 // in all other cases the proposed time will match before and after clocks are started/stopped.
91 double proposedTime = ProposedCurrentTime;
92 double elapsedTime = ProposedElapsedTime;
93
94 if (IsRunning)
95 {
96 if (IsCoupled)
97 {
98 // when coupled, we want to stop when our source clock stops.
99 if (!sourceRunning)
100 Stop();
101 }
102 else
103 {
104 // when decoupled and running, we should try to start the source clock it if it's capable of handling the current time.
105 if (!sourceRunning)
106 Start();
107 }
108 }
109 else if (IsCoupled && sourceRunning)
110 {
111 // when coupled and not running, we want to start when the source clock starts.
112 Start();
113 }
114
115 elapsedFrameTime = elapsedTime;
116
117 // the source may be started during playback but remain behind the current time in the playback direction for a number of frames.
118 // in such cases, the current time should remain paused until the source time catches up.
119 currentTime = elapsedFrameTime < 0 ? Math.Min(currentTime, proposedTime) : Math.Max(currentTime, proposedTime);
120 }
121
122 public override void ChangeSource(IClock? source)
123 {
124 if (source == null) return;
125
126 // transfer our value to the source clock.
127 (source as IAdjustableClock)?.Seek(CurrentTime);
128
129 base.ChangeSource(source);
130 }
131
132 public void Reset()
133 {
134 IsCoupled = true;
135
136 adjustableSource?.Reset();
137 decoupledStopwatch.Reset();
138 }
139
140 public void Start()
141 {
142 if (adjustableSource?.IsRunning == false)
143 {
144 if (adjustableSource.Seek(ProposedCurrentTime))
145 //only start the source clock if our time values match.
146 //this handles the case where we seeked to an unsupported value and the source clock is out of sync.
147 adjustableSource.Start();
148 }
149
150 decoupledStopwatch.Start();
151 }
152
153 public void Stop()
154 {
155 decoupledStopwatch.Stop();
156 adjustableSource?.Stop();
157 }
158
159 public bool Seek(double position)
160 {
161 try
162 {
163 bool success = adjustableSource?.Seek(position) != false;
164
165 if (IsCoupled)
166 return success;
167
168 if (!success)
169 //if we failed to seek then stop the source and use decoupled mode.
170 adjustableSource?.Stop();
171
172 return decoupledStopwatch.Seek(position);
173 }
174 finally
175 {
176 ProcessFrame();
177 }
178 }
179 }
180}