From ef479da2dcb51cb35559360291870bc7c744ba2e Mon Sep 17 00:00:00 2001 From: ns6089 <61738816+ns6089@users.noreply.github.com> Date: Fri, 12 May 2023 16:46:05 +0300 Subject: [PATCH 1/8] Remove DwmFlush() --- docs/source/about/advanced_usage.rst | 21 --------------------- src/config.cpp | 2 -- src/config.h | 1 - src/platform/windows/display.h | 1 - src/platform/windows/display_base.cpp | 19 ------------------- src_assets/common/assets/web/config.html | 12 ------------ 6 files changed, 56 deletions(-) diff --git a/docs/source/about/advanced_usage.rst b/docs/source/about/advanced_usage.rst index d7f546e8603..b8d15289c73 100644 --- a/docs/source/about/advanced_usage.rst +++ b/docs/source/about/advanced_usage.rst @@ -413,27 +413,6 @@ resolutions 3840x1600, ] -dwmflush -^^^^^^^^ - -**Description** - Invoke DwmFlush() to sync screen capture to the Windows presentation interval. - - .. Caution:: Applies to Windows only. Alleviates visual stuttering during mouse movement. - If enabled, this feature will automatically deactivate if the client framerate exceeds - the host monitor's current refresh rate. - - .. Note:: If you disable this option, you may see video stuttering during mouse movement in certain scenarios. - It is recommended to leave enabled when possible. - -**Default** - ``enabled`` - -**Example** - .. code-block:: text - - dwmflush = enabled - Audio ----- diff --git a/src/config.cpp b/src/config.cpp index 8d02933e2a8..f15eacb4773 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -438,7 +438,6 @@ namespace config { {}, // encoder {}, // adapter_name {}, // output_name - true // dwmflush }; audio_t audio { @@ -1034,7 +1033,6 @@ namespace config { string_f(vars, "encoder", video.encoder); string_f(vars, "adapter_name", video.adapter_name); string_f(vars, "output_name", video.output_name); - bool_f(vars, "dwmflush", video.dwmflush); path_f(vars, "pkey", nvhttp.pkey); path_f(vars, "cert", nvhttp.cert); diff --git a/src/config.h b/src/config.h index a4bac3b684e..b23b59cc2a3 100644 --- a/src/config.h +++ b/src/config.h @@ -64,7 +64,6 @@ namespace config { std::string encoder; std::string adapter_name; std::string output_name; - bool dwmflush; }; struct audio_t { diff --git a/src/platform/windows/display.h b/src/platform/windows/display.h index 316a36f72ec..b50acc7725d 100644 --- a/src/platform/windows/display.h +++ b/src/platform/windows/display.h @@ -109,7 +109,6 @@ namespace platf::dxgi { public: dup_t dup; bool has_frame {}; - bool use_dwmflush {}; std::chrono::steady_clock::time_point last_protected_content_warning_time {}; capture_e diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index 4fdf61b2d99..2324a8c305d 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -32,10 +32,6 @@ namespace platf::dxgi { return capture_status; } - if (use_dwmflush) { - DwmFlush(); - } - auto status = dup->AcquireNextFrame(timeout.count(), &frame_info, res_p); switch (status) { @@ -470,21 +466,6 @@ namespace platf::dxgi { << "Offset : "sv << offset_x << 'x' << offset_y << std::endl << "Virtual Desktop : "sv << env_width << 'x' << env_height; - // Enable DwmFlush() only if the current refresh rate can match the client framerate. - auto refresh_rate = config.framerate; - DWM_TIMING_INFO timing_info; - timing_info.cbSize = sizeof(timing_info); - - status = DwmGetCompositionTimingInfo(NULL, &timing_info); - if (FAILED(status)) { - BOOST_LOG(warning) << "Failed to detect active refresh rate."; - } - else { - refresh_rate = std::round((double) timing_info.rateRefresh.uiNumerator / (double) timing_info.rateRefresh.uiDenominator); - } - - dup.use_dwmflush = config::video.dwmflush && !(config.framerate > refresh_rate) ? true : false; - // Bump up thread priority { const DWORD flags = TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY; diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html index 02ee743403c..e0bb751d89e 100644 --- a/src_assets/common/assets/web/config.html +++ b/src_assets/common/assets/web/config.html @@ -582,18 +582,6 @@

