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;
}
}