452 lines
11 KiB
C++
452 lines
11 KiB
C++
#include <algorithm>
|
|
#include <chrono>
|
|
#include <exception>
|
|
#include <thread>
|
|
#include <stdexcept>
|
|
#include <destrum/App.h>
|
|
|
|
#include <destrum/FS/AssetFS.h>
|
|
#include <destrum/Graphics/Resources/FontCache.h>
|
|
#include <destrum/Scene/SceneManager.h>
|
|
#include <destrum/Util/DeltaTime.h>
|
|
|
|
#include "imgui.h"
|
|
#include "glm/gtx/transform.hpp"
|
|
#include "spdlog/spdlog.h"
|
|
|
|
#include <tracy/Tracy.hpp>
|
|
#include <common/TracySystem.hpp>
|
|
|
|
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<float>(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<float>(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<int64_t>(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();
|
|
}
|