Configuration

tools\dxgi-info.exe
- -
- - -
- Improves capture latency/smoothness during mouse movement.
- Disable if you encounter any VSync-related issues. -
-
Date: Fri, 12 May 2023 16:57:09 +0300 Subject: [PATCH 2/8] Add high_precision_sleep() method --- src/platform/windows/display.h | 5 +++ src/platform/windows/display_base.cpp | 57 +++++++++++++++++---------- 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/src/platform/windows/display.h b/src/platform/windows/display.h index b50acc7725d..238a87c27ac 100644 --- a/src/platform/windows/display.h +++ b/src/platform/windows/display.h @@ -126,6 +126,9 @@ namespace platf::dxgi { int init(const ::video::config_t &config, const std::string &display_name); + void + high_precision_sleep(std::chrono::nanoseconds duration); + capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override; @@ -141,6 +144,8 @@ namespace platf::dxgi { DXGI_FORMAT capture_format; D3D_FEATURE_LEVEL feature_level; + util::safe_ptr_v2, BOOL, CloseHandle> timer; + typedef enum _D3DKMT_SCHEDULINGPRIORITYCLASS { D3DKMT_SCHEDULINGPRIORITYCLASS_IDLE, D3DKMT_SCHEDULINGPRIORITYCLASS_BELOW_NORMAL, diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index 2324a8c305d..c016514a144 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -95,24 +95,30 @@ namespace platf::dxgi { release_frame(); } - capture_e - display_base_t::capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) { - auto next_frame = std::chrono::steady_clock::now(); - - // Use CREATE_WAITABLE_TIMER_HIGH_RESOLUTION if supported (Windows 10 1809+) - HANDLE timer = CreateWaitableTimerEx(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS); + void + display_base_t::high_precision_sleep(std::chrono::nanoseconds duration) { if (!timer) { - timer = CreateWaitableTimerEx(nullptr, nullptr, 0, TIMER_ALL_ACCESS); - if (!timer) { - auto winerr = GetLastError(); - BOOST_LOG(error) << "Failed to create timer: "sv << winerr; - return capture_e::error; - } + BOOST_LOG(error) << "Attempting high_precision_sleep() with uninitialized timer"; + return; + } + if (duration < 0s) { + BOOST_LOG(error) << "Attempting high_precision_sleep() with negative duration"; + return; + } + if (duration > 5s) { + BOOST_LOG(error) << "Attempting high_precision_sleep() with unexpectedly large duration (>5s)"; + return; } - auto close_timer = util::fail_guard([timer]() { - CloseHandle(timer); - }); + LARGE_INTEGER due_time; + due_time.QuadPart = duration.count() / -100; + SetWaitableTimer(timer.get(), &due_time, 0, nullptr, nullptr, false); + WaitForSingleObject(timer.get(), INFINITE); + } + + capture_e + display_base_t::capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) { + auto next_frame = std::chrono::steady_clock::now(); // Keep the display awake during capture. If the display goes to sleep during // capture, best case is that capture stops until it powers back on. However, @@ -131,13 +137,11 @@ namespace platf::dxgi { return platf::capture_e::reinit; } - // If the wait time is between 1 us and 1 second, wait the specified time + // If the wait time is positive and below 1 second, wait the specified time // and offset the next frame time from the exact current frame time target. - auto wait_time_us = std::chrono::duration_cast(next_frame - std::chrono::steady_clock::now()).count(); - if (wait_time_us > 0 && wait_time_us < 1000000) { - LARGE_INTEGER due_time { .QuadPart = -10LL * wait_time_us }; - SetWaitableTimer(timer, &due_time, 0, nullptr, nullptr, false); - WaitForSingleObject(timer, INFINITE); + auto wait_time = next_frame - std::chrono::steady_clock::now(); + if (wait_time > 0s && wait_time < 1s) { + high_precision_sleep(wait_time); next_frame += delay; } else { @@ -613,6 +617,17 @@ namespace platf::dxgi { // Capture format will be determined from the first call to AcquireNextFrame() capture_format = DXGI_FORMAT_UNKNOWN; + // Use CREATE_WAITABLE_TIMER_HIGH_RESOLUTION if supported (Windows 10 1809+) + timer.reset(CreateWaitableTimerEx(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS)); + if (!timer) { + timer.reset(CreateWaitableTimerEx(nullptr, nullptr, 0, TIMER_ALL_ACCESS)); + if (!timer) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "Failed to create timer: "sv << winerr; + return -1; + } + } + return 0; } From e03ac2a21dac8ff8aaff0eed686c70b3b5a64b5e Mon Sep 17 00:00:00 2001 From: ns6089 <61738816+ns6089@users.noreply.github.com> Date: Sat, 26 Aug 2023 12:38:47 +0300 Subject: [PATCH 3/8] Don't check AccumulatedFrames AMF doesn't do it and MSDN is unclear about it. --- src/platform/windows/display_vram.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/windows/display_vram.cpp b/src/platform/windows/display_vram.cpp index 890c4bb6dd7..be009a3e14c 100644 --- a/src/platform/windows/display_vram.cpp +++ b/src/platform/windows/display_vram.cpp @@ -957,7 +957,7 @@ namespace platf::dxgi { } const bool mouse_update_flag = frame_info.LastMouseUpdateTime.QuadPart != 0 || frame_info.PointerShapeBufferSize > 0; - const bool frame_update_flag = frame_info.AccumulatedFrames != 0 || frame_info.LastPresentTime.QuadPart != 0; + const bool frame_update_flag = frame_info.LastPresentTime.QuadPart != 0; const bool update_flag = mouse_update_flag || frame_update_flag; if (!update_flag) { From ff10c0aa61bd8f9cd66afd2d03fc3fe7194798ef Mon Sep 17 00:00:00 2001 From: ns6089 <61738816+ns6089@users.noreply.github.com> Date: Fri, 12 May 2023 17:07:22 +0300 Subject: [PATCH 4/8] Move client frame interval to local variable --- src/platform/windows/display.h | 6 ++++-- src/platform/windows/display_base.cpp | 14 ++++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/platform/windows/display.h b/src/platform/windows/display.h index 238a87c27ac..5c77aa0d834 100644 --- a/src/platform/windows/display.h +++ b/src/platform/windows/display.h @@ -132,14 +132,16 @@ namespace platf::dxgi { capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override; - std::chrono::nanoseconds delay; - factory1_t factory; adapter_t adapter; output_t output; device_t device; device_ctx_t device_ctx; duplication_t dup; + DXGI_RATIONAL display_refresh_rate; + int display_refresh_rate_rounded; + + int client_frame_rate; DXGI_FORMAT capture_format; D3D_FEATURE_LEVEL feature_level; diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index c016514a144..4382ef0aff0 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -119,6 +119,7 @@ namespace platf::dxgi { capture_e display_base_t::capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) { auto next_frame = std::chrono::steady_clock::now(); + const auto client_frame_interval = std::chrono::nanoseconds { 1s } / client_frame_rate; // Keep the display awake during capture. If the display goes to sleep during // capture, best case is that capture stops until it powers back on. However, @@ -142,14 +143,14 @@ namespace platf::dxgi { auto wait_time = next_frame - std::chrono::steady_clock::now(); if (wait_time > 0s && wait_time < 1s) { high_precision_sleep(wait_time); - next_frame += delay; + next_frame += client_frame_interval; } else { // If the wait time is negative (meaning the frame is past due) or the // computed wait time is beyond a second (meaning possible clock issues), // just capture the frame now and resynchronize the frame interval with // the current time. - next_frame = std::chrono::steady_clock::now() + delay; + next_frame = std::chrono::steady_clock::now() + client_frame_interval; } std::shared_ptr img_out; @@ -334,8 +335,6 @@ namespace platf::dxgi { // Ensure we can duplicate the current display syncThreadDesktop(); - delay = std::chrono::nanoseconds { 1s } / config.framerate; - // Get rectangle of full desktop for absolute mouse coordinates env_width = GetSystemMetrics(SM_CXVIRTUALSCREEN); env_height = GetSystemMetrics(SM_CYVIRTUALSCREEN); @@ -595,6 +594,13 @@ namespace platf::dxgi { BOOST_LOG(info) << "Desktop resolution ["sv << dup_desc.ModeDesc.Width << 'x' << dup_desc.ModeDesc.Height << ']'; BOOST_LOG(info) << "Desktop format ["sv << dxgi_format_to_string(dup_desc.ModeDesc.Format) << ']'; + display_refresh_rate = dup_desc.ModeDesc.RefreshRate; + display_refresh_rate_rounded = lround((double) display_refresh_rate.Numerator / display_refresh_rate.Denominator); + BOOST_LOG(info) << "Display refresh rate [" << display_refresh_rate_rounded << "Hz]"; + + client_frame_rate = config.framerate; + BOOST_LOG(info) << "Requested frame rate [" << client_frame_rate << "fps]"; + dxgi::output6_t output6 {}; status = output->QueryInterface(IID_IDXGIOutput6, (void **) &output6); if (SUCCEEDED(status)) { From 7e1ca69574e825e8850069c30cd36868661d7df0 Mon Sep 17 00:00:00 2001 From: ns6089 <61738816+ns6089@users.noreply.github.com> Date: Fri, 12 May 2023 17:34:50 +0300 Subject: [PATCH 5/8] Release duplication frame after snapshot --- src/platform/windows/display_base.cpp | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index 4382ef0aff0..c5809a48391 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -74,19 +74,20 @@ namespace platf::dxgi { } auto status = dup->ReleaseFrame(); + has_frame = false; switch (status) { case S_OK: - has_frame = false; return capture_e::ok; - case DXGI_ERROR_WAIT_TIMEOUT: - return capture_e::timeout; - case WAIT_ABANDONED: + + case DXGI_ERROR_INVALID_CALL: + BOOST_LOG(warning) << "Duplication frame already released"; + return capture_e::ok; + case DXGI_ERROR_ACCESS_LOST: - case DXGI_ERROR_ACCESS_DENIED: - has_frame = false; return capture_e::reinit; + default: - BOOST_LOG(error) << "Couldn't release frame [0x"sv << util::hex(status).to_string_view(); + BOOST_LOG(error) << "Error while releasing duplication frame [0x"sv << util::hex(status).to_string_view(); return capture_e::error; } } @@ -174,6 +175,11 @@ namespace platf::dxgi { BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; return status; } + + status = dup.release_frame(); + if (status != platf::capture_e::ok) { + return status; + } } return capture_e::ok; From e50c01980f825d61a0f3890b7a2239e3a868d455 Mon Sep 17 00:00:00 2001 From: ns6089 <61738816+ns6089@users.noreply.github.com> Date: Fri, 12 May 2023 17:39:58 +0300 Subject: [PATCH 6/8] Rework capture sleeps for better frame stability --- src/platform/windows/display_base.cpp | 68 +++++++++++++++++++++------ src/stat_trackers.h | 2 +- 2 files changed, 54 insertions(+), 16 deletions(-) diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index c5809a48391..c9a0b9f31ef 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -17,6 +17,7 @@ typedef long NTSTATUS; #include "src/config.h" #include "src/main.h" #include "src/platform/common.h" +#include "src/stat_trackers.h" #include "src/video.h" namespace platf { @@ -119,8 +120,8 @@ namespace platf::dxgi { capture_e display_base_t::capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) { - auto next_frame = std::chrono::steady_clock::now(); const auto client_frame_interval = std::chrono::nanoseconds { 1s } / client_frame_rate; + std::optional next_frame_time; // Keep the display awake during capture. If the display goes to sleep during // capture, best case is that capture stops until it powers back on. However, @@ -131,6 +132,8 @@ namespace platf::dxgi { SetThreadExecutionState(ES_CONTINUOUS); }); + stat_trackers::min_max_avg_tracker sleep_overshoot_tracker; + while (true) { // This will return false if the HDR state changes or for any number of other // display or GPU changes. We should reinit to examine the updated state of @@ -139,23 +142,58 @@ namespace platf::dxgi { return platf::capture_e::reinit; } - // If the wait time is positive and below 1 second, wait the specified time - // and offset the next frame time from the exact current frame time target. - auto wait_time = next_frame - std::chrono::steady_clock::now(); - if (wait_time > 0s && wait_time < 1s) { - high_precision_sleep(wait_time); - next_frame += client_frame_interval; + platf::capture_e status = capture_e::ok; + std::shared_ptr img_out; + + // Try to continue frame pacing group, snapshot() is called with zero timeout after waiting for client frame interval + if (next_frame_time) { + const auto sleep_period = *next_frame_time - std::chrono::steady_clock::now(); + + if (sleep_period <= 0ns) { + // We missed next frame time, invalidating current frame pacing group + next_frame_time = std::nullopt; + status = capture_e::timeout; + } + else { + high_precision_sleep(sleep_period); + + if (config::sunshine.min_log_level <= 1) { + // Print sleep overshoot stats to debug log every 20 seconds + auto print_info = [&](double min_overshoot, double max_overshoot, double avg_overshoot) { + auto f = stat_trackers::one_digit_after_decimal(); + BOOST_LOG(debug) << "Sleep overshoot (min/max/avg): " << f % min_overshoot << "ms/" << f % max_overshoot << "ms/" << f % avg_overshoot << "ms"; + }; + std::chrono::nanoseconds overshoot_ns = std::chrono::steady_clock::now() - *next_frame_time; + sleep_overshoot_tracker.collect_and_callback_on_interval(overshoot_ns.count() / 1000000., print_info, 20s); + } + + status = snapshot(pull_free_image_cb, img_out, 0ms, *cursor); + + if (status == capture_e::ok && img_out) { + *next_frame_time += client_frame_interval; + } + else { + next_frame_time = std::nullopt; + } + } } - else { - // If the wait time is negative (meaning the frame is past due) or the - // computed wait time is beyond a second (meaning possible clock issues), - // just capture the frame now and resynchronize the frame interval with - // the current time. - next_frame = std::chrono::steady_clock::now() + client_frame_interval; + + // Start new frame pacing group if necessary, snapshot() is called with non-zero timeout + if (status == capture_e::timeout || (status == capture_e::ok && !next_frame_time)) { + status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); + + if (status == capture_e::ok && img_out) { + next_frame_time = img_out->frame_timestamp; + + if (!next_frame_time) { + BOOST_LOG(warning) << "snapshot() provided image without timestamp"; + next_frame_time = std::chrono::steady_clock::now(); + } + + *next_frame_time += client_frame_interval; + } } - std::shared_ptr img_out; - auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); switch (status) { case platf::capture_e::reinit: case platf::capture_e::error: diff --git a/src/stat_trackers.h b/src/stat_trackers.h index 62df7778cc6..124b906472a 100644 --- a/src/stat_trackers.h +++ b/src/stat_trackers.h @@ -41,7 +41,7 @@ namespace stat_trackers { struct { std::chrono::steady_clock::steady_clock::time_point last_callback_time = std::chrono::steady_clock::now(); T stat_min = std::numeric_limits::max(); - T stat_max = 0; + T stat_max = std::numeric_limits::min(); double stat_total = 0; uint32_t calls = 0; } data; From 1d4b1d2d1cc896b783c96c58f3cc13aa53cb9e15 Mon Sep 17 00:00:00 2001 From: ns6089 <61738816+ns6089@users.noreply.github.com> Date: Sat, 26 Aug 2023 17:27:36 +0300 Subject: [PATCH 7/8] Adjust capture rate to better match display --- src/platform/windows/display_base.cpp | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index c9a0b9f31ef..769903633f8 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -120,7 +120,28 @@ namespace platf::dxgi { capture_e display_base_t::capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) { - const auto client_frame_interval = std::chrono::nanoseconds { 1s } / client_frame_rate; + auto calculate_client_frame_interval = [&]() -> std::chrono::nanoseconds { + double display_frame_rate = (double) display_refresh_rate.Numerator / display_refresh_rate.Denominator; + + double client_frame_rate_adjusted; + if (client_frame_rate >= display_frame_rate) { + client_frame_rate_adjusted = display_frame_rate * std::round(client_frame_rate / display_frame_rate); + } + else { + client_frame_rate_adjusted = display_frame_rate / std::round(display_frame_rate / client_frame_rate); + } + + // Adjust capture frame interval when display refresh rate is not integral but very close to requested fps. + // Can only decrease requested fps, otherwise client may start accumulating frames and suffer increased latency. + if (client_frame_rate > client_frame_rate_adjusted && client_frame_rate_adjusted / client_frame_rate > 0.99) { + BOOST_LOG(info) << "Adjusted capture rate to " << client_frame_rate_adjusted << "fps to better match display"; + return std::chrono::nanoseconds(std::llround(std::nano::den / client_frame_rate_adjusted)); + } + + return std::chrono::nanoseconds(1s) / client_frame_rate; + }; + + const auto client_frame_interval = calculate_client_frame_interval(); std::optional next_frame_time; // Keep the display awake during capture. If the display goes to sleep during From 9514e21fcd5b3aa05100db970ff11b5dc29a7311 Mon Sep 17 00:00:00 2001 From: ns6089 <61738816+ns6089@users.noreply.github.com> Date: Sun, 27 Aug 2023 19:13:21 +0300 Subject: [PATCH 8/8] Don't accumulate errors in capture frame pacing --- src/platform/windows/display_base.cpp | 66 +++++++++++++++------------ 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index 769903633f8..09847a1d5b0 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -120,29 +120,30 @@ namespace platf::dxgi { capture_e display_base_t::capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) { - auto calculate_client_frame_interval = [&]() -> std::chrono::nanoseconds { - double display_frame_rate = (double) display_refresh_rate.Numerator / display_refresh_rate.Denominator; - - double client_frame_rate_adjusted; - if (client_frame_rate >= display_frame_rate) { - client_frame_rate_adjusted = display_frame_rate * std::round(client_frame_rate / display_frame_rate); - } - else { - client_frame_rate_adjusted = display_frame_rate / std::round(display_frame_rate / client_frame_rate); - } - + auto adjust_client_frame_rate = [&]() -> DXGI_RATIONAL { // Adjust capture frame interval when display refresh rate is not integral but very close to requested fps. - // Can only decrease requested fps, otherwise client may start accumulating frames and suffer increased latency. - if (client_frame_rate > client_frame_rate_adjusted && client_frame_rate_adjusted / client_frame_rate > 0.99) { - BOOST_LOG(info) << "Adjusted capture rate to " << client_frame_rate_adjusted << "fps to better match display"; - return std::chrono::nanoseconds(std::llround(std::nano::den / client_frame_rate_adjusted)); + if (display_refresh_rate.Denominator > 1) { + DXGI_RATIONAL candidate = display_refresh_rate; + if (client_frame_rate % display_refresh_rate_rounded == 0) { + candidate.Numerator *= client_frame_rate / display_refresh_rate_rounded; + } + else if (display_refresh_rate_rounded % client_frame_rate == 0) { + candidate.Denominator *= display_refresh_rate_rounded / client_frame_rate; + } + double candidate_rate = (double) candidate.Numerator / candidate.Denominator; + // Can only decrease requested fps, otherwise client may start accumulating frames and suffer increased latency. + if (client_frame_rate > candidate_rate && candidate_rate / client_frame_rate > 0.99) { + BOOST_LOG(info) << "Adjusted capture rate to " << candidate_rate << "fps to better match display"; + return candidate; + } } - return std::chrono::nanoseconds(1s) / client_frame_rate; + return { (uint32_t) client_frame_rate, 1 }; }; - const auto client_frame_interval = calculate_client_frame_interval(); - std::optional next_frame_time; + DXGI_RATIONAL client_frame_rate_adjusted = adjust_client_frame_rate(); + std::optional frame_pacing_group_start; + uint32_t frame_pacing_group_frames = 0; // Keep the display awake during capture. If the display goes to sleep during // capture, best case is that capture stops until it powers back on. However, @@ -167,12 +168,18 @@ namespace platf::dxgi { std::shared_ptr img_out; // Try to continue frame pacing group, snapshot() is called with zero timeout after waiting for client frame interval - if (next_frame_time) { - const auto sleep_period = *next_frame_time - std::chrono::steady_clock::now(); + if (frame_pacing_group_start) { + const uint32_t seconds = (uint64_t) frame_pacing_group_frames * client_frame_rate_adjusted.Denominator / client_frame_rate_adjusted.Numerator; + const uint32_t remainder = (uint64_t) frame_pacing_group_frames * client_frame_rate_adjusted.Denominator % client_frame_rate_adjusted.Numerator; + const auto sleep_target = *frame_pacing_group_start + + std::chrono::nanoseconds(1s) * seconds + + std::chrono::nanoseconds(1s) * remainder / client_frame_rate_adjusted.Numerator; + const auto sleep_period = sleep_target - std::chrono::steady_clock::now(); if (sleep_period <= 0ns) { // We missed next frame time, invalidating current frame pacing group - next_frame_time = std::nullopt; + frame_pacing_group_start = std::nullopt; + frame_pacing_group_frames = 0; status = capture_e::timeout; } else { @@ -184,34 +191,35 @@ namespace platf::dxgi { auto f = stat_trackers::one_digit_after_decimal(); BOOST_LOG(debug) << "Sleep overshoot (min/max/avg): " << f % min_overshoot << "ms/" << f % max_overshoot << "ms/" << f % avg_overshoot << "ms"; }; - std::chrono::nanoseconds overshoot_ns = std::chrono::steady_clock::now() - *next_frame_time; + std::chrono::nanoseconds overshoot_ns = std::chrono::steady_clock::now() - sleep_target; sleep_overshoot_tracker.collect_and_callback_on_interval(overshoot_ns.count() / 1000000., print_info, 20s); } status = snapshot(pull_free_image_cb, img_out, 0ms, *cursor); if (status == capture_e::ok && img_out) { - *next_frame_time += client_frame_interval; + frame_pacing_group_frames += 1; } else { - next_frame_time = std::nullopt; + frame_pacing_group_start = std::nullopt; + frame_pacing_group_frames = 0; } } } // Start new frame pacing group if necessary, snapshot() is called with non-zero timeout - if (status == capture_e::timeout || (status == capture_e::ok && !next_frame_time)) { + if (status == capture_e::timeout || (status == capture_e::ok && !frame_pacing_group_start)) { status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor); if (status == capture_e::ok && img_out) { - next_frame_time = img_out->frame_timestamp; + frame_pacing_group_start = img_out->frame_timestamp; - if (!next_frame_time) { + if (!frame_pacing_group_start) { BOOST_LOG(warning) << "snapshot() provided image without timestamp"; - next_frame_time = std::chrono::steady_clock::now(); + frame_pacing_group_start = std::chrono::steady_clock::now(); } - *next_frame_time += client_frame_interval; + frame_pacing_group_frames = 1; } }