422 lines
11 KiB
C++
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);
|
|
}
|
|
}
|