/* 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(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((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(Global.gfx_threads); } auto const cores{static_cast(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(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(reflex), "reflex sleep"); } } // the async compute: a family of its own for the passes of the sky and the weather, the frames it's done with counted apart async_compute = Global.gfx_vulkan_asynccompute && device.families().dedicated_compute && (device.families().compute != vk14::queue_families::invalid) && (device.compute_queue() != VK_NULL_HANDLE); if (async_compute) { if (!VK14_VERIFY(vkCreateSemaphore(handle, &semaphoreinfo, nullptr, &compute_timeline))) { compute_timeline = VK_NULL_HANDLE; async_compute = false; } else { instance.set_object_name(handle, VK_OBJECT_TYPE_SEMAPHORE, reinterpret_cast(compute_timeline), "compute timeline"); } } 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(frames[i].uploads), "uploads " + std::to_string(i)); frames[i].compute_stream.set_compute_queue(true); if (async_compute) { poolinfo.queueFamilyIndex = device.families().compute; bufferinfo.commandPool = VK_NULL_HANDLE; if (VK14_VERIFY(vkCreateCommandPool(handle, &poolinfo, nullptr, &frames[i].compute_pool))) { bufferinfo.commandPool = frames[i].compute_pool; } if ((bufferinfo.commandPool == VK_NULL_HANDLE) || !VK14_VERIFY(vkAllocateCommandBuffers(handle, &bufferinfo, &frames[i].compute_buffer))) { Reason = "no command buffer of the compute queue"; return false; } instance.set_object_name(handle, VK_OBJECT_TYPE_COMMAND_BUFFER, reinterpret_cast(frames[i].compute_buffer), "async compute " + std::to_string(i)); } } if (Global.gfx_vulkan_renderthread) { start_render_thread(); } return true; } bool vulkan14_renderer::context::record(frame &Frame, bool const Uploads, bool const Compute) { 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)); } if (Compute) { Frame.compute_stream.finish(); VK14_VERIFY(vkBeginCommandBuffer(Frame.compute_buffer, &begin)); Frame.compute_stream.replay(Frame.compute_buffer); VK14_VERIFY(vkEndCommandBuffer(Frame.compute_buffer)); } // 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(1, std::min(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(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, Job.async)) { Failure = "recording a frame"; return false; } if (Job.async) { // the passes of the compute queue, once the graphics queue is done with the frame before, which read what they write VkSemaphoreSubmitInfo wait{}; wait.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO; wait.semaphore = timeline; wait.value = Job.value - 1; wait.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; VkSemaphoreSubmitInfo signal{}; signal.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO; signal.semaphore = compute_timeline; signal.value = Job.value; signal.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; VkCommandBufferSubmitInfo buffer{}; buffer.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO; buffer.commandBuffer = frame.compute_buffer; VkSubmitInfo2 compute{}; compute.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2; compute.waitSemaphoreInfoCount = (Job.value > 1 ? 1 : 0); compute.pWaitSemaphoreInfos = &wait; compute.commandBufferInfoCount = 1; compute.pCommandBufferInfos = &buffer; compute.signalSemaphoreInfoCount = 1; compute.pSignalSemaphoreInfos = &signal; if (!VK14_VERIFY(vkQueueSubmit2(device.compute_queue(), 1, &compute, VK_NULL_HANDLE))) { Failure = "submitting the async compute of 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 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); } VkLatencySubmissionPresentIdNV latency{}; latency.sType = VK_STRUCTURE_TYPE_LATENCY_SUBMISSION_PRESENT_ID_NV; latency.presentID = Job.present_id; // with the async compute in two submits: the part before the split goes on at the same time as the compute queue, the rest waits // for it. the waits for the images of the windows and the signals go with the rest, which draws them std::array submits{}; std::uint32_t count{0}; auto first{commands.size()}; std::vector waits; if (Job.async) { first = (Job.uploaded ? 1 : 0) + std::min(frame.commands.split_chunk(), frame.chunks.size()); auto &before{submits[count++]}; before.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2; before.commandBufferInfoCount = static_cast(first); before.pCommandBufferInfos = commands.data(); VkSemaphoreSubmitInfo compute{}; compute.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO; compute.semaphore = compute_timeline; compute.value = Job.value; compute.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; waits.push_back(compute); } waits.insert(waits.end(), Job.waits.begin(), Job.waits.end()); auto &submit{submits[count++]}; submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2; if (lowlatency) { submit.pNext = &latency; } submit.waitSemaphoreInfoCount = static_cast(waits.size()); submit.pWaitSemaphoreInfos = waits.data(); submit.commandBufferInfoCount = static_cast(commands.size() - (Job.async ? first : 0)); submit.pCommandBufferInfos = commands.data() + (Job.async ? first : 0); submit.signalSemaphoreInfoCount = static_cast(Job.signals.size()); submit.pSignalSemaphoreInfos = Job.signals.data(); if (!VK14_VERIFY(vkQueueSubmit2(device.graphics_queue(), count, submits.data(), 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 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(generated + 1), std::max(0.0, 0.95 * present_interval - submit_time) / static_cast(generated)); } auto const step{std::chrono::duration_cast(std::chrono::duration(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 = ℑ 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 waits; std::vector swapchains; std::vector images; std::vector presentids; std::vector 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(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(waits.size()); info.pWaitSemaphores = waits.data(); info.swapchainCount = static_cast(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 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(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(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 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 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 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 lock(render_mutex); render_idle.wait(lock, [this]() { return (false == render_queued) || render_submitted; }); render_wait = std::chrono::duration(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 lock(render_mutex); render_idle.wait(lock, [this]() { return false == render_queued; }); render_wait = std::chrono::duration(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; } if (async_compute) { // what the passes of the compute queue read and write is shared by the two families memory.share_between(device.families().graphics, device.families().compute); } uploads.init(memory, deletions, device.physical()); // the textures of the background threads filled by the host, where the device can do it to the layout they're sampled in uploads.enable_host_copy(device.caps().host_image_copy && device.caps().host_copy_sampled); if (false == sampling.create(device, static_cast(Global.AnisotropicFiltering), Reason, lod_bias)) { return false; } auto const &limits{device.caps().limits}; (void)limits; // the blocks of the frame are read by the passes of the compute queue too (the async compute) auto const ringusage{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}; if (false == ring.create(memory, ring_size, Frames, ringusage, "frame ring", ring_tail, true)) { 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.background = (Global.gfx_vulkan_asyncresources ? &background : nullptr); resources.raytracing = device.raytracing_enabled(); textures = std::make_unique([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; } lastcompleted = value; 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 background threads use what goes below background.stop(); // 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.reset(); 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 (frame.compute_pool != VK_NULL_HANDLE) { vkDestroyCommandPool(handle, frame.compute_pool, nullptr); } } if (timeline != VK_NULL_HANDLE) { vkDestroySemaphore(handle, timeline, nullptr); } if (reflex != VK_NULL_HANDLE) { vkDestroySemaphore(handle, reflex, nullptr); } if (compute_timeline != VK_NULL_HANDLE) { vkDestroySemaphore(handle, compute_timeline, nullptr); } } memory.destroy(); frames.clear(); timeline = VK_NULL_HANDLE; reflex = VK_NULL_HANDLE; compute_timeline = VK_NULL_HANDLE; async_compute = false; 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 vulkan14_renderer::create_func() { return std::make_unique(); } 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(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_contextstore = std::make_unique(); m_context = m_contextstore.get(); 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::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 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()}; // 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_contextstore.reset(); m_context = nullptr; return false; } if (false == init_scene(reason)) { vk14::log_error("the renderer can't start: " + reason); destroy_scene(); m_contextstore.reset(); m_context = nullptr; 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()}; std::string reason; if (false == swapchain->create(ctx.instance, ctx.device, window, "viewport " + conf.monitor, static_cast(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_main != nullptr) { // a walker has nothing of its own to let go of return; } if (m_context) { join_walks(); } 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_walkers.clear(); m_walkstreams.clear(); m_draw.reset(); m_contextstore.reset(); m_context = nullptr; } void vulkan14_renderer::report_device_lost(char const *Where) { auto &ctx{*m_context}; if (ctx.devicelost) { return; } ctx.devicelost = true; // the timeline can't be read once the device is lost: the value read before then auto const done{std::max(ctx.completed(), ctx.lastcompleted.load())}; 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); } } // where the gpu got to (the pass it hung or failed in is after the one done last, up to the one taken in last) if (ctx.device.checkpoints_enabled()) { report += "; the checkpoints of the graphics queue: " + ctx.passes.checkpoints(ctx.device.graphics_queue()); if (ctx.async_compute && (ctx.device.compute_queue() != ctx.device.graphics_queue())) { report += "; of the compute queue: " + ctx.passes.checkpoints(ctx.device.compute_queue()); } } if (auto const fault{ctx.device.fault_report()}; false == fault.empty()) { report += "; " + fault; } 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" + background_info() + "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(); } std::string vulkan14_renderer::background_info() const { auto const &ctx{*m_context}; std::string info; if (ctx.background.running()) { auto const pending{ctx.pending.made_count.load()}; info += "background threads: " + std::to_string(ctx.background.pending()) + " tasks waiting; pipelines made ahead: " + std::to_string(ctx.warm_made.load()) + ", made late: " + std::to_string(ctx.pipelines_late.load()) + "; textures made: " + std::to_string(pending) + ", draws without theirs: " + std::to_string(ctx.pending.late_count.load()) + "\n"; } else { info += "background threads: off (gfx.vulkan.asyncresources)\n"; } if (ctx.walks.running()) { info += "walkers: " + std::to_string(m_walkpasses) + " passes in the last frame, " + std::to_string(vk14::walk_threads()) + (vk14::walk_threads() == 1 ? " thread" : " threads") + ", the main thread waited for them: " + to_string(m_walkwait, 2) + " ms\n"; } else { info += "walkers: off (gfx.vulkan.parallelpasses)\n"; } if (ctx.models.running()) { info += "helpers of the walks: " + std::to_string(m_helpedgroups) + " groups of the cullings walked by them in the last frame\n"; } else { info += "helpers of the walks: off (gfx.vulkan.parallelmodels)\n"; } if (ctx.async_compute) { info += std::string("async compute: the sky and the weather on the compute queue (family ") + std::to_string(ctx.device.families().compute) + "), " + (m_asyncframe ? "in the last frame" : "not in the last frame") + "\n"; } else { info += (Global.gfx_vulkan_asynccompute ? "async compute: off (no compute family of its own)\n" : "async compute: off (gfx.vulkan.asynccompute)\n"); } return info; } bool vulkan14_renderer::Debug_Ui_State(std::optional 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); }