#include #include #include #include #include #include #include #include #include #include #include "imgui.h" #include "glm/gtx/transform.hpp" #include "spdlog/spdlog.h" #include #include struct TracyFrameScope { ~TracyFrameScope() { FrameMark; } }; App::App() { } App::~App() { if (!cleanedUp) { try { cleanup(); } catch (...) { // Destructors must not throw. Initialization failures are already // reported by the caller, while cleanup is best effort here. } } } void App::init(const AppParams& params) { cleanedUp = false; customInitStarted = false; try { m_params = params; ZoneScopedN("App::init"); tracy::SetThreadName("Main Thread"); TracySetProgramName(params.appName.c_str()); AssetFS::GetInstance().Init(params.exeDir); window.Create({ .title = params.windowTitle, .size = params.windowSize, .mode = params.windowMode, .maximized = params.startMaximized, }); gfxDevice.init(window.GetHandle(), params.appName, false); imguiPass.init(window.GetHandle(), gfxDevice); InputManager::GetInstance().Init(); Time::GetInstance().Update(); customInitStarted = true; customInit(); cleanedUp = false; } catch (...) { try { cleanup(); } catch (...) { } throw; } } void App::run() { ZoneScopedN("App::run"); Time::GetInstance().Update(); const float fixedDt = static_cast(Time::GetInstance().FixedDeltaTime()); const int maxSteps = 5; float accumulator = 0.0f; isRunning = true; while (isRunning) { TracyFrameScope tracyFrame; ZoneScopedN("App::Frame"); Time::GetInstance().Update(); float dt = static_cast(Time::GetInstance().DeltaTime()); if (dt > 0.25f) dt = 0.25f; if (dt > 0.0f) { float newFPS = 1.0f / dt; avgFPS = std::lerp(avgFPS, newFPS, 0.1f); TracyPlot("FPS", avgFPS); TracyPlot("Delta Time ms", dt * 1000.0f); } accumulator += dt; { ZoneScopedN("Input BeginFrame"); InputManager::GetInstance().BeginFrame(); } { ZoneScopedN("SDL Events"); SDL_Event event; while (SDL_PollEvent(&event)) { ZoneScopedN("SDL Event"); imguiPass.handleEvent(event); const bool mouseEvent = event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP || event.type == SDL_MOUSEMOTION || event.type == SDL_MOUSEWHEEL; if (event.type == SDL_QUIT) { isRunning = false; break; } window.ProcessEvent(event); if (window.ConsumeResized()) { resizePending = true; lastResizeTime = std::chrono::steady_clock::now(); } if (window.ConsumeCloseRequested()) { isRunning = false; break; } const bool keyboardEvent = event.type == SDL_KEYDOWN || event.type == SDL_KEYUP || event.type == SDL_TEXTINPUT; const bool capturedByImgui = (mouseEvent && !InputManager::GetInstance().IsMouseCaptured() && imguiPass.wantsMouse()) || (keyboardEvent && imguiPass.wantsKeyboard()); if (!capturedByImgui) { ZoneScopedN("Input ProcessEvent"); if (InputManager::GetInstance().ProcessEvent(event)) { isRunning = false; } } } } if (!isRunning) break; if (window.IsMinimized()) { // The surface is 0x0 while minimized, there is nothing to render or present to accumulator = 0.0f; std::this_thread::sleep_for(std::chrono::milliseconds(16)); continue; } // Consume SDL events before updating the base camera so held and // newly pressed inputs are applied in the same frame. camera.Update(dt); { ZoneScopedN("ImGui BeginFrame"); imguiPass.beginFrame(); } { ZoneScopedN("customUpdate"); customUpdate(dt); } if (m_params.showDebugUi) { ZoneScopedN("Debug ImGui"); drawWindowsMenu(); if (m_DebugWindows.inspector) { SceneManager::GetInstance().RenderImgui(&m_DebugWindows.inspector); } if (m_DebugWindows.stats) { if (ImGui::Begin("Debug", &m_DebugWindows.stats)) { ImGui::Text("FPS: %.2f", avgFPS); } ImGui::End(); } if (m_DebugWindows.physics) { drawPhysicsPanel(dt, fixedDt, accumulator); } if (m_DebugWindows.imageCache) { m_ResourcesInspector.drawImageCacheWindow(&m_DebugWindows.imageCache, resources, imguiPass); } TracyPlot("FPS", avgFPS); TracyPlot("Frame Time ms", dt * 1000.0f); TracyPlot("Accumulator ms", accumulator * 1000.0f); } { ZoneScopedN("ImGui EndFrame"); imguiPass.endFrame(); } { ZoneScopedN("FixedUpdate"); int steps = 0; if (m_PhysicsPaused) { // Important: prevent physics from building up a huge backlog while paused. accumulator = 0.0f; if (m_PhysicsStepOnce) { ZoneScopedN("Single Physics Step"); customFixedUpdate(fixedDt * m_PhysicsTimeScale); steps = 1; m_PhysicsStepOnce = false; } } else { while (accumulator >= fixedDt && steps < maxSteps) { ZoneScopedN("Fixed Step"); customFixedUpdate(fixedDt * m_PhysicsTimeScale); accumulator -= fixedDt; steps++; } if (steps == maxSteps) { accumulator = 0.0f; } } m_PhysicsStepsLastFrame = steps; TracyPlot("Fixed Steps", static_cast(steps)); } Time::GetInstance().SetFixedAlpha(std::clamp(accumulator / fixedDt, 0.0f, 1.0f)); { ZoneScopedN("Swapchain Resize Check"); if (gfxDevice.needsSwapchainRecreate() || resizePending) { auto now = std::chrono::steady_clock::now(); if (resizePending && now - lastResizeTime < std::chrono::milliseconds(100)) { continue; } const glm::ivec2 drawableSize = window.GetDrawableSize(); const int w = drawableSize.x; const int h = drawableSize.y; if (w == 0 || h == 0) { continue; } { ZoneScopedN("Recreate Swapchain"); if (!gfxDevice.recreateSwapchain(w, h)) { continue; } spdlog::info("Recreated swapchain at {}x{}", w, h); imguiPass.onSwapchainRecreated(); onWindowResize(w, h); } resizePending = false; continue; } } { ZoneScopedN("customDraw"); customDraw(); } if (frameLimit) { ZoneScopedN("Frame Limit Sleep"); auto sleepTime = Time::GetInstance().SleepDuration(); if (sleepTime.count() > 0) { std::this_thread::sleep_for(sleepTime); } } } gfxDevice.waitIdle(); } void App::cleanup() { if (cleanedUp || cleaningUp) { return; } cleaningUp = true; spdlog::info("Cleaning up"); std::exception_ptr firstException; const auto attempt = [&firstException](auto&& operation) { try { operation(); } catch (...) { if (!firstException) { firstException = std::current_exception(); } } }; if (customInitStarted) { attempt([this] { customCleanup(); }); } attempt([] { SceneManager::GetInstance().Destroy(); }); attempt([] { FontCache::GetInstance().Clear(); }); attempt([this] { renderer.cleanup(gfxDevice); }); attempt([this] { imguiPass.cleanup(); }); attempt([this] { resources.cleanup(gfxDevice); }); attempt([this] { gfxDevice.cleanup(); }); window.Destroy(); AssetFS::GetInstance().Reset(); customInitStarted = false; cleanedUp = true; cleaningUp = false; if (firstException) { std::rethrow_exception(firstException); } } void App::drawWindowsMenu() { // ImGui appends to the main menu bar, so this ends up next to whatever menus the game added if (!ImGui::BeginMainMenuBar()) { return; } if (ImGui::BeginMenu("Windows")) { ImGui::MenuItem("Debug", nullptr, &m_DebugWindows.stats); ImGui::MenuItem("Physics", nullptr, &m_DebugWindows.physics); ImGui::MenuItem("Inspector", nullptr, &m_DebugWindows.inspector); ImGui::MenuItem("Image Cache", nullptr, &m_DebugWindows.imageCache); customWindowsMenu(); ImGui::EndMenu(); } ImGui::EndMainMenuBar(); } void App::drawPhysicsPanel(float dt, float fixedDt, float accumulator) { if (!ImGui::Begin("Physics", &m_DebugWindows.physics)) { ImGui::End(); return; } ImGui::Text("Frame dt: %.3f ms", dt * 1000.0f); ImGui::Text("Fixed dt: %.3f ms", fixedDt * 1000.0f); ImGui::Text("Accumulator: %.3f ms", accumulator * 1000.0f); ImGui::Text("Steps last frame: %d", m_PhysicsStepsLastFrame); ImGui::Separator(); if (ImGui::Button(m_PhysicsPaused ? "Resume Physics" : "Pause Physics")) { m_PhysicsPaused = !m_PhysicsPaused; } ImGui::SameLine(); if (!m_PhysicsPaused) { ImGui::BeginDisabled(); ImGui::Button("Step Physics"); ImGui::EndDisabled(); } else { if (ImGui::Button("Step Physics")) { m_PhysicsStepOnce = true; } } ImGui::Checkbox("Paused", &m_PhysicsPaused); ImGui::Separator(); ImGui::SliderFloat("Physics Time Scale", &m_PhysicsTimeScale, 0.0f, 2.0f, "%.2fx"); if (ImGui::Button("Reset Time Scale")) { m_PhysicsTimeScale = 1.0f; } ImGui::End(); }