Commit 873b91f1 authored by stanisc's avatar stanisc Committed by Commit bot

D3D V-sync: prevent timestamp drift on a secondary monitor

I got back some preliminary UMA data from Canary experiment that
confirm the timestamp drift relative to the timing of v-sync signal
which makes BeginImplFrameLatency2 UMA to be all over the place with
a distribution that is spread evenly in the entire 0 - 16667 range.

This happens because D3D V-sync signal is generated based on v-blank
event for a display that contains contains the window (the current
display), but the timestamp is obtained from DWM which is based on
the most recent v-blank timing for the primary monitor. So if a
secondary monitor frequency is even slightly different that causes
v-sync / RAF timestamp drift that is clearly visible on some websites
like vsynctester.com.

One possible solution is to capture the timestamp when v-blank event
is received, but that seems to be a bit less smooth than the DWM
timestamp. So the compromise is to use DWM timing only when running on
a primary monitor; otherwise use the v-blank wake-up timestamp.
I've verified that this fixes BeginImplFrameLatency2 UMA distribution on
my setup where the secondary monitor refresh rate seems to differ from
the primary monitor by about 0.15 Hz.

BUG=467617,680639
CQ_INCLUDE_TRYBOTS=master.tryserver.chromium.android:android_optional_gpu_tests_rel;master.tryserver.chromium.linux:linux_optional_gpu_tests_rel;master.tryserver.chromium.mac:mac_optional_gpu_tests_rel;master.tryserver.chromium.win:win_optional_gpu_tests_rel

