Re-initialize the DXGI capturer when the DPI of the monitor changes

I am updating Chrome Remote Desktop to apply a scale factor when using
curtain mode (i.e. a loopback RDP session) and I've found that while
the changes are applied and the desktop is scaled, DXGI stops
producing frames.

This is essentially the same issue as crbug.com/1307357 except this
issue is occurring when the DPI is changed rather than the desktop
size.

The fix is to look at the effective DPI for the source being
captured (or the primary monitor when capturing the full desktop)
and then signaling an environment change when the DPI differs.

Bug: webrtc:14894,b:154733991
Change-Id: Id768d4a384434ba59e7396bc919d0ba30d0f6acc
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/292791
Reviewed-by: Alexander Cooper <alcooper@chromium.org>
Reviewed-by: Mirko Bonadei <mbonadei@webrtc.org>
Commit-Queue: Joe Downing <joedow@google.com>
Cr-Commit-Position: refs/heads/main@{#39305}
diff --git a/modules/desktop_capture/BUILD.gn b/modules/desktop_capture/BUILD.gn
index 90edfa4..6f3e9d2 100644
--- a/modules/desktop_capture/BUILD.gn
+++ b/modules/desktop_capture/BUILD.gn
@@ -516,9 +516,11 @@
     libs = [
       "d3d11.lib",
       "dxgi.lib",
+      "shcore.lib",
     ]
     deps += [
       "../../rtc_base:win32",
+      "../../rtc_base/containers:flat_map",
       "../../rtc_base/win:create_direct3d_device",
       "../../rtc_base/win:get_activation_factory",
       "../../rtc_base/win:windows_version",
diff --git a/modules/desktop_capture/win/display_configuration_monitor.cc b/modules/desktop_capture/win/display_configuration_monitor.cc
index 52d8921..6bc9269 100644
--- a/modules/desktop_capture/win/display_configuration_monitor.cc
+++ b/modules/desktop_capture/win/display_configuration_monitor.cc
@@ -10,28 +10,57 @@
 
 #include "modules/desktop_capture/win/display_configuration_monitor.h"
 
+#include <windows.h>
+
 #include "modules/desktop_capture/win/screen_capture_utils.h"
+#include "rtc_base/logging.h"
 
 namespace webrtc {
 
-bool DisplayConfigurationMonitor::IsChanged() {
+bool DisplayConfigurationMonitor::IsChanged(
+    DesktopCapturer::SourceId source_id) {
   DesktopRect rect = GetFullscreenRect();
+  DesktopVector dpi = GetDpiForSourceId(source_id);
+
   if (!initialized_) {
     initialized_ = true;
     rect_ = rect;
+    source_dpis_.emplace(source_id, std::move(dpi));
     return false;
   }
 
-  if (rect.equals(rect_)) {
-    return false;
+  if (!source_dpis_.contains(source_id)) {
+    // If this is the first time we've seen this source_id, use the current DPI
+    // so the monitor does not indicate a change and possibly get reset.
+    source_dpis_.emplace(source_id, dpi);
   }
 
-  rect_ = rect;
-  return true;
+  bool has_changed = false;
+  if (!rect.equals(rect_) || !source_dpis_.at(source_id).equals(dpi)) {
+    has_changed = true;
+    rect_ = rect;
+    source_dpis_.emplace(source_id, std::move(dpi));
+  }
+
+  return has_changed;
 }
 
 void DisplayConfigurationMonitor::Reset() {
   initialized_ = false;
+  source_dpis_.clear();
+  rect_ = {};
+}
+
+DesktopVector DisplayConfigurationMonitor::GetDpiForSourceId(
+    DesktopCapturer::SourceId source_id) {
+  HMONITOR monitor = 0;
+  if (source_id == kFullDesktopScreenId) {
+    // Get a handle to the primary monitor when capturing the full desktop.
+    monitor = MonitorFromPoint({0, 0}, MONITOR_DEFAULTTOPRIMARY);
+  } else if (!GetHmonitorFromDeviceIndex(source_id, &monitor)) {
+    RTC_LOG(LS_WARNING) << "GetHmonitorFromDeviceIndex failed.";
+  }
+  return GetDpiForMonitor(monitor);
 }
 
 }  // namespace webrtc
diff --git a/modules/desktop_capture/win/display_configuration_monitor.h b/modules/desktop_capture/win/display_configuration_monitor.h
index 39c211c..90dfd5c 100644
--- a/modules/desktop_capture/win/display_configuration_monitor.h
+++ b/modules/desktop_capture/win/display_configuration_monitor.h
@@ -11,7 +11,9 @@
 #ifndef MODULES_DESKTOP_CAPTURE_WIN_DISPLAY_CONFIGURATION_MONITOR_H_
 #define MODULES_DESKTOP_CAPTURE_WIN_DISPLAY_CONFIGURATION_MONITOR_H_
 
+#include "modules/desktop_capture/desktop_capturer.h"
 #include "modules/desktop_capture/desktop_geometry.h"
+#include "rtc_base/containers/flat_map.h"
 
 namespace webrtc {
 
@@ -20,16 +22,29 @@
 // TODO(zijiehe): Also check for pixel format changes.
 class DisplayConfigurationMonitor {
  public:
-  // Checks whether the change of display configuration has happened after last
-  // IsChanged() call. This function won't return true for the first time after
-  // constructor or Reset() call.
-  bool IsChanged();
+  // Checks whether the display configuration has changed since the last time
+  // IsChanged() was called. |source_id| is used to observe changes for a
+  // specific display or all displays if kFullDesktopScreenId is passed in.
+  // Returns false if object was Reset() or if IsChanged() has not been called.
+  bool IsChanged(DesktopCapturer::SourceId source_id);
 
   // Resets to the initial state.
   void Reset();
 
  private:
+  DesktopVector GetDpiForSourceId(DesktopCapturer::SourceId source_id);
+
+  // Represents the size of the desktop which includes all displays.
   DesktopRect rect_;
+
+  // Tracks the DPI for each display being captured. We need to track for each
+  // display as each one can be configured to use a different DPI which will not
+  // be reflected in calls to get the system DPI.
+  flat_map<DesktopCapturer::SourceId, DesktopVector> source_dpis_;
+
+  // Indicates whether |rect_| and |source_dpis_| have been initialized. This is
+  // used to prevent the monitor instance from signaling 'IsChanged()' before
+  // the initial values have been set.
   bool initialized_ = false;
 };
 
diff --git a/modules/desktop_capture/win/dxgi_duplicator_controller.cc b/modules/desktop_capture/win/dxgi_duplicator_controller.cc
index a776896..973aa3f 100644
--- a/modules/desktop_capture/win/dxgi_duplicator_controller.cc
+++ b/modules/desktop_capture/win/dxgi_duplicator_controller.cc
@@ -129,10 +129,10 @@
   return DoDuplicate(frame, monitor_id);
 }
 
-DesktopVector DxgiDuplicatorController::dpi() {
+DesktopVector DxgiDuplicatorController::system_dpi() {
   MutexLock lock(&mutex_);
   if (Initialize()) {
-    return dpi_;
+    return system_dpi_;
   }
   return DesktopVector();
 }
@@ -174,7 +174,7 @@
   // TODO(zijiehe): Confirm whether IDXGIOutput::GetDesc() and
   // IDXGIOutputDuplication::GetDesc() can detect the resolution change without
   // reinitialization.
-  if (display_configuration_monitor_.IsChanged()) {
+  if (display_configuration_monitor_.IsChanged(frame->source_id_)) {
     Deinitialize();
   }
 
@@ -286,7 +286,8 @@
   HDC hdc = GetDC(nullptr);
   // Use old DPI value if failed.
   if (hdc) {
-    dpi_.set(GetDeviceCaps(hdc, LOGPIXELSX), GetDeviceCaps(hdc, LOGPIXELSY));
+    system_dpi_.set(GetDeviceCaps(hdc, LOGPIXELSX),
+                    GetDeviceCaps(hdc, LOGPIXELSY));
     ReleaseDC(nullptr, hdc);
   }
 
@@ -343,7 +344,7 @@
   }
 
   if (result) {
-    target->set_dpi(dpi_);
+    target->set_dpi(system_dpi_);
     return true;
   }
 
diff --git a/modules/desktop_capture/win/dxgi_duplicator_controller.h b/modules/desktop_capture/win/dxgi_duplicator_controller.h
index 97ed2d8..2b1e0ab 100644
--- a/modules/desktop_capture/win/dxgi_duplicator_controller.h
+++ b/modules/desktop_capture/win/dxgi_duplicator_controller.h
@@ -41,7 +41,7 @@
 // but a later Duplicate() returns false, this usually means the display mode is
 // changing. Consumers should retry after a while. (Typically 50 milliseconds,
 // but according to hardware performance, this time may vary.)
-// The underyling DxgiOutputDuplicators may take an additional reference on the
+// The underlying DxgiOutputDuplicators may take an additional reference on the
 // frame passed in to the Duplicate methods so that they can guarantee delivery
 // of new frames when requested; since if there have been no updates to the
 // surface, they may be unable to capture a frame.
@@ -49,7 +49,7 @@
  public:
   using Context = DxgiFrameContext;
 
-  // A collection of D3d information we are interested on, which may impact
+  // A collection of D3d information we are interested in, which may impact
   // capturer performance or reliability.
   struct D3dInfo {
     // Each video adapter has its own D3D_FEATURE_LEVEL, so this structure
@@ -105,7 +105,7 @@
   // synchronize them manually. We should find a way to do it.
   Result Duplicate(DxgiFrame* frame);
 
-  // Captures one monitor and writes into target. `monitor_id` should >= 0. If
+  // Captures one monitor and writes into target. `monitor_id` must be >= 0. If
   // `monitor_id` is greater than the total screen count of all the Duplicators,
   // this function returns false. May retain a reference to `frame`'s underlying
   // |SharedDesktopFrame|.
@@ -113,7 +113,7 @@
 
   // Returns dpi of current system. Returns an empty DesktopVector if system
   // does not support DXGI based capturer.
-  DesktopVector dpi();
+  DesktopVector system_dpi();
 
   // Returns the count of screens on the system. These screens can be retrieved
   // by an integer in the range of [0, ScreenCount()). If system does not
@@ -172,7 +172,7 @@
   // Initialize().
   bool DoInitialize() RTC_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
 
-  // Clears all COM components referred by this instance. So next Duplicate()
+  // Clears all COM components referred to by this instance. So next Duplicate()
   // call will eventually initialize this instance again.
   void Deinitialize() RTC_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
 
@@ -243,12 +243,12 @@
   // a Context instance is always initialized after DxgiDuplicatorController.
   int identity_ RTC_GUARDED_BY(mutex_) = 0;
   DesktopRect desktop_rect_ RTC_GUARDED_BY(mutex_);
-  DesktopVector dpi_ RTC_GUARDED_BY(mutex_);
+  DesktopVector system_dpi_ RTC_GUARDED_BY(mutex_);
   std::vector<DxgiAdapterDuplicator> duplicators_ RTC_GUARDED_BY(mutex_);
   D3dInfo d3d_info_ RTC_GUARDED_BY(mutex_);
   DisplayConfigurationMonitor display_configuration_monitor_
       RTC_GUARDED_BY(mutex_);
-  // A number to indicate how many succeeded duplications have been performed.
+  // A number to indicate how many successful duplications have been performed.
   uint32_t succeeded_duplications_ RTC_GUARDED_BY(mutex_) = 0;
 };
 
diff --git a/modules/desktop_capture/win/screen_capture_utils.cc b/modules/desktop_capture/win/screen_capture_utils.cc
index 3d4aecf..3745e9c 100644
--- a/modules/desktop_capture/win/screen_capture_utils.cc
+++ b/modules/desktop_capture/win/screen_capture_utils.cc
@@ -10,6 +10,7 @@
 
 #include "modules/desktop_capture/win/screen_capture_utils.h"
 
+#include <shellscalingapi.h>
 #include <windows.h>
 
 #include <string>
@@ -145,6 +146,28 @@
                                GetSystemMetrics(SM_CYVIRTUALSCREEN));
 }
 
+DesktopVector GetDpiForMonitor(HMONITOR monitor) {
+  UINT dpi_x, dpi_y;
+  // MDT_EFFECTIVE_DPI includes the scale factor as well as the system DPI.
+  HRESULT hr = ::GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &dpi_x, &dpi_y);
+  if (SUCCEEDED(hr)) {
+    return {static_cast<INT>(dpi_x), static_cast<INT>(dpi_y)};
+  }
+  RTC_LOG_GLE_EX(LS_WARNING, hr) << "GetDpiForMonitor() failed";
+
+  // If we can't get the per-monitor DPI, then return the system DPI.
+  HDC hdc = GetDC(nullptr);
+  if (hdc) {
+    DesktopVector dpi{GetDeviceCaps(hdc, LOGPIXELSX),
+                      GetDeviceCaps(hdc, LOGPIXELSY)};
+    ReleaseDC(nullptr, hdc);
+    return dpi;
+  }
+
+  // If everything fails, then return the default DPI for Windows.
+  return {96, 96};
+}
+
 DesktopRect GetScreenRect(const DesktopCapturer::SourceId screen,
                           const std::wstring& device_key) {
   if (screen == kFullDesktopScreenId) {
diff --git a/modules/desktop_capture/win/screen_capture_utils.h b/modules/desktop_capture/win/screen_capture_utils.h
index 97bfe81..9aa838a 100644
--- a/modules/desktop_capture/win/screen_capture_utils.h
+++ b/modules/desktop_capture/win/screen_capture_utils.h
@@ -54,6 +54,10 @@
 // primary display's top-left. On failure, returns an empty rect.
 DesktopRect GetMonitorRect(HMONITOR monitor);
 
+// Returns the DPI for the specified monitor. On failure, returns the system DPI
+// or the Windows default DPI (96x96) if the system DPI can't be retrieved.
+DesktopVector GetDpiForMonitor(HMONITOR monitor);
+
 // Returns true if `screen` is a valid screen. The screen device key is
 // returned through `device_key` if the screen is valid. The device key can be
 // used in GetScreenRect to verify the screen matches the previously obtained
diff --git a/modules/desktop_capture/win/screen_capturer_win_gdi.cc b/modules/desktop_capture/win/screen_capturer_win_gdi.cc
index 57b1f71..4d07b6b 100644
--- a/modules/desktop_capture/win/screen_capturer_win_gdi.cc
+++ b/modules/desktop_capture/win/screen_capturer_win_gdi.cc
@@ -160,7 +160,7 @@
   }
 
   // If the display configurations have changed then recreate GDI resources.
-  if (display_configuration_monitor_.IsChanged()) {
+  if (display_configuration_monitor_.IsChanged(kFullDesktopScreenId)) {
     if (desktop_dc_) {
       ReleaseDC(NULL, desktop_dc_);
       desktop_dc_ = nullptr;