Files
WickedEngine/WickedEngine/wiApplication.cpp
T

609 lines
17 KiB
C++

#include "wiApplication.h"
#include "wiRenderPath.h"
#include "wiRenderer.h"
#include "wiHelper.h"
#include "wiTimer.h"
#include "wiInput.h"
#include "wiBacklog.h"
#include "wiApplication_BindLua.h"
#include "wiVersion.h"
#include "wiEnums.h"
#include "wiTextureHelper.h"
#include "wiProfiler.h"
#include "wiInitializer.h"
#include "wiArguments.h"
#include "wiFont.h"
#include "wiImage.h"
#include "wiEventHandler.h"
#include "wiGraphicsDevice_DX12.h"
#include "wiGraphicsDevice_Vulkan.h"
#include <string>
#include <algorithm>
#include <new>
#include <cstdlib>
#include <atomic>
static std::atomic<uint32_t> number_of_heap_allocations{ 0 };
static std::atomic<size_t> size_of_heap_allocations{ 0 };
using namespace wi::graphics;
namespace wi
{
void Application::Initialize()
{
if (initialized)
{
return;
}
initialized = true;
wi::initializer::InitializeComponentsAsync();
}
void Application::ActivatePath(RenderPath* component, float fadeSeconds, wi::Color fadeColor)
{
if (component != nullptr)
{
component->init(canvas);
}
// Fade manager will activate on fadeout
fadeManager.Clear();
fadeManager.Start(fadeSeconds, fadeColor, [this, component]() {
if (GetActivePath() != nullptr)
{
GetActivePath()->Stop();
}
if (component != nullptr)
{
component->Start();
}
activePath = component;
});
fadeManager.Update(0); // If user calls ActivatePath without fadeout, it will be instant
}
void Application::Run()
{
if (!initialized)
{
// Initialize in a lazy way, so the user application doesn't have to call this explicitly
Initialize();
initialized = true;
}
wi::font::UpdateAtlas();
ColorSpace colorspace = graphicsDevice->GetSwapChainColorSpace(&swapChain);
if (!wi::initializer::IsInitializeFinished())
{
// Until engine is not loaded, present initialization screen...
CommandList cmd = graphicsDevice->BeginCommandList();
graphicsDevice->RenderPassBegin(&swapChain, cmd);
Viewport viewport;
viewport.width = (float)swapChain.desc.width;
viewport.height = (float)swapChain.desc.height;
graphicsDevice->BindViewports(1, &viewport, cmd);
if (wi::initializer::IsInitializeFinished(wi::initializer::INITIALIZED_SYSTEM_FONT))
{
wi::backlog::DrawOutputText(canvas, cmd, colorspace);
}
graphicsDevice->RenderPassEnd(cmd);
graphicsDevice->SubmitCommandLists();
return;
}
#ifdef WICKEDENGINE_BUILD_DX12
static bool startup_workaround = false;
if (!startup_workaround)
{
startup_workaround = true;
if (dynamic_cast<GraphicsDevice_DX12*>(graphicsDevice.get()))
{
CommandList cmd = graphicsDevice->BeginCommandList();
wi::renderer::Workaround(1, cmd);
graphicsDevice->SubmitCommandLists();
}
}
#endif
static bool startup_script = false;
if (!startup_script)
{
startup_script = true;
wi::lua::RegisterObject(wi::lua::Application_BindLua::className, "main", new wi::lua::Application_BindLua(this));
wi::lua::RegisterObject(wi::lua::Application_BindLua::className, "application", new wi::lua::Application_BindLua(this));
wi::lua::RunFile("startup.lua");
}
if (!is_window_active && !wi::arguments::HasArgument("alwaysactive"))
{
// If the application is not active, disable Update loops:
deltaTimeAccumulator = 0;
return;
}
wi::profiler::BeginFrame();
deltaTime = float(std::max(0.0, timer.record_elapsed_seconds()));
wi::input::Update(window, canvas);
// Wake up the events that need to be executed on the main thread, in thread safe manner:
wi::eventhandler::FireEvent(wi::eventhandler::EVENT_THREAD_SAFE_POINT, 0);
const float dt = framerate_lock ? (1.0f / targetFrameRate) : deltaTime;
fadeManager.Update(dt);
if (GetActivePath() != nullptr)
{
GetActivePath()->colorspace = colorspace;
GetActivePath()->init(canvas);
GetActivePath()->PreUpdate();
}
// Fixed time update:
auto range = wi::profiler::BeginRangeCPU("Fixed Update");
{
if (frameskip)
{
deltaTimeAccumulator += dt;
if (deltaTimeAccumulator > 10)
{
// application probably lost control, fixed update would take too long
deltaTimeAccumulator = 0;
}
const float targetFrameRateInv = 1.0f / targetFrameRate;
while (deltaTimeAccumulator >= targetFrameRateInv)
{
FixedUpdate();
deltaTimeAccumulator -= targetFrameRateInv;
}
}
else
{
FixedUpdate();
}
}
wi::profiler::EndRange(range); // Fixed Update
// Variable-timed update:
Update(dt);
Render();
// Begin final compositing:
CommandList cmd = graphicsDevice->BeginCommandList();
wi::image::SetCanvas(canvas);
wi::font::SetCanvas(canvas);
Viewport viewport;
viewport.width = (float)swapChain.desc.width;
viewport.height = (float)swapChain.desc.height;
graphicsDevice->BindViewports(1, &viewport, cmd);
bool colorspace_conversion_required = colorspace == ColorSpace::HDR10_ST2084;
if (colorspace_conversion_required)
{
// In HDR10, we perform the compositing in a custom linear color space render target
graphicsDevice->RenderPassBegin(&renderpass, cmd);
}
else
{
// If swapchain is SRGB or Linear HDR, it can be used for blending
// - If it is SRGB, the render path will ensure tonemapping to SDR
// - If it is Linear HDR, we can blend trivially in linear space
graphicsDevice->RenderPassBegin(&swapChain, cmd);
}
Compose(cmd);
graphicsDevice->RenderPassEnd(cmd);
if (colorspace_conversion_required)
{
// In HDR10, we perform a final mapping from linear to HDR10, into the swapchain
graphicsDevice->RenderPassBegin(&swapChain, cmd);
wi::image::Params fx;
fx.enableFullScreen();
fx.enableHDR10OutputMapping();
wi::image::Draw(&rendertarget, fx, cmd);
graphicsDevice->RenderPassEnd(cmd);
}
wi::input::ClearForNextFrame();
wi::profiler::EndFrame(cmd);
graphicsDevice->SubmitCommandLists();
}
void Application::Update(float dt)
{
auto range = wi::profiler::BeginRangeCPU("Update");
wi::lua::SetDeltaTime(double(dt));
wi::lua::Update();
wi::backlog::Update(canvas, dt);
if (GetActivePath() != nullptr)
{
GetActivePath()->Update(dt);
GetActivePath()->PostUpdate();
}
wi::profiler::EndRange(range); // Update
}
void Application::FixedUpdate()
{
wi::lua::FixedUpdate();
if (GetActivePath() != nullptr)
{
GetActivePath()->FixedUpdate();
}
}
void Application::Render()
{
auto range = wi::profiler::BeginRangeCPU("Render");
wi::lua::Render();
if (GetActivePath() != nullptr)
{
GetActivePath()->Render();
}
wi::profiler::EndRange(range); // Render
}
void Application::Compose(CommandList cmd)
{
auto range = wi::profiler::BeginRangeCPU("Compose");
ColorSpace colorspace = graphicsDevice->GetSwapChainColorSpace(&swapChain);
if (GetActivePath() != nullptr)
{
GetActivePath()->Compose(cmd);
}
if (fadeManager.IsActive())
{
// display fade rect
wi::image::Params fx;
fx.enableFullScreen();
fx.color = fadeManager.color;
fx.opacity = fadeManager.opacity;
wi::image::Draw(wi::texturehelper::getWhite(), fx, cmd);
}
// Draw the information display
if (infoDisplay.active)
{
infodisplay_str.clear();
if (infoDisplay.watermark)
{
infodisplay_str += "Wicked Engine ";
infodisplay_str += wi::version::GetVersionString();
infodisplay_str += " ";
#if defined(_ARM)
infodisplay_str += "[ARM]";
#elif defined(_WIN64)
infodisplay_str += "[64-bit]";
#elif defined(_WIN32)
infodisplay_str += "[32-bit]";
#endif
#ifdef PLATFORM_UWP
infodisplay_str += "[UWP]";
#endif
#ifdef WICKEDENGINE_BUILD_DX12
if (dynamic_cast<GraphicsDevice_DX12*>(graphicsDevice.get()))
{
infodisplay_str += "[DX12]";
}
#endif
#ifdef WICKEDENGINE_BUILD_VULKAN
if (dynamic_cast<GraphicsDevice_Vulkan*>(graphicsDevice.get()))
{
infodisplay_str += "[Vulkan]";
}
#endif
#ifdef _DEBUG
infodisplay_str += "[DEBUG]";
#endif
if (graphicsDevice->IsDebugDevice())
{
infodisplay_str += "[debugdevice]";
}
infodisplay_str += "\n";
}
if (infoDisplay.device_name)
{
infodisplay_str += "Device: " + graphicsDevice->GetDeviceName() + "\n";
}
if (infoDisplay.resolution)
{
infodisplay_str += "Resolution: " + std::to_string(canvas.GetPhysicalWidth()) + " x " + std::to_string(canvas.GetPhysicalHeight()) + " (" + std::to_string(int(canvas.GetDPI())) + " dpi)\n";
}
if (infoDisplay.logical_size)
{
infodisplay_str += "Logical Size: " + std::to_string(int(canvas.GetLogicalWidth())) + " x " + std::to_string(int(canvas.GetLogicalHeight())) + "\n";
}
if (infoDisplay.colorspace)
{
infodisplay_str += "Color Space: ";
ColorSpace colorSpace = graphicsDevice->GetSwapChainColorSpace(&swapChain);
switch (colorSpace)
{
default:
case wi::graphics::ColorSpace::SRGB:
infodisplay_str += "sRGB";
break;
case wi::graphics::ColorSpace::HDR10_ST2084:
infodisplay_str += "ST.2084 (HDR10)";
break;
case wi::graphics::ColorSpace::HDR_LINEAR:
infodisplay_str += "Linear (HDR)";
break;
}
infodisplay_str += "\n";
}
if (infoDisplay.fpsinfo)
{
deltatimes[fps_avg_counter++ % arraysize(deltatimes)] = deltaTime;
float displaydeltatime = deltaTime;
if (fps_avg_counter > arraysize(deltatimes))
{
float avg_time = 0;
for (int i = 0; i < arraysize(deltatimes); ++i)
{
avg_time += deltatimes[i];
}
displaydeltatime = avg_time / arraysize(deltatimes);
}
infodisplay_str += std::to_string(int(std::round(1.0f / displaydeltatime))) + " FPS\n";
}
if (infoDisplay.heap_allocation_counter)
{
infodisplay_str += "Heap allocations per frame: " + std::to_string(number_of_heap_allocations.load()) + " (" + std::to_string(size_of_heap_allocations.load()) + " bytes)\n";
number_of_heap_allocations.store(0);
size_of_heap_allocations.store(0);
}
if (infoDisplay.pipeline_count)
{
infodisplay_str += "Graphics pipelines active: " + std::to_string(graphicsDevice->GetActivePipelineCount()) + "\n";
}
wi::font::Params params = wi::font::Params(
4,
4,
infoDisplay.size,
wi::font::WIFALIGN_LEFT,
wi::font::WIFALIGN_TOP,
wi::Color::White(),
wi::Color::Shadow()
);
params.shadow_softness = 0.4f;
// Explanation: this compose pass is in LINEAR space if display output is linear or HDR10
// If HDR10, the HDR10 output mapping will be performed on whole image later when drawing to swapchain
if (colorspace != ColorSpace::SRGB)
{
params.enableLinearOutputMapping(9);
}
params.cursor = wi::font::Draw(infodisplay_str, params, cmd);
// VRAM:
{
GraphicsDevice::MemoryUsage vram = graphicsDevice->GetMemoryUsage();
bool warn = false;
if (vram.usage > vram.budget)
{
params.color = wi::Color::Error();
warn = true;
}
else if (float(vram.usage) / float(vram.budget) > 0.9f)
{
params.color = wi::Color::Warning();
warn = true;
}
if (infoDisplay.vram_usage || warn)
{
params.cursor = wi::font::Draw("VRAM usage: " + std::to_string(vram.usage / 1024 / 1024) + "MB / " + std::to_string(vram.budget / 1024 / 1024) + "MB\n", params, cmd);
params.color = wi::Color::White();
}
}
// Write warnings below:
params.color = wi::Color::Warning();
#ifdef _DEBUG
params.cursor = wi::font::Draw("Warning: This is a [DEBUG] build, performance will be slow!\n", params, cmd);
#endif
if (graphicsDevice->IsDebugDevice())
{
params.cursor = wi::font::Draw("Warning: Graphics is in [debugdevice] mode, performance will be slow!\n", params, cmd);
}
// Write errors below:
params.color = wi::Color::Error();
if (wi::renderer::GetShaderMissingCount() > 0)
{
params.cursor = wi::font::Draw(std::to_string(wi::renderer::GetShaderMissingCount()) + " shaders missing! Check the backlog for more information!\n", params, cmd);
}
if (wi::renderer::GetShaderErrorCount() > 0)
{
params.cursor = wi::font::Draw(std::to_string(wi::renderer::GetShaderErrorCount()) + " shader compilation errors! Check the backlog for more information!\n", params, cmd);
}
if (infoDisplay.colorgrading_helper)
{
wi::image::Draw(wi::texturehelper::getColorGradeDefault(), wi::image::Params(0, 0, 256.0f / canvas.GetDPIScaling(), 16.0f / canvas.GetDPIScaling()), cmd);
}
}
wi::profiler::DrawData(canvas, 4, 120, cmd, colorspace);
wi::backlog::Draw(canvas, cmd, colorspace);
wi::profiler::EndRange(range); // Compose
}
void Application::SetWindow(wi::platform::window_type window)
{
this->window = window;
// User can also create a graphics device if custom logic is desired, but they must do before this function!
if (graphicsDevice == nullptr)
{
ValidationMode validationMode = ValidationMode::Disabled;
if (wi::arguments::HasArgument("debugdevice"))
{
validationMode = ValidationMode::Enabled;
}
if (wi::arguments::HasArgument("gpuvalidation"))
{
validationMode = ValidationMode::GPU;
}
bool use_dx12 = wi::arguments::HasArgument("dx12");
bool use_vulkan = wi::arguments::HasArgument("vulkan");
#ifndef WICKEDENGINE_BUILD_DX12
if (use_dx12) {
wi::helper::messageBox("The engine was built without DX12 support!", "Error");
use_dx12 = false;
}
#endif
#ifndef WICKEDENGINE_BUILD_VULKAN
if (use_vulkan) {
wi::helper::messageBox("The engine was built without Vulkan support!", "Error");
use_vulkan = false;
}
#endif
if (!use_dx12 && !use_vulkan)
{
#if defined(WICKEDENGINE_BUILD_DX12)
use_dx12 = true;
#elif defined(WICKEDENGINE_BUILD_VULKAN)
use_vulkan = true;
#else
wi::backlog::post("No rendering backend is enabled! Please enable at least one so we can use it as default", wi::backlog::LogLevel::Error);
assert(false);
#endif
}
assert(use_dx12 || use_vulkan);
if (use_vulkan)
{
#ifdef WICKEDENGINE_BUILD_VULKAN
wi::renderer::SetShaderPath(wi::renderer::GetShaderPath() + "spirv/");
graphicsDevice = std::make_unique<GraphicsDevice_Vulkan>(window, validationMode);
#endif
}
else if (use_dx12)
{
#ifdef WICKEDENGINE_BUILD_DX12
wi::renderer::SetShaderPath(wi::renderer::GetShaderPath() + "hlsl6/");
graphicsDevice = std::make_unique<GraphicsDevice_DX12>(validationMode);
#endif
}
}
wi::graphics::GetDevice() = graphicsDevice.get();
canvas.init(window);
SwapChainDesc desc;
if (swapChain.IsValid())
{
// it will only resize, but keep format and other settings
desc = swapChain.desc;
}
else
{
// initialize for the first time
desc.buffer_count = 3;
desc.format = Format::R10G10B10A2_UNORM;
}
desc.width = canvas.GetPhysicalWidth();
desc.height = canvas.GetPhysicalHeight();
desc.allow_hdr = allow_hdr;
bool success = graphicsDevice->CreateSwapChain(&desc, window, &swapChain);
assert(success);
swapChainVsyncChangeEvent = wi::eventhandler::Subscribe(wi::eventhandler::EVENT_SET_VSYNC, [this](uint64_t userdata) {
SwapChainDesc desc = swapChain.desc;
desc.vsync = userdata != 0;
bool success = graphicsDevice->CreateSwapChain(&desc, nullptr, &swapChain);
assert(success);
});
if (graphicsDevice->GetSwapChainColorSpace(&swapChain) == ColorSpace::HDR10_ST2084)
{
TextureDesc desc;
desc.width = swapChain.desc.width;
desc.height = swapChain.desc.height;
desc.format = Format::R11G11B10_FLOAT;
desc.bind_flags = BindFlag::RENDER_TARGET | BindFlag::SHADER_RESOURCE;
bool success = graphicsDevice->CreateTexture(&desc, nullptr, &rendertarget);
assert(success);
graphicsDevice->SetName(&rendertarget, "Application::rendertarget");
RenderPassDesc renderpassdesc;
renderpassdesc.attachments.push_back(RenderPassAttachment::RenderTarget(rendertarget, RenderPassAttachment::LoadOp::CLEAR));
success = graphicsDevice->CreateRenderPass(&renderpassdesc, &renderpass);
assert(success);
}
}
}
// Heap alloc replacements are used to count heap allocations:
// It is good practice to reduce the amount of heap allocations that happen during the frame,
// so keep an eye on the info display of the engine while Application::InfoDisplayer::heap_allocation_counter is enabled
void* operator new(std::size_t size) {
number_of_heap_allocations.fetch_add(1);
size_of_heap_allocations.fetch_add(size);
void* p = malloc(size);
if (!p) throw std::bad_alloc();
return p;
}
void* operator new[](std::size_t size) {
number_of_heap_allocations.fetch_add(1);
size_of_heap_allocations.fetch_add(size);
void* p = malloc(size);
if (!p) throw std::bad_alloc();
return p;
}
void* operator new[](std::size_t size, const std::nothrow_t&) throw() {
number_of_heap_allocations.fetch_add(1);
size_of_heap_allocations.fetch_add(size);
return malloc(size);
}
void* operator new(std::size_t size, const std::nothrow_t&) throw() {
number_of_heap_allocations.fetch_add(1);
size_of_heap_allocations.fetch_add(size);
return malloc(size);
}
void operator delete(void* ptr) throw() { free(ptr); }
void operator delete (void* ptr, const std::nothrow_t&) throw() { free(ptr); }
void operator delete[](void* ptr) throw() { free(ptr); }
void operator delete[](void* ptr, const std::nothrow_t&) throw() { free(ptr); }