Skip to content

Commit

Permalink
D3D V-sync: prevent timestamp drift on a secondary monitor
Browse files Browse the repository at this point in the history
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}
  • Loading branch information
stanisc authored and Commit bot committed May 17, 2017
1 parent 5e6a740 commit 873b91f
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 34 deletions.
1 change: 1 addition & 0 deletions gpu/ipc/service/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ target(link_target_type, "ipc_service_sources") {
"direct_composition_surface_win.h",
"image_transport_surface_win.cc",
]
libs += [ "dwmapi.lib" ]
}
if (is_mac) {
sources += [
Expand Down
184 changes: 150 additions & 34 deletions gpu/ipc/service/gpu_vsync_provider_win.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

#include "gpu/ipc/service/gpu_vsync_provider_win.h"

#include <dwmapi.h>
#include <windows.h>

#include <string>

#include "base/atomicops.h"
Expand All @@ -14,15 +17,12 @@
#include "gpu/ipc/common/gpu_messages.h"
#include "ipc/ipc_message_macros.h"
#include "ipc/message_filter.h"
#include "ui/gl/vsync_provider_win.h"

#include <windows.h>

namespace gpu {

namespace {
// Default interval used when no v-sync interval comes from DWM.
const int kDefaultTimerBasedInterval = 16666;
// Default v-sync interval used when there is no history of v-sync timestamps.
const int kDefaultInterval = 16666;

// from <D3dkmthk.h>
typedef LONG NTSTATUS;
Expand Down Expand Up @@ -79,14 +79,20 @@ class GpuVSyncWorker : public base::Thread,
void CloseAdapter();
NTSTATUS WaitForVBlankEvent();

void SendGpuVSyncUpdate(base::TimeTicks now,
base::TimeTicks timestamp,
base::TimeDelta interval);
void AddTimestamp(base::TimeTicks timestamp);
void AddInterval(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,
base::TimeDelta interval);
void UseDelayBasedVSyncOnError();
void ScheduleDelayBasedVSync(base::TimeTicks timebase,
base::TimeDelta interval);

// Specifies whether background tasks are running.
// This can be set on background thread only.
Expand All @@ -99,25 +105,45 @@ class GpuVSyncWorker : public base::Thread,
const gfx::VSyncProvider::UpdateVSyncCallback callback_;
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_;
PFND3DKMTCLOSEADAPTER close_adapter_ptr_;
PFND3DKMTWAITFORVERTICALBLANKEVENT wait_for_vertical_blank_event_ptr_;

std::wstring current_device_name_;
D3DKMT_HANDLE current_adapter_handle_ = 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(
const gfx::VSyncProvider::UpdateVSyncCallback& callback,
SurfaceHandle surface_handle)
: base::Thread(base::StringPrintf("VSync-%d", surface_handle)),
callback_(callback),
surface_handle_(surface_handle),
vsync_provider_(new gl::VSyncProviderWin(surface_handle)) {
surface_handle_(surface_handle) {
HMODULE gdi32 = GetModuleHandle(L"gdi32");
if (!gdi32) {
NOTREACHED() << "Can't open gdi32.dll";
Expand Down Expand Up @@ -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();
if (wait_result != STATUS_SUCCESS) {
if (wait_result == STATUS_GRAPHICS_PRESENT_OCCLUDED) {
Expand All @@ -221,34 +255,119 @@ void GpuVSyncWorker::WaitForVSyncOnThread() {
}
}

vsync_provider_->GetVSyncParameters(
base::Bind(&GpuVSyncWorker::SendGpuVSyncUpdate, base::Unretained(this),
base::TimeTicks::Now()));
SendGpuVSyncUpdate(base::TimeTicks::Now(), use_dwm);
}

void GpuVSyncWorker::AddTimestamp(base::TimeTicks timestamp) {
if (!last_timestamp_.is_null()) {
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::SendGpuVSyncUpdate(base::TimeTicks now,
base::TimeTicks timestamp,
base::TimeDelta interval) {
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;

if (!(timestamp.is_null() || interval.is_zero())) {
if (use_dwm && GetDwmVBlankTimestamp(&timestamp)) {
// Timestamp comes from DwmGetCompositionTimingInfo and apparently it might
// be up to 2-3 vsync cycles in the past or in the future.
// The adjustment formula was suggested here:
// http://www.vsynctester.com/firefoxisbroken.html
base::TimeDelta interval = GetAverageInterval();
adjustment =
((now - timestamp + interval / 8) % interval + interval) % interval -
interval / 8;
timestamp = now - adjustment;
} else {
// DWM must be disabled.
// Not using DWM.
timestamp = now;
}

AddTimestamp(timestamp);

TRACE_EVENT1("gpu", "GpuVSyncWorker::SendGpuVSyncUpdate", "adjustment",
adjustment.ToInternalValue());

InvokeCallbackAndReschedule(timestamp, interval);
DCHECK_GT(GetAverageInterval().InMillisecondsF(), 0);
InvokeCallbackAndReschedule(timestamp, GetAverageInterval());
}

void GpuVSyncWorker::InvokeCallbackAndReschedule(base::TimeTicks timestamp,
Expand All @@ -261,6 +380,8 @@ void GpuVSyncWorker::InvokeCallbackAndReschedule(base::TimeTicks timestamp,
base::Unretained(this)));
} else {
running_ = false;
// Clear last_timestamp_ to avoid a long interval when the worker restarts.
last_timestamp_ = base::TimeTicks();
}
}

Expand All @@ -269,18 +390,11 @@ void GpuVSyncWorker::UseDelayBasedVSyncOnError() {
// Use timer based mechanism as a backup for one v-sync cycle, start with
// getting VSync parameters to determine timebase and interval.
// 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::TimeDelta interval) {
// 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::TimeTicks timebase;
GetDwmVBlankTimestamp(&timebase);

base::TimeDelta interval = GetAverageInterval();
base::TimeTicks now = base::TimeTicks::Now();
base::TimeTicks next_vsync = now.SnappedToNextTick(timebase, interval);

Expand Down Expand Up @@ -327,6 +441,8 @@ void GpuVSyncWorker::CloseAdapter() {

current_adapter_handle_ = 0;
current_device_name_.clear();

ClearIntervalHistory();
}
}

Expand Down

0 comments on commit 873b91f

Please sign in to comment.