17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-09 10:12:24 +01:00

Add DLSS Frame Generation (gfx.framegeneration x2/x3/x4) and Reflex (gfx.reflex) to the Vulkan renderer

This commit is contained in:
2026-10-25 20:52:12 +01:00
parent 1841e6d7b0
commit a6bdeeaa2b
23 changed files with 1474 additions and 85 deletions

View File

@@ -40,6 +40,36 @@ 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()
{
@@ -70,6 +100,18 @@ bool vulkan14_renderer::context::create_frames(std::uint32_t const Count, std::s
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)
@@ -168,8 +210,29 @@ bool vulkan14_renderer::context::record(frame &Frame, bool const Uploads)
}
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";
@@ -194,6 +257,13 @@ bool vulkan14_renderer::context::execute(render_job &Job, char const *&Failure)
}
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());
@@ -205,35 +275,101 @@ bool vulkan14_renderer::context::execute(render_job &Job, char const *&Failure)
Failure = "submitting a frame";
return false;
}
if ((false == Job.windows.empty()) && (false == present(Job.windows)))
if (lowlatency)
{
Failure = "presenting";
return false;
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)
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]);
@@ -264,7 +400,28 @@ void vulkan14_renderer::context::start_render_thread()
auto done{false};
try
{
done = execute(render_pending, failure);
// 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)
{
@@ -307,11 +464,25 @@ 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())
@@ -431,10 +602,15 @@ void vulkan14_renderer::context::destroy()
{
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();
}
@@ -518,6 +694,20 @@ void vulkan14_renderer::start_dlss()
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);
@@ -547,6 +737,10 @@ bool vulkan14_renderer::Init(GLFWwindow *Window)
{
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)};
@@ -564,7 +758,15 @@ bool vulkan14_renderer::Init(GLFWwindow *Window)
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)
@@ -705,7 +907,7 @@ void vulkan14_renderer::SwapBuffers()
// 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))
if (false == ctx.present(ctx.acquired, ctx.framecount + 1))
{
report_device_lost("presenting");
}
@@ -716,6 +918,34 @@ void vulkan14_renderer::SwapBuffers()
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;