decouple fixed timestep from draw, add frame cap

main
cosmonaut 2022-06-04 15:48:55 -07:00
parent 5a9709c843
commit d2a51ce524
2 changed files with 59 additions and 30 deletions

14
src/FramerateSettings.cs Normal file
View File

@ -0,0 +1,14 @@
namespace MoonWorks
{
public enum FramerateMode
{
Uncapped,
Capped
}
public struct FramerateSettings
{
public FramerateMode Mode;
public int Cap;
}
}

View File

@ -18,7 +18,8 @@ namespace MoonWorks
private Stopwatch gameTimer; private Stopwatch gameTimer;
private TimeSpan timestep; private TimeSpan timestep;
private long previousTicks = 0; private long previousTicks = 0;
TimeSpan accumulatedElapsedTime = TimeSpan.Zero; TimeSpan accumulatedUpdateTime = TimeSpan.Zero;
TimeSpan accumulatedDrawTime = TimeSpan.Zero;
// must be a power of 2 so we can do a bitmask optimization when checking worst case // must be a power of 2 so we can do a bitmask optimization when checking worst case
private const int PREVIOUS_SLEEP_TIME_COUNT = 128; private const int PREVIOUS_SLEEP_TIME_COUNT = 128;
private const int SLEEP_TIME_MASK = PREVIOUS_SLEEP_TIME_COUNT - 1; private const int SLEEP_TIME_MASK = PREVIOUS_SLEEP_TIME_COUNT - 1;
@ -26,6 +27,9 @@ namespace MoonWorks
private int sleepTimeIndex = 0; private int sleepTimeIndex = 0;
private TimeSpan worstCaseSleepPrecision = TimeSpan.FromMilliseconds(1); private TimeSpan worstCaseSleepPrecision = TimeSpan.FromMilliseconds(1);
private bool FramerateCapped = false;
private TimeSpan FramerateCapTimeSpan = TimeSpan.Zero;
public Window Window { get; } public Window Window { get; }
public GraphicsDevice GraphicsDevice { get; } public GraphicsDevice GraphicsDevice { get; }
public AudioDevice AudioDevice { get; } public AudioDevice AudioDevice { get; }
@ -42,6 +46,7 @@ namespace MoonWorks
public Game( public Game(
WindowCreateInfo windowCreateInfo, WindowCreateInfo windowCreateInfo,
PresentMode presentMode, PresentMode presentMode,
FramerateSettings framerateSettings,
int targetTimestep = 60, int targetTimestep = 60,
bool debugMode = false bool debugMode = false
) )
@ -49,6 +54,13 @@ namespace MoonWorks
timestep = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / targetTimestep); timestep = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / targetTimestep);
gameTimer = Stopwatch.StartNew(); gameTimer = Stopwatch.StartNew();
FramerateCapped = framerateSettings.Mode == FramerateMode.Capped;
if (FramerateCapped)
{
FramerateCapTimeSpan = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / framerateSettings.Cap);
}
for (int i = 0; i < previousSleepTimes.Length; i += 1) for (int i = 0; i < previousSleepTimes.Length; i += 1)
{ {
previousSleepTimes[i] = TimeSpan.FromMilliseconds(1); previousSleepTimes[i] = TimeSpan.FromMilliseconds(1);
@ -90,61 +102,63 @@ namespace MoonWorks
} }
protected abstract void Update(TimeSpan delta); protected abstract void Update(TimeSpan delta);
protected abstract void Draw(TimeSpan delta, double alpha); protected abstract void Draw(double alpha);
private void Tick() private void Tick()
{ {
AdvanceElapsedTime(); AdvanceElapsedTime();
/* We want to wait until the next frame, if (FramerateCapped)
* but we don't want to oversleep. Requesting repeated 1ms sleeps and
* seeing how long we actually slept for lets us estimate the worst case
* sleep precision so we don't oversleep the next frame.
*/
while (accumulatedElapsedTime + worstCaseSleepPrecision < timestep)
{ {
System.Threading.Thread.Sleep(1); /* We want to wait until the framerate cap,
TimeSpan timeAdvancedSinceSleeping = AdvanceElapsedTime(); * but we don't want to oversleep. Requesting repeated 1ms sleeps and
UpdateEstimatedSleepPrecision(timeAdvancedSinceSleeping); * seeing how long we actually slept for lets us estimate the worst case
} * sleep precision so we don't oversleep the next frame.
*/
while (accumulatedDrawTime + worstCaseSleepPrecision < FramerateCapTimeSpan)
{
System.Threading.Thread.Sleep(1);
TimeSpan timeAdvancedSinceSleeping = AdvanceElapsedTime();
UpdateEstimatedSleepPrecision(timeAdvancedSinceSleeping);
}
/* Now that we have slept into the sleep precision threshold, we need to wait /* Now that we have slept into the sleep precision threshold, we need to wait
* for just a little bit longer until the target elapsed time has been reached. * for just a little bit longer until the target elapsed time has been reached.
* SpinWait(1) works by pausing the thread for very short intervals, so it is * SpinWait(1) works by pausing the thread for very short intervals, so it is
* an efficient and time-accurate way to wait out the rest of the time. * an efficient and time-accurate way to wait out the rest of the time.
*/ */
while (accumulatedElapsedTime < timestep) while (accumulatedDrawTime < FramerateCapTimeSpan)
{ {
System.Threading.Thread.SpinWait(1); System.Threading.Thread.SpinWait(1);
AdvanceElapsedTime(); AdvanceElapsedTime();
}
} }
// Now that we are going to perform an update, let's handle SDL events. // Now that we are going to perform an update, let's handle SDL events.
HandleSDLEvents(); HandleSDLEvents();
// Do not let any step take longer than our maximum. // Do not let any step take longer than our maximum.
if (accumulatedElapsedTime > MAX_DELTA_TIME) if (accumulatedUpdateTime > MAX_DELTA_TIME)
{ {
accumulatedElapsedTime = MAX_DELTA_TIME; accumulatedUpdateTime = MAX_DELTA_TIME;
} }
if (!quit) if (!quit)
{ {
while (accumulatedElapsedTime >= timestep) while (accumulatedUpdateTime >= timestep)
{ {
Inputs.Mouse.Wheel = 0;
Inputs.Update(); Inputs.Update();
AudioDevice.Update(); AudioDevice.Update();
Update(timestep); Update(timestep);
accumulatedElapsedTime -= timestep; accumulatedUpdateTime -= timestep;
} }
var alpha = accumulatedElapsedTime / timestep; var alpha = accumulatedUpdateTime / timestep;
Draw(timestep, alpha); Draw(alpha);
accumulatedDrawTime -= FramerateCapTimeSpan;
} }
} }
@ -201,7 +215,8 @@ namespace MoonWorks
{ {
long currentTicks = gameTimer.Elapsed.Ticks; long currentTicks = gameTimer.Elapsed.Ticks;
TimeSpan timeAdvanced = TimeSpan.FromTicks(currentTicks - previousTicks); TimeSpan timeAdvanced = TimeSpan.FromTicks(currentTicks - previousTicks);
accumulatedElapsedTime += timeAdvanced; accumulatedUpdateTime += timeAdvanced;
accumulatedDrawTime += timeAdvanced;
previousTicks = currentTicks; previousTicks = currentTicks;
return timeAdvanced; return timeAdvanced;
} }