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;
7using System.Diagnostics;
8using System.Threading;
9
10namespace osu.Framework.Timing
11{
12 /// <summary>
13 /// A FrameClock which will limit the number of frames processed by adding Thread.Sleep calls on each ProcessFrame.
14 /// </summary>
15 public class ThrottledFrameClock : FramedClock
16 {
17 /// <summary>
18 /// The target number of updates per second. Only used when <see cref="Throttling"/> is <c>true</c>.
19 /// </summary>
20 /// <remarks>
21 /// A value of 0 is treated the same as "unlimited" or <see cref="double.MaxValue"/>.
22 /// </remarks>
23 public double MaximumUpdateHz = 1000.0;
24
25 /// <summary>
26 /// Whether throttling should be enabled. Defaults to <c>true</c>.
27 /// </summary>
28 public bool Throttling = true;
29
30 /// <summary>
31 /// The time spent in a Thread.Sleep state during the last frame.
32 /// </summary>
33 public double TimeSlept { get; private set; }
34
35 public override void ProcessFrame()
36 {
37 Debug.Assert(MaximumUpdateHz >= 0);
38
39 base.ProcessFrame();
40
41 if (Throttling)
42 {
43 if (MaximumUpdateHz > 0 && MaximumUpdateHz < double.MaxValue)
44 {
45 throttle();
46 }
47 else
48 {
49 // Even when running at unlimited frame-rate, we should call the scheduler
50 // to give lower-priority background processes a chance to do work.
51 TimeSlept = sleepAndUpdateCurrent(0);
52 }
53 }
54 else
55 {
56 TimeSlept = 0;
57 }
58
59 Debug.Assert(TimeSlept <= ElapsedFrameTime);
60 }
61
62 private double accumulatedSleepError;
63
64 private void throttle()
65 {
66 double excessFrameTime = 1000d / MaximumUpdateHz - ElapsedFrameTime;
67
68 TimeSlept = sleepAndUpdateCurrent((int)Math.Max(0, excessFrameTime + accumulatedSleepError));
69
70 accumulatedSleepError += excessFrameTime - TimeSlept;
71
72 // Never allow the sleep error to become too negative and induce too many catch-up frames
73 accumulatedSleepError = Math.Max(-1000 / 30.0, accumulatedSleepError);
74 }
75
76 private double sleepAndUpdateCurrent(int milliseconds)
77 {
78 double before = CurrentTime;
79
80 Thread.Sleep(milliseconds);
81
82 return (CurrentTime = SourceTime) - before;
83 }
84 }
85}