17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-27 19:57:29 +01:00
Files
maszyna/rendering/vulkan14/vulkan14renderer.cpp

1091 lines
38 KiB
C++

/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "rendering/vulkan14/vulkan14context.h"
#include "rendering/nullrenderer.h"
#include "application/application.h"
#include "simulation/simulation.h"
#include "utilities/Globals.h"
#include "utilities/Logs.h"
#include "utilities/Timer.h"
#include "utilities/crashreporter.h"
#include "utilities/utilities.h"
#include "imgui/imgui.h"
namespace
{
// the colour the windows are cleared to once a scenery is loaded, until the scenery is drawn (stage D3 of the plan): the initial
// background of the opengl renderer (srgb). before that the windows are black, as with the opengl renderer
glm::vec3 const clear_colour{51.f / 255.f, 102.f / 255.f, 85.f / 255.f};
// VkPhysicalDeviceProperties::vendorID of nvidia, the gpus dlss runs on
std::uint32_t constexpr nvidia_vendor_id{0x10de};
// the ring each frame in flight streams its data through: the user interface, the blocks of the scene and of the light clusters,
// the light buffers of the passes, the vertices of the particles and the sky; the descriptors read the light buffers with the range
// of the whole gl::light_data, which the tail past the last frame leaves room for
VkDeviceSize const ring_size{16 * 1024 * 1024};
VkDeviceSize const ring_tail{(sizeof(gl::light_data) + 65535) / 65536 * 65536};
float srgb_to_linear(float const Value)
{
return (Value <= 0.04045f ? Value / 12.92f : std::pow((Value + 0.055f) / 1.055f, 2.4f));
}
// waits until Time, to a part of a millisecond: the presents of the frames generated by dlss are spread over the time of a frame. the
// sleeps of windows go by its timer, 15.6 ms unless a program asks for less, so it's a timer of a high resolution there (windows 10
// 1803 and newer), with what's left of the time (or all of it without that timer) waited out by yielding
void wait_until(std::chrono::steady_clock::time_point const Time)
{
#if defined(_WIN32)
#ifndef CREATE_WAITABLE_TIMER_HIGH_RESOLUTION
#define CREATE_WAITABLE_TIMER_HIGH_RESOLUTION 0x00000002
#endif
static thread_local HANDLE const timer{::CreateWaitableTimerExW(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS)};
auto const left{std::chrono::duration_cast<std::chrono::nanoseconds>(Time - std::chrono::steady_clock::now()).count()};
if ((timer != nullptr) && (left > 200000))
{
// in units of 100 ns, negative for a time from now; a little early, for the yields to end it on time
LARGE_INTEGER due;
due.QuadPart = -static_cast<LONGLONG>((left - 100000) / 100);
if (::SetWaitableTimerEx(timer, &due, 0, nullptr, nullptr, nullptr, 0))
{
::WaitForSingleObject(timer, INFINITE);
}
}
while (std::chrono::steady_clock::now() < Time)
{
std::this_thread::yield();
}
#else
std::this_thread::sleep_until(Time);
#endif
}
// the threads of the renderer (gfx.threads), the calling one with them: 0, the cores less two
std::size_t render_threads()
{
if (Global.gfx_threads > 0)
{
return static_cast<std::size_t>(Global.gfx_threads);
}
auto const cores{static_cast<std::size_t>(std::thread::hardware_concurrency())};
return (cores > 3 ? cores - 2 : 1);
}
} // namespace
bool vulkan14_renderer::context::create_frames(std::uint32_t const Count, std::string &Reason)
{
auto const handle{device.handle()};
VkSemaphoreTypeCreateInfo timelineinfo{};
timelineinfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO;
timelineinfo.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE;
timelineinfo.initialValue = 0;
VkSemaphoreCreateInfo semaphoreinfo{};
semaphoreinfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
semaphoreinfo.pNext = &timelineinfo;
if (!VK14_VERIFY(vkCreateSemaphore(handle, &semaphoreinfo, nullptr, &timeline)))
{
Reason = "no timeline semaphore";
return false;
}
instance.set_object_name(handle, VK_OBJECT_TYPE_SEMAPHORE, reinterpret_cast<std::uint64_t>(timeline), "frame timeline");
// the timeline the sleep of reflex signals
if (device.low_latency_enabled())
{
if (!VK14_VERIFY(vkCreateSemaphore(handle, &semaphoreinfo, nullptr, &reflex)))
{
reflex = VK_NULL_HANDLE;
}
else
{
instance.set_object_name(handle, VK_OBJECT_TYPE_SEMAPHORE, reinterpret_cast<std::uint64_t>(reflex), "reflex sleep");
}
}
frames.resize(Count);
for (std::uint32_t i = 0; i < Count; ++i)
{
VkCommandPoolCreateInfo poolinfo{};
poolinfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolinfo.flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT;
poolinfo.queueFamilyIndex = device.families().graphics;
if (!VK14_VERIFY(vkCreateCommandPool(handle, &poolinfo, nullptr, &frames[i].pool)))
{
Reason = "no command pool";
return false;
}
VkCommandBufferAllocateInfo bufferinfo{};
bufferinfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
bufferinfo.commandPool = frames[i].pool;
bufferinfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
bufferinfo.commandBufferCount = 1;
if (!VK14_VERIFY(vkAllocateCommandBuffers(handle, &bufferinfo, &frames[i].uploads)))
{
Reason = "no command buffer";
return false;
}
instance.set_object_name(handle, VK_OBJECT_TYPE_COMMAND_BUFFER, reinterpret_cast<std::uint64_t>(frames[i].uploads), "uploads " + std::to_string(i));
}
if (Global.gfx_vulkan_renderthread)
{
start_render_thread();
}
return true;
}
bool vulkan14_renderer::context::record(frame &Frame, bool const Uploads)
{
auto const handle{device.handle()};
auto const start{std::chrono::steady_clock::now()};
Frame.upload_stream.finish();
Frame.commands.finish();
VkCommandBufferBeginInfo begin{};
begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
if (Uploads)
{
VK14_VERIFY(vkBeginCommandBuffer(Frame.uploads, &begin));
Frame.upload_stream.replay(Frame.uploads);
VK14_VERIFY(vkEndCommandBuffer(Frame.uploads));
}
// the chunks split evenly between the workers, each with a command pool of its own (a pool is used by one thread at a time);
// which worker records a chunk doesn't matter: each chunk has a command buffer of its own, they go to the queue in their order
auto const chunks{Frame.commands.chunks()};
auto const threads{std::max<std::size_t>(1, std::min<std::size_t>(render_threads(), chunks))};
while (Frame.workers.size() < threads)
{
VkCommandPoolCreateInfo poolinfo{};
poolinfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolinfo.flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT;
poolinfo.queueFamilyIndex = device.families().graphics;
VkCommandPool pool{VK_NULL_HANDLE};
if (!VK14_VERIFY(vkCreateCommandPool(handle, &poolinfo, nullptr, &pool)))
{
return false;
}
Frame.workers.emplace_back().pool = pool;
}
Frame.chunks.assign(chunks, VK_NULL_HANDLE);
workers.run(threads, threads, [&](std::size_t const Worker) {
auto &commands{Frame.workers[Worker]};
for (auto chunk = chunks * Worker / threads; chunk < chunks * (Worker + 1) / threads; ++chunk)
{
if (commands.used == commands.buffers.size())
{
VkCommandBufferAllocateInfo bufferinfo{};
bufferinfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
bufferinfo.commandPool = commands.pool;
bufferinfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
bufferinfo.commandBufferCount = 1;
VkCommandBuffer buffer{VK_NULL_HANDLE};
if (!VK14_VERIFY(vkAllocateCommandBuffers(handle, &bufferinfo, &buffer)))
{
return;
}
commands.buffers.push_back(buffer);
}
auto const buffer{commands.buffers[commands.used++]};
VK14_VERIFY(vkBeginCommandBuffer(buffer, &begin));
Frame.commands.replay(chunk, buffer);
VK14_VERIFY(vkEndCommandBuffer(buffer));
Frame.chunks[chunk] = buffer;
}
});
record_threads = threads;
record_time = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
record_calls = Frame.commands.calls();
record_chunks = chunks;
return std::find(Frame.chunks.begin(), Frame.chunks.end(), VK_NULL_HANDLE) == Frame.chunks.end();
}
bool vulkan14_renderer::context::execute(render_job &Job, char const *&Failure)
{
if (false == submit(Job, Failure))
{
return false;
}
if ((false == Job.windows.empty()) && (false == present(Job.windows, Job.present_id)))
{
Failure = "presenting";
return false;
}
return true;
}
bool vulkan14_renderer::context::submit(render_job &Job, char const *&Failure)
{
auto &frame{*Job.target};
// reflex: the recording and the submit are of the frame with the id of its present
auto const *main{windows.empty() ? nullptr : windows.front().get()};
auto const lowlatency{(main != nullptr) && main->low_latency() && (Job.present_id > 0)};
if (lowlatency)
{
main->latency_marker(Job.present_id, VK_LATENCY_MARKER_RENDERSUBMIT_START_NV);
}
if (false == record(frame, Job.uploaded))
{
Failure = "recording a frame";
return false;
}
// the uploads first, in the same submit: their barriers make them done before the frame uses what they copied
std::vector<VkCommandBufferSubmitInfo> commands;
if (Job.uploaded)
{
VkCommandBufferSubmitInfo info{};
info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO;
info.commandBuffer = frame.uploads;
commands.push_back(info);
}
// the chunks of the frame in their order: a rendering suspended at the end of one is resumed at the start of the next
for (auto const chunk : frame.chunks)
{
VkCommandBufferSubmitInfo info{};
info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO;
info.commandBuffer = chunk;
commands.push_back(info);
}
VkSubmitInfo2 submit{};
submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2;
VkLatencySubmissionPresentIdNV latency{};
latency.sType = VK_STRUCTURE_TYPE_LATENCY_SUBMISSION_PRESENT_ID_NV;
latency.presentID = Job.present_id;
if (lowlatency)
{
submit.pNext = &latency;
}
submit.waitSemaphoreInfoCount = static_cast<std::uint32_t>(Job.waits.size());
submit.pWaitSemaphoreInfos = Job.waits.data();
submit.commandBufferInfoCount = static_cast<std::uint32_t>(commands.size());
submit.pCommandBufferInfos = commands.data();
submit.signalSemaphoreInfoCount = static_cast<std::uint32_t>(Job.signals.size());
submit.pSignalSemaphoreInfos = Job.signals.data();
if (!VK14_VERIFY(vkQueueSubmit2(device.graphics_queue(), 1, &submit, VK_NULL_HANDLE)))
{
Failure = "submitting a frame";
return false;
}
if (lowlatency)
{
main->latency_marker(Job.present_id, VK_LATENCY_MARKER_RENDERSUBMIT_END_NV);
}
return true;
}
bool vulkan14_renderer::context::present(std::vector<vk14::swapchain *> const &Windows, std::uint64_t const Id)
{
auto *main{windows.empty() ? nullptr : windows.front().get()};
auto const withmain{(main != nullptr) && (std::find(Windows.begin(), Windows.end(), main) != Windows.end())};
auto const generated{withmain ? main->generated_count() : 0u};
auto const lowlatency{withmain && main->low_latency() && (Id > 0)};
auto const ids{device.low_latency_enabled()};
// the frames generated by dlss go first, one after another, then the frame drawn: at equal times over the time of a frame, as
// measured between the frames the main thread handed over (the guide of the frame generation, 7), and done in time for the
// next one to be submitted. the main thread meanwhile goes on with the next frame up to its windows; without the render thread
// they go at once (vsync spaces them)
auto const start{std::chrono::steady_clock::now()};
auto const paced{(generated > 0) && render_threaded() && (present_interval > 0.0)};
auto spacing{0.0};
if (paced)
{
spacing = std::min(present_interval / static_cast<double>(generated + 1), std::max(0.0, 0.95 * present_interval - submit_time) / static_cast<double>(generated));
}
auto const step{std::chrono::duration_cast<std::chrono::steady_clock::duration>(std::chrono::duration<double>(spacing))};
for (std::uint32_t i = 0; i < generated; ++i)
{
if (paced && (i > 0))
{
wait_until(start + step * i);
}
auto const semaphore{main->generated_present_semaphore(i)};
auto const swapchain{main->handle()};
auto const image{main->generated_index(i)};
std::uint64_t const noid{0};
VkPresentIdKHR presentid{};
presentid.sType = VK_STRUCTURE_TYPE_PRESENT_ID_KHR;
presentid.swapchainCount = 1;
presentid.pPresentIds = &noid;
VkPresentInfoKHR info{};
info.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
info.pNext = (ids ? &presentid : nullptr);
info.waitSemaphoreCount = 1;
info.pWaitSemaphores = &semaphore;
info.swapchainCount = 1;
info.pSwapchains = &swapchain;
info.pImageIndices = &image;
auto const result{vkQueuePresentKHR(device.graphics_queue(), &info)};
main->presented(result);
if ((result == VK_ERROR_DEVICE_LOST) || (result == VK_ERROR_SURFACE_LOST_KHR))
{
vk14::log_error(std::string("vkQueuePresentKHR failed with ") + vk14::result_string(result));
return false;
}
}
if (paced)
{
wait_until(start + step * generated);
}
std::vector<VkSemaphore> waits;
std::vector<VkSwapchainKHR> swapchains;
std::vector<std::uint32_t> images;
std::vector<std::uint64_t> presentids;
std::vector<VkResult> results(Windows.size(), VK_SUCCESS);
for (auto *window : Windows)
{
waits.push_back(window->present_semaphore());
swapchains.push_back(window->handle());
images.push_back(window->image_index());
presentids.push_back(window == main ? Id : 0);
}
VkPresentIdKHR presentid{};
presentid.sType = VK_STRUCTURE_TYPE_PRESENT_ID_KHR;
presentid.swapchainCount = static_cast<std::uint32_t>(presentids.size());
presentid.pPresentIds = presentids.data();
VkPresentInfoKHR info{};
info.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
info.pNext = (ids ? &presentid : nullptr);
info.waitSemaphoreCount = static_cast<std::uint32_t>(waits.size());
info.pWaitSemaphores = waits.data();
info.swapchainCount = static_cast<std::uint32_t>(swapchains.size());
info.pSwapchains = swapchains.data();
info.pImageIndices = images.data();
info.pResults = results.data();
if (lowlatency)
{
main->latency_marker(Id, VK_LATENCY_MARKER_PRESENT_START_NV);
}
auto const result{vkQueuePresentKHR(device.graphics_queue(), &info)};
if (lowlatency)
{
main->latency_marker(Id, VK_LATENCY_MARKER_PRESENT_END_NV);
}
for (std::size_t i = 0; i < Windows.size(); ++i)
{
Windows[i]->presented(results[i]);
}
if ((result == VK_ERROR_DEVICE_LOST) || (result == VK_ERROR_SURFACE_LOST_KHR))
{
vk14::log_error(std::string("vkQueuePresentKHR failed with ") + vk14::result_string(result));
return false;
}
return true;
}
void vulkan14_renderer::context::start_render_thread()
{
render_stop = false;
render_thread = std::thread([this]() {
std::unique_lock<std::mutex> lock(render_mutex);
while (true)
{
render_wake.wait(lock, [this]() { return render_stop || render_queued; });
if (false == render_queued)
{
// stopped, with nothing left to do
break;
}
lock.unlock();
char const *failure{nullptr};
auto done{false};
try
{
// the frame submitted, the main thread may go on with the next one (wait_submitted()) while the windows are
// presented: with the frames generated by dlss the presents are spread over the time of a frame
auto const started{std::chrono::steady_clock::now()};
done = submit(render_pending, failure);
submit_time = std::chrono::duration<double>(std::chrono::steady_clock::now() - started).count();
auto const handed{render_pending.handed};
if (present_last != std::chrono::steady_clock::time_point{})
{
auto const period{std::chrono::duration<double>(handed - present_last).count()};
present_interval = ((period > 0.0) && (period < 0.25)) ? (present_interval > 0.0 ? 0.9 * present_interval + 0.1 * period : period) : 0.0;
}
present_last = handed;
{
std::lock_guard<std::mutex> submitted(render_mutex);
render_submitted = true;
}
render_idle.notify_all();
if (done && (false == render_pending.windows.empty()) && (false == present(render_pending.windows, render_pending.present_id)))
{
done = false;
failure = "presenting";
}
}
catch (std::exception const &Error)
{
vk14::log_error(std::string("the render thread failed: ") + Error.what());
failure = "recording a frame";
}
catch (...)
{
vk14::log_error("the render thread failed");
failure = "recording a frame";
}
lock.lock();
if ((false == done) && (render_failure == nullptr))
{
render_failure = (failure != nullptr ? failure : "recording a frame");
}
render_queued = false;
render_idle.notify_all();
}
});
}
void vulkan14_renderer::context::stop_render_thread()
{
if (false == render_thread.joinable())
{
return;
}
{
std::lock_guard<std::mutex> lock(render_mutex);
render_stop = true;
}
render_wake.notify_all();
render_thread.join();
}
void vulkan14_renderer::context::hand_over(render_job &&Job)
{
{
std::unique_lock<std::mutex> lock(render_mutex);
render_idle.wait(lock, [this]() { return false == render_queued; });
render_pending = std::move(Job);
render_pending.handed = std::chrono::steady_clock::now();
render_queued = true;
render_submitted = false;
}
render_wake.notify_one();
}
void vulkan14_renderer::context::wait_submitted()
{
if (false == render_threaded())
{
return;
}
auto const start{std::chrono::steady_clock::now()};
std::unique_lock<std::mutex> lock(render_mutex);
render_idle.wait(lock, [this]() { return (false == render_queued) || render_submitted; });
render_wait = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
}
char const *vulkan14_renderer::context::drain()
{
if (false == render_threaded())
{
return nullptr;
}
auto const start{std::chrono::steady_clock::now()};
std::unique_lock<std::mutex> lock(render_mutex);
render_idle.wait(lock, [this]() { return false == render_queued; });
render_wait = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
auto const failure{render_failure};
render_failure = nullptr;
return failure;
}
// the parts of the renderer made once the device is there
bool vulkan14_renderer::context::create_core(std::uint32_t const Frames, std::string &Reason)
{
if (false == memory.create(instance, device, Reason))
{
return false;
}
uploads.init(memory, deletions, device.physical());
if (false == sampling.create(device, static_cast<float>(Global.AnisotropicFiltering), Reason, lod_bias))
{
return false;
}
auto const &limits{device.caps().limits};
(void)limits;
if (false == ring.create(memory, ring_size, Frames, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, "frame ring", ring_tail))
{
Reason = "no ring for the data of the frames";
return false;
}
if ((false == shaders.init(vk14::binding_preamble(device.raytracing_enabled()), Reason)) || (false == pipelines.init(device, Reason)) || (false == passes.create(device, Frames, Reason)))
{
return false;
}
resources.gpu = &device;
resources.memory = &memory;
resources.uploads = &uploads;
resources.deletions = &deletions;
resources.sampling = &sampling;
resources.images = &images;
resources.thread = std::this_thread::get_id();
resources.pending = &pending;
resources.raytracing = device.raytracing_enabled();
textures = std::make_unique<texture_bank>([this]() -> texture_source * { return new vk14::vulkan14_texture(resources); });
return imgui.create(resources, shaders, pipelines, ring, Reason);
}
// the value of the timeline the gpu reached, 0 when it can't be read
std::uint64_t vulkan14_renderer::context::completed() const
{
std::uint64_t value{0};
if ((timeline == VK_NULL_HANDLE) || (vkGetSemaphoreCounterValue(device.handle(), timeline, &value) != VK_SUCCESS))
{
return 0;
}
return value;
}
bool vulkan14_renderer::context::wait(std::uint64_t const Value) const
{
if ((Value == 0) || (completed() >= Value))
{
return true;
}
VkSemaphoreWaitInfo wait{};
wait.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO;
wait.semaphoreCount = 1;
wait.pSemaphores = &timeline;
wait.pValues = &Value;
return VK14_VERIFY(vkWaitSemaphores(device.handle(), &wait, UINT64_MAX));
}
void vulkan14_renderer::context::destroy()
{
auto const handle{device.handle()};
// the frame given to the render thread goes to the queue first
stop_render_thread();
if (handle != VK_NULL_HANDLE)
{
// the end of the renderer, not a frame: everything on the gpu has to be done
vkDeviceWaitIdle(handle);
}
acquired.clear();
windows.clear();
imgui.destroy();
// the textures give their images to the deletion queue, which goes now that the gpu is idle
textures.reset();
uploads.end_frame();
uploads = vk14::uploader{};
images.clear();
deletions.flush();
// the features of dlss released by the deletions above, ngx goes before the device
dlss.shutdown();
pipelines.shutdown();
shaders.shutdown();
passes.destroy();
ring.destroy();
if (handle != VK_NULL_HANDLE)
{
sampling.destroy(handle);
for (auto &frame : frames)
{
if (frame.pool != VK_NULL_HANDLE)
{
vkDestroyCommandPool(handle, frame.pool, nullptr);
}
for (auto &worker : frame.workers)
{
vkDestroyCommandPool(handle, worker.pool, nullptr);
}
}
if (timeline != VK_NULL_HANDLE)
{
vkDestroySemaphore(handle, timeline, nullptr);
}
if (reflex != VK_NULL_HANDLE)
{
vkDestroySemaphore(handle, reflex, nullptr);
}
}
memory.destroy();
frames.clear();
timeline = VK_NULL_HANDLE;
reflex = VK_NULL_HANDLE;
device.destroy();
instance.destroy();
}
namespace
{
// whether the renderer can run on the machine, before there is a window: the loader, an instance of vulkan 1.4 and a gpu
// with everything the renderer needs
bool probe(std::string &Reason)
{
vk14::instance instance;
if (false == instance.create(vk14::instance_config{}, Reason))
{
return false;
}
vk14::device device;
if (false == device.select(instance.handle(), VK_NULL_HANDLE, Global.gfx_vulkan_device, true, Reason))
{
return false;
}
WriteLog(vk14::capability_report(device.caps()));
return true;
}
void window_hints()
{
// the swapchain shows the images; a window with an opengl context couldn't be given to vulkan
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
}
void window_failed(std::string const &Reason)
{
vk14::log_error("the window couldn't be made (" + Reason + ")");
}
} // namespace
vulkan14_renderer::vulkan14_renderer() = default;
vulkan14_renderer::~vulkan14_renderer()
{
Shutdown();
}
std::unique_ptr<gfx_renderer> vulkan14_renderer::create_func()
{
return std::make_unique<vulkan14_renderer>();
}
gfx_backend_desc vulkan14_renderer::backend_desc()
{
gfx_backend_desc desc;
desc.name = "vulkan 1.4";
desc.create = &vulkan14_renderer::create_func;
desc.probe = &probe;
desc.window_hints = &window_hints;
desc.window_failed = &window_failed;
desc.gl_context = false;
return desc;
}
bool vulkan14_renderer::renderer_register = gfx_renderer_factory::get_instance()->register_backend("vulkan", vulkan14_renderer::backend_desc());
// dlss on the device made for it, and the size the scene of the main window is drawn at; where it can't be had, taa instead
void vulkan14_renderer::start_dlss()
{
auto &ctx{*m_context};
std::string reason;
glm::ivec2 render{0};
auto const output{glm::max(Global.fb_size, glm::ivec2(1))};
if (ctx.device.caps().vendor_id != nvidia_vendor_id)
{
reason = ctx.device.caps().device_name + " isn't an nvidia gpu";
}
else if (ctx.dlss.init(ctx.instance.handle(), ctx.device.physical(), ctx.device.handle(), reason))
{
if (ctx.dlss.plan(output, Global.gfx_dlss_native ? 1.f : Global.gfx_buffer_scale, render))
{
m_dlssrender = render;
m_dlssoutput = output;
ctx.lod_bias = ctx.dlss.lod_bias();
vk14::log_info("dlss: the bias of the mipmaps of the scene " + std::to_string(ctx.lod_bias) + ", " + std::to_string(ctx.dlss.jitter_phases()) + " offsets of the jitter");
// the frame generation, as far as the gpu goes (x3 and x4 from the rtx 50 series on)
if (Global.gfx_framegeneration > 1)
{
auto const wanted{static_cast<std::uint32_t>(Global.gfx_framegeneration - 1)};
m_generated = std::min(wanted, ctx.dlss.generation_max());
if ((m_generated > 0) && (m_generated < wanted))
{
vk14::log_info("gfx.framegeneration x" + std::to_string(wanted + 1) + ": the gpu goes up to x" + std::to_string(m_generated + 1) + ", using that");
}
if ((m_generated > 0) && (false == Global.gfx_vulkan_renderthread))
{
vk14::log_info("gfx.framegeneration: without the render thread (gfx.vulkan.renderthread no) the frames generated are shown at once, spaced only by vsync");
}
}
return;
}
reason = "it doesn't upscale to the window of " + std::to_string(output.x) + "x" + std::to_string(output.y);
}
ctx.dlss.shutdown();
vk14::log_info("gfx.antialiasing dlss: " + reason + ", using taa");
Global.gfx_antialiasing = global_settings::antialiasing_mode::taa;
}
bool vulkan14_renderer::Init(GLFWwindow *Window)
{
m_context = std::make_unique<context>();
auto &ctx{*m_context};
std::string reason;
vk14::instance_config config;
config.validation = Global.gfx_vulkan_validation;
config.debug_utils = (Global.gfx_vulkan_validation > 0);
// dlss (gfx.antialiasing dlss): the extensions ngx needs go into the instance and the device; where it can't be had, taa
if (Global.gfx_dlss() && (false == vk14::dlss::compiled()))
{
vk14::log_info("gfx.antialiasing dlss: this build is without the dlss sdk (WITH_DLSS), using taa");
Global.gfx_antialiasing = global_settings::antialiasing_mode::taa;
}
if (Global.gfx_dlss())
{
vk14::dlss::instance_extensions(config.wanted_extensions);
}
else if (Global.gfx_framegeneration > 1)
{
vk14::log_info("gfx.framegeneration: it goes with gfx.antialiasing dlss or dlaa, the frames aren't generated");
}
auto const framesinflight{static_cast<std::uint32_t>(std::clamp(Global.gfx_vulkan_framesinflight, 2, 3))};
auto ok{ctx.instance.create(config, reason) && ctx.device.select(ctx.instance.handle(), VK_NULL_HANDLE, Global.gfx_vulkan_device, false, reason)};
std::vector<std::string> deviceextensions;
if (ok && Global.gfx_dlss() && (ctx.device.caps().vendor_id == nvidia_vendor_id))
{
vk14::dlss::device_extensions(ctx.instance.handle(), ctx.device.physical(), deviceextensions);
}
ok = ok && ctx.device.create(ctx.instance, reason, deviceextensions);
if (ok && Global.gfx_dlss())
{
start_dlss();
}
ok = ok && ctx.create_frames(framesinflight, reason) && ctx.create_core(framesinflight, reason);
if (ok)
{
auto mainwindow{std::make_unique<vk14::swapchain>()};
// the frame generation needs an image of the swapchain for each frame it makes, and reflex with it (the guide of the frame
// generation; the latency the frames shown later add is taken back by it)
mainwindow->set_generated(m_generated);
mainwindow->set_low_latency(std::max(Global.gfx_reflex, m_generated > 0 ? 1 : 0));
ok = mainwindow->create(ctx.instance, ctx.device, Window, "main window", framesinflight, reason);
if (ok && (Global.gfx_reflex > 0) && ctx.device.low_latency_enabled() && (false == mainwindow->low_latency()) && (mainwindow->handle() != VK_NULL_HANDLE))
{
vk14::log_info("gfx.reflex: the driver didn't turn it on for the main window");
}
ctx.windows.emplace_back(std::move(mainwindow));
}
if (false == ok)
{
vk14::log_error("the renderer can't start: " + reason);
m_context.reset();
return false;
}
if (false == init_scene(reason))
{
vk14::log_error("the renderer can't start: " + reason);
destroy_scene();
m_context.reset();
return false;
}
init_frame_data();
vk14::log_info(std::to_string(framesinflight) + " frames in flight");
// until the stages of the plan bring them, these parts of the simulator aren't there with vulkan
vk14::log_info("not available with vulkan yet: the map panel, python screens in their own windows (N5), panels in their own windows (R5b), VR (X4)");
// the settings of eu07.ini which only the opengl renderer takes; everything else of gfx.* is taken here as there
vk14::log_info("settings of the opengl renderer without effect with vulkan: compresstex (the textures go to the gpu as the files give them), gfx.depthprepass.replay (the scene "
"is walked again for its depth), gfx.bufferstreaming, gfx.geometry.shared, gfx.format.color, gfx.format.depth, gfx.postfx.motionblur.format (the formats of the "
"targets are fixed), gfx.gldebug (gfx.vulkan.validation instead)");
WriteLog("Gfx Renderer: setup complete");
update_info();
return true;
}
bool vulkan14_renderer::AddViewport(global_settings::extraviewport_config const &conf)
{
if (!m_context || m_viewports.empty())
{
return false;
}
viewport_config *viewport{nullptr};
if (conf.monitor == "MAIN")
{
// the main window drawn with the projection of the viewport
viewport = &m_viewports.front();
}
else
{
// a window of its own, without an opengl context, with a swapchain of its own
auto *window{Application.window(-1, true, conf.width, conf.height, Application.find_monitor(conf.monitor), true, false)};
if (window == nullptr)
{
vk14::log_error("the window of the extra viewport on " + conf.monitor + " couldn't be made");
return false;
}
auto &ctx{*m_context};
// the windows the render thread presents are left as they are until it's done
if (auto const *failure{ctx.drain()})
{
report_device_lost(failure);
return false;
}
auto swapchain{std::make_unique<vk14::swapchain>()};
std::string reason;
if (false == swapchain->create(ctx.instance, ctx.device, window, "viewport " + conf.monitor, static_cast<std::uint32_t>(ctx.frames.size()), reason))
{
vk14::log_error(reason);
return false;
}
ctx.windows.emplace_back(std::move(swapchain));
m_viewports.emplace_back();
viewport = &m_viewports.back();
viewport->window = ctx.windows.size() - 1;
}
if (viewport->main && (viewport->output_width > 0))
{
// the size dlss was planned for isn't the one of the viewport: the main window without it
vk14::log_info("dlss: off for the main window, drawn by the viewport " + conf.monitor + " at a size of its own");
viewport->output_width = 0;
viewport->output_height = 0;
}
viewport->width = conf.width;
viewport->height = conf.height;
viewport->projection = conf.projection;
viewport->proj_type = viewport_config::custom;
viewport->draw_range = conf.draw_range;
vk14::log_info("viewport " + conf.monitor + ": " + std::to_string(conf.width) + "x" + std::to_string(conf.height));
return true;
}
void vulkan14_renderer::Shutdown()
{
if (m_context && (m_context->device.handle() != VK_NULL_HANDLE))
{
// the end of the renderer, not a frame: everything on the gpu has to be done, the frame of the render thread with it
m_context->stop_render_thread();
vkDeviceWaitIdle(m_context->device.handle());
}
// the geometry on the gpu goes with the context; the data of the banks stays for the simulation
for (auto *bank : m_banks)
{
if (bank != nullptr)
{
bank->release();
}
}
destroy_scene();
m_draw.reset();
m_context.reset();
}
void vulkan14_renderer::report_device_lost(char const *Where)
{
auto &ctx{*m_context};
if (ctx.devicelost)
{
return;
}
ctx.devicelost = true;
auto const done{ctx.completed()};
std::string report{std::string("the device was lost (") + Where + "); the gpu was done with the timeline value " + std::to_string(done) + " of " + std::to_string(ctx.timelinevalue)};
for (auto const &frame : ctx.frames)
{
if ((frame.done > done) && (frame.done <= ctx.timelinevalue))
{
report += "; frame " + std::to_string(frame.number) + " (value " + std::to_string(frame.done) + ") wasn't done, its passes: " + ctx.passes.passes_of(frame.number);
}
}
vk14::log_error(report);
crashreport_add_info("vulkan_device_lost", report);
}
void vulkan14_renderer::SwapBuffers()
{
if (!m_context)
{
return;
}
auto &ctx{*m_context};
Timer::subsystem.gfx_swap.start();
// with the render thread the windows are presented by it (ctx.acquired was handed over to it)
if (false == ctx.acquired.empty())
{
if (false == ctx.present(ctx.acquired, ctx.framecount + 1))
{
report_device_lost("presenting");
}
ctx.acquired.clear();
}
++ctx.framecount;
Timer::subsystem.gfx_swap.stop();
}
void vulkan14_renderer::Frame_Start()
{
if (!m_context || m_context->devicelost || m_context->windows.empty() || (m_context->reflex == VK_NULL_HANDLE))
{
return;
}
auto &ctx{*m_context};
auto const &main{*ctx.windows.front()};
if (false == main.low_latency())
{
return;
}
// the driver says how long to wait for the gpu before the input and the simulation of the frame, once between two presents. the
// semaphore is waited for at most a while: a window without a swapchain (minimized) has nothing for the driver to measure
if (main.latency_sleep(ctx.reflex, ctx.reflexvalue + 1))
{
++ctx.reflexvalue;
VkSemaphoreWaitInfo wait{};
wait.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO;
wait.semaphoreCount = 1;
wait.pSemaphores = &ctx.reflex;
wait.pValues = &ctx.reflexvalue;
vkWaitSemaphores(ctx.device.handle(), &wait, 100000000ull);
}
main.latency_marker(ctx.framecount + 1, VK_LATENCY_MARKER_SIMULATION_START_NV);
main.latency_marker(ctx.framecount + 1, VK_LATENCY_MARKER_INPUT_SAMPLE_NV);
}
float vulkan14_renderer::Framerate()
{
return m_framerate;
}
// maintenance
void vulkan14_renderer::Update(double const Deltatime)
{
Update_Pick_Control();
Update_Pick_Node();
update_screenshot();
m_updateaccumulator += Deltatime;
if (m_updateaccumulator < 1.0)
{
// too early for any work
return;
}
m_updateaccumulator = 0.0;
m_framerate = 1000.f / Timer::subsystem.mainloop_total.average();
// adjust draw ranges etc, based on recent performance (opengl46_renderer::Update())
if (Global.targetfps != 0.0f)
{
float fps_diff = Global.targetfps - m_framerate;
if (fps_diff > 0.0f)
Global.fDistanceFactor = std::max(0.5f, Global.fDistanceFactor - 0.05f);
else
Global.fDistanceFactor = std::min(Global.gfx_distance_factor_max, Global.fDistanceFactor + 0.05f);
}
// legacy framerate parameters
else if (Global.fFpsAverage != 0.f)
{
auto const fps_diff = m_framerate - Global.fFpsAverage;
if (fps_diff < -Global.fFpsDeviation)
{
Global.fDistanceFactor = std::min(std::max(1.0f, Global.fDistanceFactor - 0.05f), Global.gfx_distance_factor_max);
}
else if (fps_diff > Global.fFpsDeviation)
{
Global.fDistanceFactor = std::min(std::min(3.0f, Global.fDistanceFactor + 0.05f), Global.gfx_distance_factor_max);
}
}
if (Global.UpdateMaterials)
{
// update resources if there was environmental change
simulation_state simulationstate{Global.Weather, Global.Season};
std::swap(m_simulationstate, simulationstate);
if ((m_simulationstate.season != simulationstate.season) && (false == simulationstate.season.empty()))
{
m_materials.on_season_change();
}
if ((m_simulationstate.weather != simulationstate.weather) && (false == simulationstate.weather.empty()))
{
m_materials.on_weather_change();
}
if (((m_simulationstate.season != simulationstate.season) && (false == simulationstate.season.empty())) ||
((m_simulationstate.weather != simulationstate.weather) && (false == simulationstate.weather.empty())))
{
// the heightmap terrain keeps copies of the textures of its layer materials
if (m_context && m_context->scene.terrain)
{
m_context->scene.terrain->on_environment_change();
}
// the seasonal variants of the models are shown and hidden anew: the walks kept by the gpu culling go
m_walkrecords.clear();
}
}
if (m_context && m_context->textures && (true == Global.ResourceSweep) && (true == simulation::is_ready))
{
// garbage collection
m_geometry.update();
m_context->textures->update();
}
if ((Global.gfx_texture_releasedistance > 0.f) && (true == simulation::is_ready))
{
update_texture_release();
}
update_info();
}
void vulkan14_renderer::update_info()
{
m_infotimes.clear();
m_infostats.clear();
if (!m_context)
{
return;
}
auto const &ctx{*m_context};
auto const &caps{ctx.device.caps()};
m_infotimes = "vulkan " + vk14::version_string(caps.api_version) + ": " + caps.device_name + " (" + caps.driver_name + ")\n" + "frames: " + std::to_string(ctx.framecount) + ", in flight " + std::to_string(ctx.frames.size()) + "\n" +
"mainloop total: " + to_string(Timer::subsystem.mainloop_total.average(), 2) + " ms, frame cpu: " + to_string(Timer::subsystem.gfx_total.average(), 2) + " ms, ui: " + to_string(Timer::subsystem.gfx_gui.average(), 2) +
" ms, present: " + to_string(Timer::subsystem.gfx_swap.average(), 2) + " ms\n" + "command buffers recorded: " + to_string(ctx.record_time, 2) + " ms, " + std::to_string(ctx.record_calls) + " calls in " +
std::to_string(ctx.record_chunks) + " chunks, " + std::to_string(ctx.record_threads) + (ctx.record_threads == 1 ? " thread" : " threads") +
(ctx.render_threaded() ? ", on the render thread; waited for it: " + to_string(ctx.render_wait, 2) + " ms\n" : "\n") + "gpu frame parts:\n" + ctx.passes.info();
VkDeviceSize usage{0}, budget{0};
ctx.memory.budget(usage, budget);
m_infostats = "device memory: " + vk14::memory_size_string(usage) + " of " + vk14::memory_size_string(budget) + (ctx.device.memory_budget_enabled() ? "" : " (no budget from the driver)") + "\n" + "images: " + std::to_string(ctx.images.size()) +
", uploaded " + vk14::memory_size_string(ctx.uploads.uploaded()) + ", waiting for the gpu to be deleted: " + std::to_string(ctx.deletions.size()) + "\n" + (ctx.textures ? ctx.textures->info() + "\n" : std::string{}) + ctx.shaders.info() +
"\n" + ctx.pipelines.info() + "\n" + "frame ring: " + vk14::memory_size_string(ctx.ring.used()) + " of " + vk14::memory_size_string(ctx.ring.capacity()) + "\n";
for (auto const &window : ctx.windows)
{
auto const extent{window->extent()};
m_infostats += "window: " + std::to_string(extent.width) + "x" + std::to_string(extent.height) + (window->srgb() ? ", srgb" : ", unorm") + (window->present_mode() == VK_PRESENT_MODE_FIFO_KHR ? ", vsync" : ", no vsync") + "\n";
}
if (false == m_viewports.empty())
{
auto const &main{m_viewports.front()};
m_infostats += "scene drawn at " + std::to_string(main.width) + "x" + std::to_string(main.height) + (dlss_active() ? ", " + ctx.dlss.info() : std::string{}) + "\n";
}
// the statistics of the scene and of the cullings, as the opengl renderer gives them
collect_cluster_stats();
m_infostats += "\n" + scene_info() + cull_info();
}
bool vulkan14_renderer::Debug_Ui_State(std::optional<bool> State)
{
if (State)
{
m_debug_ui_active = *State;
}
return m_debug_ui_active;
}
std::string const &vulkan14_renderer::info_times() const
{
return m_infotimes;
}
std::string const &vulkan14_renderer::info_stats() const
{
return m_infostats;
}
imgui_renderer *vulkan14_renderer::GetImguiRenderer()
{
return (m_context ? &m_context->imgui : nullptr);
}