2024-07-05 14:32:58 +02:00
|
|
|
using Nerfed.Runtime.Audio;
|
|
|
|
using Nerfed.Runtime.Graphics;
|
|
|
|
using SDL2;
|
2024-07-12 23:10:44 +02:00
|
|
|
using System.Diagnostics;
|
2024-07-05 14:32:58 +02:00
|
|
|
|
|
|
|
namespace Nerfed.Runtime;
|
|
|
|
|
|
|
|
public static class Engine
|
|
|
|
{
|
2024-07-12 23:10:44 +02:00
|
|
|
public static event Action OnInitialize;
|
|
|
|
public static event Action OnUpdate;
|
|
|
|
public static event Action OnRender;
|
|
|
|
public static event Action OnQuit;
|
|
|
|
|
2024-07-05 14:32:58 +02:00
|
|
|
public static TimeSpan MaxDeltaTime { get; set; } = TimeSpan.FromMilliseconds(100);
|
|
|
|
public static bool VSync { get; set; }
|
|
|
|
|
|
|
|
public static GraphicsDevice GraphicsDevice { get; private set; }
|
|
|
|
public static AudioDevice AudioDevice { get; private set; }
|
|
|
|
public static Window MainWindow { get; private set; }
|
|
|
|
public static TimeSpan Timestep { get; private set; }
|
|
|
|
|
|
|
|
private static bool quit;
|
|
|
|
private static Stopwatch gameTimer;
|
|
|
|
private static long previousTicks = 0;
|
|
|
|
private static TimeSpan accumulatedUpdateTime = TimeSpan.Zero;
|
|
|
|
|
|
|
|
private static TimeSpan accumulatedDrawTime = TimeSpan.Zero;
|
|
|
|
|
|
|
|
// must be a power of 2 so we can do a bitmask optimization when checking worst case
|
|
|
|
private const int previousSleepTimeCount = 128;
|
|
|
|
private const int sleepTimeMask = previousSleepTimeCount - 1;
|
|
|
|
private static readonly TimeSpan[] previousSleepTimes = new TimeSpan[previousSleepTimeCount];
|
|
|
|
private static int sleepTimeIndex;
|
|
|
|
private static TimeSpan worstCaseSleepPrecision = TimeSpan.FromMilliseconds(1);
|
|
|
|
private static bool framerateCapped;
|
|
|
|
private static TimeSpan framerateCapTimeSpan = TimeSpan.Zero;
|
|
|
|
|
|
|
|
//TODO: These are temp
|
|
|
|
private const int MaxFps = 300;
|
|
|
|
private const int TargetTimestep = 60;
|
|
|
|
private const int WindowWidth = 1280;
|
|
|
|
private const int WindowHeight = 720;
|
|
|
|
private const string WindowTitle = "Nerfed";
|
|
|
|
//..
|
|
|
|
|
2024-07-11 23:46:32 +02:00
|
|
|
public static void Run(string[] args)
|
2024-07-05 14:32:58 +02:00
|
|
|
{
|
|
|
|
Timestep = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / TargetTimestep);
|
|
|
|
gameTimer = Stopwatch.StartNew();
|
|
|
|
SetFrameLimiter(new FrameLimiterSettings(FrameLimiterMode.Capped, MaxFps));
|
|
|
|
|
|
|
|
for (int i = 0; i < previousSleepTimes.Length; i += 1)
|
|
|
|
{
|
|
|
|
previousSleepTimes[i] = TimeSpan.FromMilliseconds(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_TIMER | SDL.SDL_INIT_GAMECONTROLLER) < 0)
|
|
|
|
{
|
|
|
|
throw new Exception("Failed to init SDL");
|
|
|
|
}
|
2024-07-06 23:33:04 +02:00
|
|
|
|
2024-07-05 14:32:58 +02:00
|
|
|
GraphicsDevice = new GraphicsDevice(BackendFlags.All);
|
2024-07-13 13:45:12 +02:00
|
|
|
GraphicsDevice.LoadDefaultPipelines();
|
2024-07-05 14:32:58 +02:00
|
|
|
|
|
|
|
MainWindow = new Window(GraphicsDevice, new WindowCreateInfo(WindowTitle, WindowWidth, WindowHeight, ScreenMode.Windowed));
|
|
|
|
if (!GraphicsDevice.ClaimWindow(MainWindow, SwapchainComposition.SDR, VSync ? PresentMode.VSync : PresentMode.Mailbox))
|
|
|
|
{
|
|
|
|
throw new Exception("Failed to claim window");
|
|
|
|
}
|
|
|
|
|
|
|
|
AudioDevice = new AudioDevice();
|
|
|
|
|
2024-07-12 23:10:44 +02:00
|
|
|
OnInitialize?.Invoke();
|
|
|
|
|
2024-07-05 14:32:58 +02:00
|
|
|
while (!quit)
|
|
|
|
{
|
|
|
|
Tick();
|
|
|
|
}
|
|
|
|
|
2024-07-12 23:10:44 +02:00
|
|
|
OnQuit?.Invoke();
|
|
|
|
|
2024-07-05 14:32:58 +02:00
|
|
|
GraphicsDevice.UnclaimWindow(MainWindow);
|
|
|
|
MainWindow.Dispose();
|
|
|
|
GraphicsDevice.Dispose();
|
|
|
|
AudioDevice.Dispose();
|
|
|
|
SDL.SDL_Quit();
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Updates the frame limiter settings.
|
|
|
|
/// </summary>
|
|
|
|
public static void SetFrameLimiter(FrameLimiterSettings settings)
|
|
|
|
{
|
|
|
|
framerateCapped = settings.Mode == FrameLimiterMode.Capped;
|
|
|
|
|
|
|
|
if (framerateCapped)
|
|
|
|
{
|
|
|
|
framerateCapTimeSpan = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / settings.Cap);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
framerateCapTimeSpan = TimeSpan.Zero;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void Quit()
|
|
|
|
{
|
|
|
|
quit = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static void Tick()
|
|
|
|
{
|
2024-10-19 23:41:05 +02:00
|
|
|
Profiler.BeginFrame();
|
|
|
|
|
2024-07-05 14:32:58 +02:00
|
|
|
AdvanceElapsedTime();
|
|
|
|
|
|
|
|
if (framerateCapped)
|
|
|
|
{
|
2024-10-19 23:41:05 +02:00
|
|
|
Profiler.BeginSample("framerateCapped");
|
|
|
|
|
2024-07-05 14:32:58 +02:00
|
|
|
/* We want to wait until the framerate cap,
|
|
|
|
* 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 (accumulatedDrawTime + worstCaseSleepPrecision < framerateCapTimeSpan)
|
|
|
|
{
|
|
|
|
Thread.Sleep(1);
|
|
|
|
TimeSpan timeAdvancedSinceSleeping = AdvanceElapsedTime();
|
|
|
|
UpdateEstimatedSleepPrecision(timeAdvancedSinceSleeping);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* 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.
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
while (accumulatedDrawTime < framerateCapTimeSpan)
|
|
|
|
{
|
|
|
|
Thread.SpinWait(1);
|
|
|
|
AdvanceElapsedTime();
|
|
|
|
}
|
2024-10-19 23:41:05 +02:00
|
|
|
|
|
|
|
Profiler.EndSample();
|
2024-07-05 14:32:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Do not let any step take longer than our maximum.
|
|
|
|
if (accumulatedUpdateTime > MaxDeltaTime)
|
|
|
|
{
|
|
|
|
accumulatedUpdateTime = MaxDeltaTime;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!quit)
|
|
|
|
{
|
|
|
|
while (accumulatedUpdateTime >= Timestep)
|
|
|
|
{
|
2024-10-19 23:41:05 +02:00
|
|
|
Profiler.BeginSample("Update");
|
2024-07-05 14:32:58 +02:00
|
|
|
Keyboard.Update();
|
|
|
|
Mouse.Update();
|
|
|
|
GamePad.Update();
|
|
|
|
|
|
|
|
ProcessSDLEvents();
|
|
|
|
|
|
|
|
// Tick game here...
|
2024-10-19 23:41:05 +02:00
|
|
|
Profiler.BeginSample("OnUpdate");
|
2024-07-12 23:10:44 +02:00
|
|
|
OnUpdate?.Invoke();
|
2024-10-19 23:41:05 +02:00
|
|
|
Profiler.EndSample();
|
2024-07-05 14:32:58 +02:00
|
|
|
|
|
|
|
AudioDevice.WakeThread();
|
|
|
|
accumulatedUpdateTime -= Timestep;
|
2024-10-19 23:41:05 +02:00
|
|
|
Profiler.EndSample();
|
2024-07-05 14:32:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
double alpha = accumulatedUpdateTime / Timestep;
|
|
|
|
|
|
|
|
// Render here..
|
2024-10-19 23:41:05 +02:00
|
|
|
Profiler.BeginSample("OnRender");
|
2024-07-12 23:10:44 +02:00
|
|
|
OnRender?.Invoke();
|
2024-10-19 23:41:05 +02:00
|
|
|
Profiler.EndSample();
|
2024-07-05 14:32:58 +02:00
|
|
|
|
|
|
|
accumulatedDrawTime -= framerateCapTimeSpan;
|
|
|
|
}
|
2024-10-19 23:41:05 +02:00
|
|
|
|
|
|
|
Profiler.EndFrame();
|
2024-07-05 14:32:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private static TimeSpan AdvanceElapsedTime()
|
|
|
|
{
|
|
|
|
long currentTicks = gameTimer.Elapsed.Ticks;
|
|
|
|
TimeSpan timeAdvanced = TimeSpan.FromTicks(currentTicks - previousTicks);
|
|
|
|
accumulatedUpdateTime += timeAdvanced;
|
|
|
|
accumulatedDrawTime += timeAdvanced;
|
|
|
|
previousTicks = currentTicks;
|
|
|
|
return timeAdvanced;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static void ProcessSDLEvents()
|
|
|
|
{
|
|
|
|
while (SDL.SDL_PollEvent(out SDL.SDL_Event ev) == 1)
|
|
|
|
{
|
|
|
|
switch (ev.type)
|
|
|
|
{
|
|
|
|
case SDL.SDL_EventType.SDL_QUIT:
|
|
|
|
Quit();
|
|
|
|
break;
|
|
|
|
case SDL.SDL_EventType.SDL_TEXTINPUT:
|
|
|
|
case SDL.SDL_EventType.SDL_KEYDOWN:
|
|
|
|
case SDL.SDL_EventType.SDL_KEYUP:
|
|
|
|
Keyboard.ProcessEvent(ref ev);
|
|
|
|
break;
|
|
|
|
case SDL.SDL_EventType.SDL_MOUSEBUTTONDOWN:
|
|
|
|
case SDL.SDL_EventType.SDL_MOUSEBUTTONUP:
|
|
|
|
case SDL.SDL_EventType.SDL_MOUSEWHEEL:
|
|
|
|
case SDL.SDL_EventType.SDL_MOUSEMOTION:
|
|
|
|
Mouse.ProcessEvent(ref ev);
|
|
|
|
break;
|
|
|
|
case SDL.SDL_EventType.SDL_CONTROLLERDEVICEADDED:
|
|
|
|
case SDL.SDL_EventType.SDL_CONTROLLERDEVICEREMOVED:
|
|
|
|
case SDL.SDL_EventType.SDL_CONTROLLERBUTTONDOWN:
|
|
|
|
case SDL.SDL_EventType.SDL_CONTROLLERBUTTONUP:
|
|
|
|
case SDL.SDL_EventType.SDL_CONTROLLERAXISMOTION:
|
|
|
|
case SDL.SDL_EventType.SDL_CONTROLLERTOUCHPADDOWN:
|
|
|
|
case SDL.SDL_EventType.SDL_CONTROLLERTOUCHPADUP:
|
|
|
|
case SDL.SDL_EventType.SDL_CONTROLLERTOUCHPADMOTION:
|
|
|
|
case SDL.SDL_EventType.SDL_CONTROLLERSENSORUPDATE:
|
|
|
|
GamePad.ProcessEvent(ref ev);
|
|
|
|
break;
|
|
|
|
case SDL.SDL_EventType.SDL_WINDOWEVENT:
|
|
|
|
Window.ProcessEvent(ref ev);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* To calculate the sleep precision of the OS, we take the worst case
|
|
|
|
* time spent sleeping over the results of previous requests to sleep 1ms.
|
|
|
|
*/
|
|
|
|
private static void UpdateEstimatedSleepPrecision(TimeSpan timeSpentSleeping)
|
|
|
|
{
|
|
|
|
/* It is unlikely that the scheduler will actually be more imprecise than
|
|
|
|
* 4ms and we don't want to get wrecked by a single long sleep so we cap this
|
|
|
|
* value at 4ms for sanity.
|
|
|
|
*/
|
|
|
|
TimeSpan upperTimeBound = TimeSpan.FromMilliseconds(4);
|
|
|
|
|
|
|
|
if (timeSpentSleeping > upperTimeBound)
|
|
|
|
{
|
|
|
|
timeSpentSleeping = upperTimeBound;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* We know the previous worst case - it's saved in worstCaseSleepPrecision.
|
|
|
|
* We also know the current index. So the only way the worst case changes
|
|
|
|
* is if we either 1) just got a new worst case, or 2) the worst case was
|
|
|
|
* the oldest entry on the list.
|
|
|
|
*/
|
|
|
|
if (timeSpentSleeping >= worstCaseSleepPrecision)
|
|
|
|
{
|
|
|
|
worstCaseSleepPrecision = timeSpentSleeping;
|
|
|
|
}
|
|
|
|
else if (previousSleepTimes[sleepTimeIndex] == worstCaseSleepPrecision)
|
|
|
|
{
|
|
|
|
TimeSpan maxSleepTime = TimeSpan.MinValue;
|
|
|
|
for (int i = 0; i < previousSleepTimes.Length; i++)
|
|
|
|
{
|
|
|
|
if (previousSleepTimes[i] > maxSleepTime)
|
|
|
|
{
|
|
|
|
maxSleepTime = previousSleepTimes[i];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
worstCaseSleepPrecision = maxSleepTime;
|
|
|
|
}
|
|
|
|
|
|
|
|
previousSleepTimes[sleepTimeIndex] = timeSpentSleeping;
|
|
|
|
sleepTimeIndex = (sleepTimeIndex + 1) & sleepTimeMask;
|
|
|
|
}
|
|
|
|
}
|