using Nerfed.Runtime.Audio; using Nerfed.Runtime.Graphics; using SDL2; using System.Diagnostics; namespace Nerfed.Runtime; public static class Engine { 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"; //.. private static Gui.GuiController Controller; internal static void Run(string[] args) { 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"); } GraphicsDevice = new GraphicsDevice(BackendFlags.All); 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(); Controller = new Gui.GuiController(GraphicsDevice, MainWindow, Color.DarkOliveGreen); Controller.OnGui += () => { ImGuiNET.ImGui.ShowDemoWindow(); }; while (!quit) { Tick(); } Controller.Dispose(); GraphicsDevice.UnclaimWindow(MainWindow); MainWindow.Dispose(); GraphicsDevice.Dispose(); AudioDevice.Dispose(); SDL.SDL_Quit(); } /// /// Updates the frame limiter settings. /// 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() { AdvanceElapsedTime(); if (framerateCapped) { /* 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(); } } // Do not let any step take longer than our maximum. if (accumulatedUpdateTime > MaxDeltaTime) { accumulatedUpdateTime = MaxDeltaTime; } if (!quit) { while (accumulatedUpdateTime >= Timestep) { Keyboard.Update(); Mouse.Update(); GamePad.Update(); ProcessSDLEvents(); // Tick game here... Controller.Update((float)Timestep.TotalSeconds); AudioDevice.WakeThread(); accumulatedUpdateTime -= Timestep; } double alpha = accumulatedUpdateTime / Timestep; // Render here.. Controller.Render(); accumulatedDrawTime -= framerateCapTimeSpan; } } 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; } }