diff --git a/CMakeLists.txt b/CMakeLists.txt index 2db681326..1c68cc974 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -252,7 +252,10 @@ if (NOT USE_SYSTEM_LIBLUA) include_directories(contrib/lua) endif (NOT USE_SYSTEM_LIBLUA) +add_subdirectory(src/core) + define_pioneer_library(pioneer-lib PIONEER_CXX_FILES PIONEER_HXX_FILES) +target_link_libraries(pioneer-lib PUBLIC pioneer-core) add_subdirectory(src/lua) diff --git a/src/core/Application.cpp b/src/core/Application.cpp new file mode 100644 index 000000000..75b968a74 --- /dev/null +++ b/src/core/Application.cpp @@ -0,0 +1,160 @@ +// Copyright © 2008-2020 Pioneer Developers. See AUTHORS.txt for details +// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +#include "Application.h" +#include "FileSystem.h" +#include "OS.h" +#include "profiler/Profiler.h" +#include "utils.h" + +#include "SDL_timer.h" + +#include + +void Application::QueueLifecycle(std::shared_ptr cycle) +{ + if (!cycle) + throw std::runtime_error("Invalid Lifecycle object pushed to Application queue!"); + + m_queuedLifecycles.push(cycle); +} + +void Application::Startup() +{ + PROFILE_SCOPED() + + OS::EnableBreakpad(); + OS::NotifyLoadBegin(); + + FileSystem::Init(); + // ensure the config directory exists + FileSystem::userFiles.MakeDirectory(""); + +#ifdef PIONEER_PROFILER + FileSystem::userFiles.MakeDirectory("profiler"); +#endif +} + +void Application::Shutdown() +{ + FileSystem::Uninit(); +} + +bool Application::StartLifecycle() +{ + // can't start a lifecycle if there are no more queued. + if (!m_priorityLifecycle && !m_queuedLifecycles.size()) + return false; + + // If we still have an active lifestyle, we don't need to queue any more. + if (m_activeLifecycle) + throw std::runtime_error("Attempt to start a new lifecycle object while one is still active!"); + + // Lifecycle objects returned from Lifecycle::End() take priority over queued objects + if (m_priorityLifecycle) { + m_activeLifecycle = m_priorityLifecycle; + m_priorityLifecycle = nullptr; + } else { + m_activeLifecycle = std::move(m_queuedLifecycles.front()); + m_queuedLifecycles.pop(); + } + + m_activeLifecycle->m_application = this; + m_activeLifecycle->Start(); + return true; +} + +void Application::EndLifecycle() +{ + // Can't end a lifecycle if we don't have one active. + if (!m_activeLifecycle) + return; + + // The only time we should have a prioritized lifecycle is + // between EndLifecycle() and the next StartLifecycle(). + // If it's still set, something has gone wrong. + if (m_priorityLifecycle) + throw std::runtime_error("Unable to prioritize lifecycle object due to already-prioritized lifecycle. Did you mess up your control flow?"); + + m_activeLifecycle->End(); + m_activeLifecycle->m_application = nullptr; + m_activeLifecycle->m_endLifecycle = false; + + // wait until we've finished the control flow for the lifecycle; + // the lifecycle may decide to set the next lifecycle in End() + m_priorityLifecycle = m_activeLifecycle->m_nextLifecycle; + m_activeLifecycle = nullptr; +} + +void Application::ClearQueuedLifecycles() +{ + // std::queue doesn't provide a clear() method + // so simply invoke the destructor by reinitializing the queue + m_queuedLifecycles = {}; +} + +void Application::Run() +{ + Profiler::Clock m_runtime{}; + m_runtime.Start(); + + Startup(); + + if (!m_queuedLifecycles.size()) + throw std::runtime_error("Application::Run must have a queued lifecycle object (did you forget to queue one?)"); + +#ifdef PIONEER_PROFILER + // For good measure, reset the profiler at the start of the first frame + Profiler::reset(); +#endif + + // SoftStop updates the elapsed time measured by the clock, and continues to run the clock. + m_runtime.SoftStop(); + m_totalTime = m_runtime.seconds(); + while (m_applicationRunning) { + m_runtime.SoftStop(); + double thisTime = m_runtime.seconds(); + m_deltaTime = thisTime - m_totalTime; + m_totalTime = thisTime; + + if (!m_activeLifecycle) { + if (!StartLifecycle()) + break; + } + + // If there is no lifecycle object to start, end now. + if (!m_activeLifecycle) + break; + + BeginFrame(); + + // The PreUpdate hook should be used for setting up per-frame state, etc. + PreUpdate(); + + m_activeLifecycle->Update(m_deltaTime); + + // The PostUpdate hook should be used for finalizing per-frame state, rendering, etc. + PostUpdate(); + + EndFrame(); + + if (m_activeLifecycle->m_endLifecycle) { + EndLifecycle(); + } + + // TODO: design a better profiling interface, cache results of slow frames so they can be inspected + // in pigui, etc. + // m_runtime.SoftStop(); + // thisTime = m_runtime.seconds(); + // if (thisTime - m_totalTime > 0.100) // profile frames taking longer than 100ms + // Profiler::dumphtml(FileSystem::JoinPathBelow(FileSystem::GetUserDir(), "profiler"); + +#ifdef PIONEER_PROFILER + // reset the profiler at the end of the frame + if (!m_activeLifecycle || !m_activeLifecycle->m_profilerAccumulate) + Profiler::reset(); +#endif + } + + Shutdown(); +} diff --git a/src/core/Application.h b/src/core/Application.h new file mode 100644 index 000000000..15ad0a21e --- /dev/null +++ b/src/core/Application.h @@ -0,0 +1,111 @@ +// Copyright © 2008-2020 Pioneer Developers. See AUTHORS.txt for details +// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +#pragma once + +#include +#include + +class Application { +public: + Application(){}; + virtual ~Application(){}; + + class Lifecycle { + public: + Lifecycle(){}; + Lifecycle(bool profilerAccumulate) : + m_profilerAccumulate(profilerAccumulate){}; + virtual ~Lifecycle(){}; + + // Once called, the lifecycle is terminated at the end of the current update. + void RequestEndLifecycle() { m_endLifecycle = true; } + + protected: + friend class Application; + + // Called when the Lifecycle begins execution + virtual void Start(){}; + + // Called in a continual loop and passed the time since the last invocation. + virtual void Update(float deltaTime) = 0; + + // Called when the lifecycle is leaving execution + // If a valid Lifecycle pointer is returned, it is run before any queued lifecycles. + virtual void End() {} + + // Set a lifecycle that should begin immediately after this lifecycle has finished execution + void SetNextLifecycle(std::shared_ptr l) + { + m_nextLifecycle = l; + } + + Application *GetApp(); + + private: + // set to true when you want to accumulate all updates in a lifecycle into a single profile frame + bool m_profilerAccumulate = false; + bool m_endLifecycle = false; + std::shared_ptr m_nextLifecycle; + Application *m_application; + }; + + // Add a lifecycle object to the queue of pending lifecycles + // You must add at least one lifecycle before calling Run() + void QueueLifecycle(std::shared_ptr cycle); + + // Use this function very sparingly (e.g. when the application is shutting down) + void ClearQueuedLifecycles(); + + // Runs the application as long as there is a valid lifecycle object + void Run(); + + // Get the time between the start of the last update and the current one + float DeltaTime() { return m_deltaTime; } + + double GetTime() { return m_totalTime; } + +protected: + // Hooks for inheriting classes to add their own behaviors to. + + // Runs before the main loop begins + virtual void Startup(); + + // Runs after the main loop ends + virtual void Shutdown(); + + // Runs at the top of each frame. + virtual void BeginFrame() {} + + // Runs before each Update() call + virtual void PreUpdate() {} + + // Runs after each Update() call + virtual void PostUpdate() {} + + // Runs at the bottom of each frame. + virtual void EndFrame(){}; + + // Request the application quit immediately at the end of the update, + // ignoring all queued lifecycles + void RequestQuit() { m_applicationRunning = false; } + + Lifecycle *GetActiveLifecycle() { return m_activeLifecycle.get(); } + +private: + bool StartLifecycle(); + void EndLifecycle(); + + bool m_applicationRunning; + float m_deltaTime; + double m_totalTime; + + // The lifecycle we're actually running right now + std::shared_ptr m_activeLifecycle; + + // A lifecycle that should be run next before the rest of the queue. + std::shared_ptr m_priorityLifecycle; + + // Lifecycles queued by QueueLifecycle() + std::queue> m_queuedLifecycles; +}; diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt new file mode 100644 index 000000000..c5eaaf956 --- /dev/null +++ b/src/core/CMakeLists.txt @@ -0,0 +1,11 @@ + +list(APPEND CORE_SRC_FOLDERS + ${CMAKE_CURRENT_SOURCE_DIR} +) + +# Creates variables CORE_CXX_FILES and CORE_HXX_FILES +add_source_folders(CORE CORE_SRC_FOLDERS) + +# Creates a library, adds it to the build, and sets C++ target properties on it +define_pioneer_library(pioneer-core CORE_CXX_FILES CORE_HXX_FILES) +target_include_directories(pioneer-core PRIVATE ${CMAKE_BINARY_DIR}) diff --git a/src/core/GuiApplication.cpp b/src/core/GuiApplication.cpp new file mode 100644 index 000000000..4e315bf11 --- /dev/null +++ b/src/core/GuiApplication.cpp @@ -0,0 +1,147 @@ +// Copyright © 2008-2020 Pioneer Developers. See AUTHORS.txt for details +// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +#include "GuiApplication.h" +#include "GameConfig.h" +#include "OS.h" + +#include "SDL.h" +#include "graphics/Drawables.h" +#include "graphics/Graphics.h" +#include "graphics/RenderState.h" +#include "graphics/RenderTarget.h" +#include "graphics/Renderer.h" +#include "graphics/Texture.h" +#include "utils.h" +#include "versioningInfo.h" + +// FIXME: add support for offscreen rendertarget drawing and multisample RTs +#define RTT 0 + +void GuiApplication::BeginFrame() +{ +#if RTT + m_renderer->SetRenderTarget(m_renderTarget); +#endif + // TODO: render target size + m_renderer->SetViewport(0, 0, Graphics::GetScreenWidth(), Graphics::GetScreenHeight()); + m_renderer->BeginFrame(); +} + +void GuiApplication::DrawRenderTarget() +{ +#if RTT + m_renderer->BeginFrame(); + m_renderer->SetViewport(0, 0, Graphics::GetScreenWidth(), Graphics::GetScreenHeight()); + m_renderer->SetTransform(matrix4x4f::Identity()); + + { + m_renderer->SetMatrixMode(Graphics::MatrixMode::PROJECTION); + m_renderer->PushMatrix(); + m_renderer->SetOrthographicProjection(0, Graphics::GetScreenWidth(), Graphics::GetScreenHeight(), 0, -1, 1); + m_renderer->SetMatrixMode(Graphics::MatrixMode::MODELVIEW); + m_renderer->PushMatrix(); + m_renderer->LoadIdentity(); + } + + m_renderQuad->Draw(m_renderer); + + { + m_renderer->SetMatrixMode(Graphics::MatrixMode::PROJECTION); + m_renderer->PopMatrix(); + m_renderer->SetMatrixMode(Graphics::MatrixMode::MODELVIEW); + m_renderer->PopMatrix(); + } + + m_renderer->EndFrame(); +#endif +} + +void GuiApplication::EndFrame() +{ +#if RTT + m_renderer->SetRenderTarget(nullptr); + DrawRenderTarget(); +#endif + m_renderer->EndFrame(); + m_renderer->SwapBuffers(); +} + +Graphics::RenderTarget *GuiApplication::CreateRenderTarget(const Graphics::Settings &settings) +{ + /* @fluffyfreak here's a rendertarget implementation you can use for oculusing and other things. It's pretty simple: + - fill out a RenderTargetDesc struct and call Renderer::CreateRenderTarget + - pass target to Renderer::SetRenderTarget to start rendering to texture + - set up viewport, clear etc, then draw as usual + - SetRenderTarget(0) to resume render to screen + - you can access the attached texture with GetColorTexture to use it with a material + You can reuse the same target with multiple textures. + In that case, leave the color format to NONE so the initial texture is not created, then use SetColorTexture to attach your own. + */ +#if RTT + Graphics::RenderStateDesc rsd; + rsd.depthTest = false; + rsd.depthWrite = false; + rsd.blendMode = Graphics::BLEND_SOLID; + m_renderState.reset(m_renderer->CreateRenderState(rsd)); + + // Complete the RT description so we can request a buffer. + Graphics::RenderTargetDesc rtDesc( + width, + height, + Graphics::TEXTURE_RGB_888, // don't create a texture + Graphics::TEXTURE_DEPTH, + false, settings.requestedSamples); + m_renderTarget.reset(m_renderer->CreateRenderTarget(rtDesc)); + + m_renderTarget->SetColorTexture(*m_renderTexture); +#endif + + return nullptr; +} + +Graphics::Renderer *GuiApplication::StartupRenderer(const GameConfig *config, bool hidden) +{ + PROFILE_SCOPED() + // Initialize SDL + Uint32 sdlInitFlags = SDL_INIT_VIDEO | SDL_INIT_JOYSTICK; + if (SDL_Init(sdlInitFlags) < 0) { + Error("SDL initialization failed: %s\n", SDL_GetError()); + } + + OutputVersioningInfo(); + + // determine what renderer we should use, default to Opengl 3.x + const std::string rendererName = config->String("RendererName", Graphics::RendererNameFromType(Graphics::RENDERER_OPENGL_3x)); + // if we add new renderer types, make sure to update this logic + Graphics::RendererType rType = Graphics::RENDERER_OPENGL_3x; + + Graphics::Settings videoSettings = {}; + videoSettings.rendererType = rType; + videoSettings.width = config->Int("ScrWidth"); + videoSettings.height = config->Int("ScrHeight"); + videoSettings.fullscreen = (config->Int("StartFullscreen") != 0); + videoSettings.hidden = hidden; + videoSettings.requestedSamples = config->Int("AntiAliasingMode"); + videoSettings.vsync = (config->Int("VSync") != 0); + videoSettings.useTextureCompression = (config->Int("UseTextureCompression") != 0); + videoSettings.useAnisotropicFiltering = (config->Int("UseAnisotropicFiltering") != 0); + videoSettings.enableDebugMessages = (config->Int("EnableGLDebug") != 0); + videoSettings.gl3ForwardCompatible = (config->Int("GL3ForwardCompatible") != 0); + videoSettings.iconFile = OS::GetIconFilename(); + videoSettings.title = m_applicationTitle.c_str(); + + m_renderer.reset(Graphics::Init(videoSettings)); + m_renderTarget.reset(CreateRenderTarget(videoSettings)); + + return m_renderer.get(); +} + +void GuiApplication::ShutdownRenderer() +{ + m_renderTarget.reset(); + m_renderState.reset(); + m_renderer.reset(); + + SDL_QuitSubSystem(SDL_INIT_VIDEO); +} diff --git a/src/core/GuiApplication.h b/src/core/GuiApplication.h new file mode 100644 index 000000000..588d31327 --- /dev/null +++ b/src/core/GuiApplication.h @@ -0,0 +1,49 @@ +// Copyright © 2008-2020 Pioneer Developers. See AUTHORS.txt for details +// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +#pragma once + +#include "Application.h" +#include "GameConfig.h" +#include "RefCounted.h" + +#include "graphics/RenderState.h" +#include "graphics/RenderTarget.h" +#include "graphics/Renderer.h" + +class GuiApplication : public Application { +public: + GuiApplication(std::string title) : + Application(), m_applicationTitle(title) + {} + +protected: + Graphics::Renderer *GetRenderer() { return m_renderer.get(); } + + // Called at the end of the frame automatically, blits the RT onto the application + // framebuffer + void DrawRenderTarget(); + + // Call this from your Startup() method + Graphics::Renderer *StartupRenderer(const GameConfig *config, bool hidden = false); + + // Call this from your Shutdown() method + void ShutdownRenderer(); + + // Hook to bind the RT and clear the screen. + // If you override BeginFrame, make sure you call this. + void BeginFrame() override; + + // Hook to end the frame and draw to the application framebuffer. + // If you override EndFrame, make sure you call this. + void EndFrame() override; + +private: + Graphics::RenderTarget *CreateRenderTarget(const Graphics::Settings &settings); + + std::string m_applicationTitle; + + std::unique_ptr m_renderer; + std::unique_ptr m_renderTarget; + std::unique_ptr m_renderState; +};