Review-Url: https://codereview.chromium.org/2874833003
Cr-Commit-Position: refs/heads/master@{#472279}
parent 5e6a740e
...@@ -87,6 +87,7 @@ target(link_target_type, "ipc_service_sources") { ...@@ -87,6 +87,7 @@ target(link_target_type, "ipc_service_sources") {
"direct_composition_surface_win.h", "direct_composition_surface_win.h",
"image_transport_surface_win.cc", "image_transport_surface_win.cc",
] ]
libs += [ "dwmapi.lib" ]
} }
if (is_mac) { if (is_mac) {
sources += [ sources += [
......
...@@ -4,6 +4,9 @@ ...@@ -4,6 +4,9 @@
#include "gpu/ipc/service/gpu_vsync_provider_win.h" #include "gpu/ipc/service/gpu_vsync_provider_win.h"
#include <dwmapi.h>
#include <windows.h>
#include <string> #include <string>
#include "base/atomicops.h" #include "base/atomicops.h"
...@@ -14,15 +17,12 @@ ...@@ -14,15 +17,12 @@
#include "gpu/ipc/common/gpu_messages.h" #include "gpu/ipc/common/gpu_messages.h"
#include "ipc/ipc_message_macros.h" #include "ipc/ipc_message_macros.h"
#include "ipc/message_filter.h" #include "ipc/message_filter.h"
#include "ui/gl/vsync_provider_win.h"
#include <windows.h>
namespace gpu { namespace gpu {
namespace { namespace {
// Default interval used when no v-sync interval comes from DWM. // Default v-sync interval used when there is no history of v-sync timestamps.
const int kDefaultTimerBasedInterval = 16666; const int kDefaultInterval = 16666;
// from <D3dkmthk.h> // from <D3dkmthk.h>
typedef LONG NTSTATUS; typedef LONG NTSTATUS;
...@@ -79,14 +79,20 @@ class GpuVSyncWorker : public base::Thread, ...@@ -79,14 +79,20 @@ class GpuVSyncWorker : public base::Thread,
void CloseAdapter(); void CloseAdapter();
NTSTATUS WaitForVBlankEvent(); NTSTATUS WaitForVBlankEvent();
void SendGpuVSyncUpdate(base::TimeTicks now, void AddTimestamp(base::TimeTicks timestamp);
base::TimeTicks timestamp, void AddInterval(base::TimeDelta interval);
base::TimeDelta interval); base::TimeDelta GetAverageInterval() const;
void ClearIntervalHistory();
bool GetDisplayFrequency(const wchar_t* device_name, DWORD* frequency);
void UpdateCurrentDisplayFrequency();
bool GetDwmVBlankTimestamp(base::TimeTicks* timestamp);
void SendGpuVSyncUpdate(base::TimeTicks now, bool use_dwm);
void InvokeCallbackAndReschedule(base::TimeTicks timestamp, void InvokeCallbackAndReschedule(base::TimeTicks timestamp,
base::TimeDelta interval); base::TimeDelta interval);
void UseDelayBasedVSyncOnError(); void UseDelayBasedVSyncOnError();
void ScheduleDelayBasedVSync(base::TimeTicks timebase,
base::TimeDelta interval);
// Specifies whether background tasks are running. // Specifies whether background tasks are running.
// This can be set on background thread only. // This can be set on background thread only.
...@@ -99,9 +105,6 @@ class GpuVSyncWorker : public base::Thread, ...@@ -99,9 +105,6 @@ class GpuVSyncWorker : public base::Thread,
const gfx::VSyncProvider::UpdateVSyncCallback callback_; const gfx::VSyncProvider::UpdateVSyncCallback callback_;
const SurfaceHandle surface_handle_; const SurfaceHandle surface_handle_;
// The actual timing and interval comes from the nested provider.
std::unique_ptr<gl::VSyncProviderWin> vsync_provider_;
PFND3DKMTOPENADAPTERFROMHDC open_adapter_from_hdc_ptr_; PFND3DKMTOPENADAPTERFROMHDC open_adapter_from_hdc_ptr_;
PFND3DKMTCLOSEADAPTER close_adapter_ptr_; PFND3DKMTCLOSEADAPTER close_adapter_ptr_;
PFND3DKMTWAITFORVERTICALBLANKEVENT wait_for_vertical_blank_event_ptr_; PFND3DKMTWAITFORVERTICALBLANKEVENT wait_for_vertical_blank_event_ptr_;
...@@ -109,6 +112,30 @@ class GpuVSyncWorker : public base::Thread, ...@@ -109,6 +112,30 @@ class GpuVSyncWorker : public base::Thread,
std::wstring current_device_name_; std::wstring current_device_name_;
D3DKMT_HANDLE current_adapter_handle_ = 0; D3DKMT_HANDLE current_adapter_handle_ = 0;
D3DDDI_VIDEO_PRESENT_SOURCE_ID current_source_id_ = 0; D3DDDI_VIDEO_PRESENT_SOURCE_ID current_source_id_ = 0;
// Last known v-sync timestamp.
base::TimeTicks last_timestamp_;
// Current display refresh frequency in Hz which is used to detect
// when the frequency changes and and to update the accepted interval
// range below.
DWORD current_display_frequency_ = 0;
// Range of intervals accepted for the average calculation which is
// +/-20% from the interval corresponding to the display frequency above.
// This is used to filter out outliers.
base::TimeDelta min_accepted_interval_;
base::TimeDelta max_accepted_interval_;
// History of recent deltas between timestamps which is used to calculate the
// average v-sync interval and organized as a circular buffer.
static const size_t kIntervalHistorySize = 60;
base::TimeDelta interval_history_[kIntervalHistorySize];
size_t history_index_ = 0;
size_t history_size_ = 0;
// Rolling sum of intervals in the circular buffer above.
base::TimeDelta rolling_interval_sum_;
}; };
GpuVSyncWorker::GpuVSyncWorker( GpuVSyncWorker::GpuVSyncWorker(
...@@ -116,8 +143,7 @@ GpuVSyncWorker::GpuVSyncWorker( ...@@ -116,8 +143,7 @@ GpuVSyncWorker::GpuVSyncWorker(
SurfaceHandle surface_handle) SurfaceHandle surface_handle)
: base::Thread(base::StringPrintf("VSync-%d", surface_handle)), : base::Thread(base::StringPrintf("VSync-%d", surface_handle)),
callback_(callback), callback_(callback),
surface_handle_(surface_handle), surface_handle_(surface_handle) {
vsync_provider_(new gl::VSyncProviderWin(surface_handle)) {
HMODULE gdi32 = GetModuleHandle(L"gdi32"); HMODULE gdi32 = GetModuleHandle(L"gdi32");
if (!gdi32) { if (!gdi32) {
NOTREACHED() << "Can't open gdi32.dll"; NOTREACHED() << "Can't open gdi32.dll";
...@@ -209,6 +235,14 @@ void GpuVSyncWorker::WaitForVSyncOnThread() { ...@@ -209,6 +235,14 @@ void GpuVSyncWorker::WaitForVSyncOnThread() {
} }
} }
UpdateCurrentDisplayFrequency();
// Use DWM timing only when running on the primary monitor which DWM
// is synchronized with and only if we can get accurate high resulution
// timestamps.
bool use_dwm = (monitor_info.dwFlags & MONITORINFOF_PRIMARY) != 0 &&
base::TimeTicks::IsHighResolution();
NTSTATUS wait_result = WaitForVBlankEvent(); NTSTATUS wait_result = WaitForVBlankEvent();
if (wait_result != STATUS_SUCCESS) { if (wait_result != STATUS_SUCCESS) {
if (wait_result == STATUS_GRAPHICS_PRESENT_OCCLUDED) { if (wait_result == STATUS_GRAPHICS_PRESENT_OCCLUDED) {
...@@ -221,34 +255,119 @@ void GpuVSyncWorker::WaitForVSyncOnThread() { ...@@ -221,34 +255,119 @@ void GpuVSyncWorker::WaitForVSyncOnThread() {
} }
} }
vsync_provider_->GetVSyncParameters( SendGpuVSyncUpdate(base::TimeTicks::Now(), use_dwm);
base::Bind(&GpuVSyncWorker::SendGpuVSyncUpdate, base::Unretained(this),
base::TimeTicks::Now()));
} }
void GpuVSyncWorker::SendGpuVSyncUpdate(base::TimeTicks now, void GpuVSyncWorker::AddTimestamp(base::TimeTicks timestamp) {
base::TimeTicks timestamp, if (!last_timestamp_.is_null()) {
base::TimeDelta interval) { AddInterval(timestamp - last_timestamp_);
}
last_timestamp_ = timestamp;
}
void GpuVSyncWorker::AddInterval(base::TimeDelta interval) {
if (interval < min_accepted_interval_ || interval > max_accepted_interval_)
return;
if (history_size_ == kIntervalHistorySize) {
rolling_interval_sum_ -= interval_history_[history_index_];
} else {
history_size_++;
}
interval_history_[history_index_] = interval;
rolling_interval_sum_ += interval;
history_index_ = (history_index_ + 1) % kIntervalHistorySize;
}
void GpuVSyncWorker::ClearIntervalHistory() {
last_timestamp_ = base::TimeTicks();
rolling_interval_sum_ = base::TimeDelta();
history_index_ = 0;
history_size_ = 0;
}
base::TimeDelta GpuVSyncWorker::GetAverageInterval() const {
return !rolling_interval_sum_.is_zero()
? rolling_interval_sum_ / history_size_
: base::TimeDelta::FromMicroseconds(kDefaultInterval);
}
bool GpuVSyncWorker::GetDisplayFrequency(const wchar_t* device_name,
DWORD* frequency) {
DEVMODE display_info;
display_info.dmSize = sizeof(DEVMODE);
display_info.dmDriverExtra = 0;
BOOL result =
EnumDisplaySettings(device_name, ENUM_CURRENT_SETTINGS, &display_info);
if (result && display_info.dmDisplayFrequency > 1) {
*frequency = display_info.dmDisplayFrequency;
return true;
}
return false;
}
void GpuVSyncWorker::UpdateCurrentDisplayFrequency() {
DWORD frequency;
DCHECK(!current_device_name_.empty());
if (!GetDisplayFrequency(current_device_name_.c_str(), &frequency)) {
current_display_frequency_ = 0;
return;
}
if (frequency != current_display_frequency_) {
current_display_frequency_ = frequency;
base::TimeDelta interval = base::TimeDelta::FromMicroseconds(
base::Time::kMicrosecondsPerSecond / static_cast<double>(frequency));
ClearIntervalHistory();
min_accepted_interval_ = interval * 0.8;
max_accepted_interval_ = interval * 1.2;
AddInterval(interval);
}
}
bool GpuVSyncWorker::GetDwmVBlankTimestamp(base::TimeTicks* timestamp) {
DWM_TIMING_INFO timing_info;
timing_info.cbSize = sizeof(timing_info);
HRESULT result = DwmGetCompositionTimingInfo(nullptr, &timing_info);
if (result != S_OK)
return false;
*timestamp = base::TimeTicks::FromQPCValue(
static_cast<LONGLONG>(timing_info.qpcVBlank));
return true;
}
void GpuVSyncWorker::SendGpuVSyncUpdate(base::TimeTicks now, bool use_dwm) {
base::TimeTicks timestamp;
base::TimeDelta adjustment; base::TimeDelta adjustment;
if (!(timestamp.is_null() || interval.is_zero())) { if (use_dwm && GetDwmVBlankTimestamp(&timestamp)) {
// Timestamp comes from DwmGetCompositionTimingInfo and apparently it might // Timestamp comes from DwmGetCompositionTimingInfo and apparently it might
// be up to 2-3 vsync cycles in the past or in the future. // be up to 2-3 vsync cycles in the past or in the future.
// The adjustment formula was suggested here: // The adjustment formula was suggested here:
// http://www.vsynctester.com/firefoxisbroken.html // http://www.vsynctester.com/firefoxisbroken.html
base::TimeDelta interval = GetAverageInterval();
adjustment = adjustment =
((now - timestamp + interval / 8) % interval + interval) % interval - ((now - timestamp + interval / 8) % interval + interval) % interval -
interval / 8; interval / 8;
timestamp = now - adjustment; timestamp = now - adjustment;
} else { } else {
// DWM must be disabled. // Not using DWM.
timestamp = now; timestamp = now;
} }
AddTimestamp(timestamp);
TRACE_EVENT1("gpu", "GpuVSyncWorker::SendGpuVSyncUpdate", "adjustment", TRACE_EVENT1("gpu", "GpuVSyncWorker::SendGpuVSyncUpdate", "adjustment",
adjustment.ToInternalValue()); adjustment.ToInternalValue());
InvokeCallbackAndReschedule(timestamp, interval); DCHECK_GT(GetAverageInterval().InMillisecondsF(), 0);
InvokeCallbackAndReschedule(timestamp, GetAverageInterval());
} }
void GpuVSyncWorker::InvokeCallbackAndReschedule(base::TimeTicks timestamp, void GpuVSyncWorker::InvokeCallbackAndReschedule(base::TimeTicks timestamp,
...@@ -261,6 +380,8 @@ void GpuVSyncWorker::InvokeCallbackAndReschedule(base::TimeTicks timestamp, ...@@ -261,6 +380,8 @@ void GpuVSyncWorker::InvokeCallbackAndReschedule(base::TimeTicks timestamp,
base::Unretained(this))); base::Unretained(this)));
} else { } else {
running_ = false; running_ = false;
// Clear last_timestamp_ to avoid a long interval when the worker restarts.
last_timestamp_ = base::TimeTicks();
} }
} }
...@@ -269,18 +390,11 @@ void GpuVSyncWorker::UseDelayBasedVSyncOnError() { ...@@ -269,18 +390,11 @@ void GpuVSyncWorker::UseDelayBasedVSyncOnError() {
// Use timer based mechanism as a backup for one v-sync cycle, start with // Use timer based mechanism as a backup for one v-sync cycle, start with
// getting VSync parameters to determine timebase and interval. // getting VSync parameters to determine timebase and interval.
// TODO(stanisc): Consider a slower v-sync rate in this particular case. // TODO(stanisc): Consider a slower v-sync rate in this particular case.
vsync_provider_->GetVSyncParameters(base::Bind(
&GpuVSyncWorker::ScheduleDelayBasedVSync, base::Unretained(this)));
}
void GpuVSyncWorker::ScheduleDelayBasedVSync(base::TimeTicks timebase, base::TimeTicks timebase;
base::TimeDelta interval) { GetDwmVBlankTimestamp(&timebase);
// This is called only when WaitForVBlankEvent fails due to monitor going to
// sleep. Use a delay based v-sync as a back-up.
if (interval.is_zero()) {
interval = base::TimeDelta::FromMicroseconds(kDefaultTimerBasedInterval);
}
base::TimeDelta interval = GetAverageInterval();
base::TimeTicks now = base::TimeTicks::Now(); base::TimeTicks now = base::TimeTicks::Now();
base::TimeTicks next_vsync = now.SnappedToNextTick(timebase, interval); base::TimeTicks next_vsync = now.SnappedToNextTick(timebase, interval);
...@@ -327,6 +441,8 @@ void GpuVSyncWorker::CloseAdapter() { ...@@ -327,6 +441,8 @@ void GpuVSyncWorker::CloseAdapter() {
current_adapter_handle_ = 0; current_adapter_handle_ = 0;
current_device_name_.clear(); current_device_name_.clear();
ClearIntervalHistory();
} }
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment