Files
Destrum/destrum/src/App.cpp
T
2026-10-24 00:59:22 +02:00

422 lines
11 KiB
C++

#include <destrum/App.h>
#include <algorithm>
#include <cmath>
#include <exception>
#include <thread>
#include <imgui.h>
#include <spdlog/spdlog.h>
#include <tracy/Tracy.hpp>
#include <common/TracySystem.hpp>
#include <destrum/Audio/AudioSystem.h>
#include <destrum/FS/AssetFS.h>
#include <destrum/Graphics/Resources/FontCache.h>
#include <destrum/Scene/ResourceCollector.h>
#include <destrum/Scene/Scene.h>
#include <destrum/Scene/SceneManager.h>
#include <destrum/Threading/JobSystem.h>
#include <destrum/Util/DeltaTime.h>
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<AppLayer> 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<float>(extent.width) / static_cast<float>(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<float>(Time::GetInstance().FixedDeltaTime());
isRunning = true;
while (isRunning) {
TracyFrameScope tracyFrame;
ZoneScopedN("App::Frame");
Time::GetInstance().Update();
const float dt = std::min(static_cast<float>(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);
}
}