#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { struct TracyFrameScope { ~TracyFrameScope() { FrameMark; } }; constexpr float MaxFrameDelta = 0.25f; constexpr int MaxFixedSteps = 5; [[nodiscard]] bool IsMouseEvent(const SDL_Event& event) { return event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP || event.type == SDL_MOUSEMOTION || event.type == SDL_MOUSEWHEEL; } [[nodiscard]] bool IsKeyboardEvent(const SDL_Event& event) { return event.type == SDL_KEYDOWN || event.type == SDL_KEYUP || event.type == SDL_TEXTINPUT; } } App::App() = default; App::~App() { if (!cleanedUp) { try { cleanup(); } catch (...) { } } } void App::init(const AppParams& params) { ZoneScopedN("App::init"); cleanedUp = false; customInitStarted = false; try { m_params = params; tracy::SetThreadName("Main Thread"); TracySetProgramName(params.appName.c_str()); AssetFS::GetInstance().Init(params.exeDir); JobSystem::GetInstance().Init(); 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); resources.init(gfxDevice); renderer.init(gfxDevice, resources, params.renderSize); InputManager::GetInstance().Init(); AudioSystem::GetInstance().Init(); Time::GetInstance().Update(); customInitStarted = true; customInit(); } catch (...) { try { cleanup(); } catch (...) { } throw; } } void App::attachLayer(std::unique_ptr newLayer) { if (layer) { layer->onDetach(); } layer = std::move(newLayer); if (layer) { layer->onAttach(); } } bool App::isPlaying() const { return SceneManager::GetInstance().IsPlaying(); } bool App::isPaused() const { return SceneManager::GetInstance().IsPaused(); } float App::getRenderAspectRatio() const { const VkExtent3D extent = renderer.getDrawImage().extent; return extent.height > 0 ? static_cast(extent.width) / static_cast(extent.height) : 1.0f; } Camera& App::getViewCamera() { if (layer) { if (Camera* camera = layer->viewCamera()) { return *camera; } } if (Camera* camera = gameCamera()) { return *camera; } return fallbackCamera; } void App::resizeRenderTarget(const glm::ivec2& size) { renderer.resize(gfxDevice, size); } void App::collectUnusedResources() { CollectUnusedResources(SceneManager::GetInstance(), resources, gfxDevice); AudioSystem::GetInstance().ReleaseUnusedClips(); } void App::run() { ZoneScopedN("App::run"); Time::GetInstance().Update(); simulation.fixedDeltaTime = static_cast(Time::GetInstance().FixedDeltaTime()); isRunning = true; while (isRunning) { TracyFrameScope tracyFrame; ZoneScopedN("App::Frame"); Time::GetInstance().Update(); const float dt = std::min(static_cast(Time::GetInstance().DeltaTime()), MaxFrameDelta); if (dt > 0.0f) { avgFPS = std::lerp(avgFPS, 1.0f / dt, 0.1f); TracyPlot("FPS", avgFPS); } simulation.accumulator += dt; processEvents(); if (!isRunning) { break; } if (window.IsMinimized()) { // The surface is 0x0 while minimized, there is nothing to render or present to simulation.accumulator = 0.0f; std::this_thread::sleep_for(std::chrono::milliseconds(16)); continue; } JobSystem::GetInstance().DrainMainThreadQueue(); if (layer) { layer->onBeginFrame(); } imguiPass.beginFrame(); if (layer) { layer->onImGuiBegin(dt); } { ZoneScopedN("Update"); customUpdate(dt); SceneManager::GetInstance().Update(dt); SceneManager::GetInstance().LateUpdate(dt); } if (layer) { ZoneScopedN("Layer ImGui"); layer->onImGui(dt); } imguiPass.endFrame(); if (layer) { layer->onEndFrame(); } runFixedSteps(); AudioSystem::GetInstance().Update(dt, isPlaying() && (isPaused() || simulation.paused), &getViewCamera()); bool skipFrame = false; handleSwapchainResize(skipFrame); if (skipFrame) { continue; } renderFrame(); if (frameLimit) { if (const auto sleepTime = Time::GetInstance().SleepDuration(); sleepTime.count() > 0) { std::this_thread::sleep_for(sleepTime); } } } } void App::processEvents() { ZoneScopedN("SDL Events"); auto& input = InputManager::GetInstance(); input.BeginFrame(); SDL_Event event; while (SDL_PollEvent(&event)) { imguiPass.handleEvent(event); if (event.type == SDL_QUIT) { isRunning = false; return; } window.ProcessEvent(event); if (window.ConsumeResized()) { resizePending = true; lastResizeTime = std::chrono::steady_clock::now(); } if (window.ConsumeCloseRequested()) { isRunning = false; return; } // ImGui wants every event while one of its windows is hovered or focused, a layer can claim // them back for the game (the editor viewport is an ImGui window) const AppLayer::InputFocus focus = layer ? layer->gameInputFocus() : AppLayer::InputFocus{}; const bool mouseCaptured = input.IsMouseCaptured(); const bool capturedByImgui = (IsMouseEvent(event) && !mouseCaptured && !focus.mouse && imguiPass.wantsMouse()) || (IsKeyboardEvent(event) && !focus.keyboard && imguiPass.wantsKeyboard()); if (!capturedByImgui && input.ProcessEvent(event)) { isRunning = false; } } } void App::runFixedSteps() { ZoneScopedN("FixedUpdate"); const float step = simulation.fixedDeltaTime * simulation.timeScale; int steps = 0; const auto fixedUpdate = [&] { customFixedUpdate(step); SceneManager::GetInstance().FixedUpdate(step); }; if (simulation.paused) { simulation.accumulator = 0.0f; if (simulation.stepOnce) { fixedUpdate(); steps = 1; simulation.stepOnce = false; } } else { while (simulation.accumulator >= simulation.fixedDeltaTime && steps < MaxFixedSteps) { fixedUpdate(); simulation.accumulator -= simulation.fixedDeltaTime; ++steps; } if (steps == MaxFixedSteps) { simulation.accumulator = 0.0f; } } simulation.stepsLastFrame = steps; Time::GetInstance().SetFixedAlpha(std::clamp(simulation.accumulator / simulation.fixedDeltaTime, 0.0f, 1.0f)); } void App::handleSwapchainResize(bool& skipFrame) { if (!gfxDevice.needsSwapchainRecreate() && !resizePending) { return; } skipFrame = true; if (resizePending && std::chrono::steady_clock::now() - lastResizeTime < std::chrono::milliseconds(100)) { return; } const glm::ivec2 size = window.GetDrawableSize(); if (size.x == 0 || size.y == 0 || !gfxDevice.recreateSwapchain(size.x, size.y)) { return; } spdlog::info("Recreated swapchain at {}x{}", size.x, size.y); imguiPass.onSwapchainRecreated(); if (!layer || !layer->hostsViewport()) { resizeRenderTarget(size); } onWindowResize(size.x, size.y); resizePending = false; } void App::renderFrame() { ZoneScopedN("Render"); const VkCommandBuffer cmd = gfxDevice.beginFrame(); if (cmd == VK_NULL_HANDLE) { return; } renderer.beginDrawing(gfxDevice); customRender(renderer); if (layer) { layer->onRender(renderer); } Camera& camera = getViewCamera(); if (const float aspectRatio = getRenderAspectRatio(); camera.GetAspectRatio() != aspectRatio) { camera.setAspectRatio(aspectRatio); } auto& sceneManager = SceneManager::GetInstance(); const SceneEnvironment environment = sceneManager.GetActiveScenes().empty() ? SceneEnvironment{} : sceneManager.GetCurrentScene().GetEnvironment(); const GameRenderer::SceneData sceneData{ .camera = camera, .ambientColor = environment.ambientColor, .ambientIntensity = environment.ambientIntensity, .fogColor = environment.fogColor, .fogDensity = environment.fogDensity, .skyboxRotation = environment.skyboxRotation, .exposure = environment.exposure, .tonemapper = environment.tonemapper, }; sceneManager.Render(RenderContext{ .renderer = renderer, .camera = camera, .sceneData = sceneData, }); renderer.endDrawing(); renderer.draw(cmd, gfxDevice, camera, sceneData); gfxDevice.endFrame(cmd, renderer.getDrawImage(), { .clearColor = {{0.0f, 0.0f, 0.0f, 1.0f}}, .imguiPass = &imguiPass, .copyDrawImageToSwapchain = !layer || !layer->hostsViewport(), }); } 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(); } } }; // The one place we wait for the GPU on shutdown: the cleanup calls below destroy // pipelines and other objects directly, which is only valid once nothing is in flight. attempt([this] { gfxDevice.waitIdle(); }); attempt([] { JobSystem::GetInstance().Shutdown(); }); attempt([this] { if (layer) { layer->onDetach(); layer.reset(); } }); if (customInitStarted) { attempt([this] { customCleanup(); }); } attempt([] { SceneManager::GetInstance().Destroy(); }); attempt([] { AudioSystem::GetInstance().Shutdown(); }); 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); } }