dxgi_output_duplicator.cc (17410B)
1 /* 2 * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 3 * 4 * Use of this source code is governed by a BSD-style license 5 * that can be found in the LICENSE file in the root of the source 6 * tree. An additional intellectual property rights grant can be found 7 * in the file PATENTS. All contributing project authors may 8 * be found in the AUTHORS file in the root of the source tree. 9 */ 10 11 #include "modules/desktop_capture/win/dxgi_output_duplicator.h" 12 13 #include <dxgi.h> 14 #include <dxgiformat.h> 15 #include <unknwn.h> 16 #include <windows.h> 17 18 #include <algorithm> 19 #include <cstdint> 20 #include <cstring> 21 #include <optional> 22 23 #include "modules/desktop_capture/desktop_frame.h" 24 #include "modules/desktop_capture/desktop_frame_rotation.h" 25 #include "modules/desktop_capture/desktop_geometry.h" 26 #include "modules/desktop_capture/desktop_region.h" 27 #include "modules/desktop_capture/shared_desktop_frame.h" 28 #include "modules/desktop_capture/win/d3d_device.h" 29 #include "modules/desktop_capture/win/desktop_capture_utils.h" 30 #include "modules/desktop_capture/win/dxgi_texture_mapping.h" 31 #include "modules/desktop_capture/win/dxgi_texture_staging.h" 32 #include "rtc_base/checks.h" 33 #include "rtc_base/logging.h" 34 #include "rtc_base/string_utils.h" 35 #include "rtc_base/win32.h" 36 #include "system_wrappers/include/metrics.h" 37 38 namespace webrtc { 39 40 using Microsoft::WRL::ComPtr; 41 42 namespace { 43 44 // Timeout for AcquireNextFrame() call. 45 // DxgiDuplicatorController leverages external components to do the capture 46 // scheduling. So here DxgiOutputDuplicator does not need to actively wait for a 47 // new frame. 48 const int kAcquireTimeoutMs = 0; 49 50 DesktopRect RECTToDesktopRect(const RECT& rect) { 51 return DesktopRect::MakeLTRB(rect.left, rect.top, rect.right, rect.bottom); 52 } 53 54 Rotation DxgiRotationToRotation(DXGI_MODE_ROTATION rotation) { 55 switch (rotation) { 56 case DXGI_MODE_ROTATION_IDENTITY: 57 case DXGI_MODE_ROTATION_UNSPECIFIED: 58 return Rotation::CLOCK_WISE_0; 59 case DXGI_MODE_ROTATION_ROTATE90: 60 return Rotation::CLOCK_WISE_90; 61 case DXGI_MODE_ROTATION_ROTATE180: 62 return Rotation::CLOCK_WISE_180; 63 case DXGI_MODE_ROTATION_ROTATE270: 64 return Rotation::CLOCK_WISE_270; 65 } 66 67 RTC_DCHECK_NOTREACHED(); 68 return Rotation::CLOCK_WISE_0; 69 } 70 71 } // namespace 72 73 DxgiOutputDuplicator::DxgiOutputDuplicator(const D3dDevice& device, 74 const ComPtr<IDXGIOutput1>& output, 75 const DXGI_OUTPUT_DESC& desc) 76 : device_(device), 77 output_(output), 78 device_name_(ToUtf8(desc.DeviceName)), 79 desktop_rect_(RECTToDesktopRect(desc.DesktopCoordinates)), 80 monitor_(desc.Monitor) { 81 RTC_DCHECK(output_); 82 RTC_DCHECK(!desktop_rect_.is_empty()); 83 RTC_DCHECK_GT(desktop_rect_.width(), 0); 84 RTC_DCHECK_GT(desktop_rect_.height(), 0); 85 } 86 87 DxgiOutputDuplicator::DxgiOutputDuplicator(DxgiOutputDuplicator&& other) = 88 default; 89 90 DxgiOutputDuplicator::~DxgiOutputDuplicator() { 91 if (duplication_) { 92 duplication_->ReleaseFrame(); 93 } 94 texture_.reset(); 95 } 96 97 bool DxgiOutputDuplicator::Initialize() { 98 if (DuplicateOutput()) { 99 if (desc_.DesktopImageInSystemMemory) { 100 texture_.reset(new DxgiTextureMapping(duplication_.Get())); 101 } else { 102 texture_.reset(new DxgiTextureStaging(device_)); 103 } 104 return true; 105 } else { 106 duplication_.Reset(); 107 return false; 108 } 109 } 110 111 bool DxgiOutputDuplicator::DuplicateOutput() { 112 RTC_DCHECK(!duplication_); 113 _com_error error = 114 output_->DuplicateOutput(static_cast<IUnknown*>(device_.d3d_device()), 115 duplication_.GetAddressOf()); 116 if (error.Error() != S_OK || !duplication_) { 117 RTC_LOG(LS_WARNING) << "Failed to duplicate output from IDXGIOutput1: " 118 << desktop_capture::utils::ComErrorToString(error); 119 return false; 120 } 121 122 memset(&desc_, 0, sizeof(desc_)); 123 duplication_->GetDesc(&desc_); 124 125 // DXGI_FORMAT_R16G16B16A16_FLOAT is returned for HDR monitor, 126 // DXGI_FORMAT_B8G8R8A8_UNORM for others. 127 if ((desc_.ModeDesc.Format != DXGI_FORMAT_B8G8R8A8_UNORM) && 128 (desc_.ModeDesc.Format != DXGI_FORMAT_R16G16B16A16_FLOAT)) { 129 RTC_LOG(LS_ERROR) << "IDXGIDuplicateOutput does not use RGBA (8, 16 bit)" 130 << "which is required by downstream components" 131 << "format is " << desc_.ModeDesc.Format; 132 return false; 133 } 134 135 if (static_cast<int>(desc_.ModeDesc.Width) != desktop_rect_.width() || 136 static_cast<int>(desc_.ModeDesc.Height) != desktop_rect_.height()) { 137 RTC_LOG(LS_ERROR) 138 << "IDXGIDuplicateOutput does not return a same size as its " 139 << "IDXGIOutput1, size returned by IDXGIDuplicateOutput is " 140 << desc_.ModeDesc.Width << " x " << desc_.ModeDesc.Height 141 << ", size returned by IDXGIOutput1 is " << desktop_rect_.width() 142 << " x " << desktop_rect_.height(); 143 return false; 144 } 145 146 rotation_ = DxgiRotationToRotation(desc_.Rotation); 147 unrotated_size_ = RotateSize(desktop_size(), ReverseRotation(rotation_)); 148 149 return true; 150 } 151 152 bool DxgiOutputDuplicator::ReleaseFrame() { 153 RTC_DCHECK(duplication_); 154 _com_error error = duplication_->ReleaseFrame(); 155 if (error.Error() != S_OK) { 156 RTC_LOG(LS_ERROR) << "Failed to release frame from IDXGIOutputDuplication: " 157 << desktop_capture::utils::ComErrorToString(error); 158 return false; 159 } 160 return true; 161 } 162 163 bool DxgiOutputDuplicator::ContainsMouseCursor( 164 const DXGI_OUTDUPL_FRAME_INFO& frame_info) { 165 // The DXGI_OUTDUPL_POINTER_POSITION structure that describes the most recent 166 // mouse position is only valid if the LastMouseUpdateTime member is a non- 167 // zero value. 168 if (frame_info.LastMouseUpdateTime.QuadPart == 0) 169 return false; 170 171 // Ignore cases when the mouse shape has changed and not the position. 172 const bool new_pointer_shape = (frame_info.PointerShapeBufferSize != 0); 173 if (new_pointer_shape) 174 return false; 175 176 // The mouse cursor has moved and we can now query if the mouse pointer is 177 // drawn onto the desktop image or not to decide if we must draw the mouse 178 // pointer shape onto the desktop image (always done by default currently). 179 // Either the mouse pointer is already drawn onto the desktop image that 180 // IDXGIOutputDuplication::AcquireNextFrame provides or the mouse pointer is 181 // separate from the desktop image. If the mouse pointer is drawn onto the 182 // desktop image, the pointer position data that is reported by 183 // AcquireNextFrame indicates that a separate pointer isn’t visible, hence 184 // `frame_info.PointerPosition.Visible` is false. 185 const bool cursor_embedded_in_frame = !frame_info.PointerPosition.Visible; 186 RTC_HISTOGRAM_BOOLEAN("WebRTC.DesktopCapture.Win.DirectXCursorEmbedded", 187 cursor_embedded_in_frame); 188 return cursor_embedded_in_frame; 189 } 190 191 bool DxgiOutputDuplicator::Duplicate(Context* context, 192 DesktopVector offset, 193 SharedDesktopFrame* target) { 194 RTC_DCHECK(duplication_); 195 RTC_DCHECK(texture_); 196 RTC_DCHECK(target); 197 if (!DesktopRect::MakeSize(target->size()) 198 .ContainsRect(GetTranslatedDesktopRect(offset))) { 199 // target size is not large enough to cover current output region. 200 return false; 201 } 202 203 DXGI_OUTDUPL_FRAME_INFO frame_info; 204 memset(&frame_info, 0, sizeof(frame_info)); 205 ComPtr<IDXGIResource> resource; 206 _com_error error = duplication_->AcquireNextFrame( 207 kAcquireTimeoutMs, &frame_info, resource.GetAddressOf()); 208 if (error.Error() != S_OK && error.Error() != DXGI_ERROR_WAIT_TIMEOUT) { 209 RTC_LOG(LS_ERROR) << "Failed to capture frame: " 210 << desktop_capture::utils::ComErrorToString(error); 211 return false; 212 } 213 214 const bool cursor_embedded_in_frame = ContainsMouseCursor(frame_info); 215 216 // We need to merge updated region with the one from context, but only spread 217 // updated region from current frame. So keeps a copy of updated region from 218 // context here. The `updated_region` always starts from (0, 0). 219 DesktopRegion updated_region; 220 updated_region.Swap(&context->updated_region); 221 if (error.Error() == S_OK && frame_info.AccumulatedFrames > 0 && resource) { 222 DetectUpdatedRegion(frame_info, &context->updated_region); 223 SpreadContextChange(context); 224 if (!texture_->CopyFrom(frame_info, resource.Get())) { 225 return false; 226 } 227 updated_region.AddRegion(context->updated_region); 228 // TODO(zijiehe): Figure out why clearing context->updated_region() here 229 // triggers screen flickering? 230 231 const DesktopFrame& source = texture_->AsDesktopFrame(); 232 if (rotation_ != Rotation::CLOCK_WISE_0) { 233 for (DesktopRegion::Iterator it(updated_region); !it.IsAtEnd(); 234 it.Advance()) { 235 // The `updated_region` returned by Windows is rotated, but the `source` 236 // frame is not. So we need to rotate it reversely. 237 const DesktopRect source_rect = 238 RotateRect(it.rect(), desktop_size(), ReverseRotation(rotation_)); 239 RotateDesktopFrame(source, source_rect, rotation_, offset, target); 240 } 241 } else { 242 for (DesktopRegion::Iterator it(updated_region); !it.IsAtEnd(); 243 it.Advance()) { 244 // The DesktopRect in `target`, starts from offset. 245 DesktopRect dest_rect = it.rect(); 246 dest_rect.Translate(offset); 247 target->CopyPixelsFrom(source, it.rect().top_left(), dest_rect); 248 } 249 } 250 last_frame_ = target->Share(); 251 last_frame_offset_ = offset; 252 updated_region.Translate(offset.x(), offset.y()); 253 target->mutable_updated_region()->AddRegion(updated_region); 254 target->set_may_contain_cursor(cursor_embedded_in_frame); 255 num_frames_captured_++; 256 return texture_->Release() && ReleaseFrame(); 257 } 258 259 if (last_frame_) { 260 // No change since last frame or AcquireNextFrame() timed out, we will 261 // export last frame to the target. 262 for (DesktopRegion::Iterator it(updated_region); !it.IsAtEnd(); 263 it.Advance()) { 264 // The DesktopRect in `source`, starts from last_frame_offset_. 265 DesktopRect source_rect = it.rect(); 266 // The DesktopRect in `target`, starts from offset. 267 DesktopRect target_rect = source_rect; 268 source_rect.Translate(last_frame_offset_); 269 target_rect.Translate(offset); 270 target->CopyPixelsFrom(*last_frame_, source_rect.top_left(), target_rect); 271 } 272 updated_region.Translate(offset.x(), offset.y()); 273 target->mutable_updated_region()->AddRegion(updated_region); 274 target->set_may_contain_cursor(cursor_embedded_in_frame); 275 } else { 276 // If we were at the very first frame, and capturing failed, the 277 // context->updated_region should be kept unchanged for next attempt. 278 context->updated_region.Swap(&updated_region); 279 } 280 // If AcquireNextFrame() failed with timeout error, we do not need to release 281 // the frame. 282 return error.Error() == DXGI_ERROR_WAIT_TIMEOUT || ReleaseFrame(); 283 } 284 285 DesktopRect DxgiOutputDuplicator::GetTranslatedDesktopRect( 286 DesktopVector offset) const { 287 DesktopRect result(DesktopRect::MakeSize(desktop_size())); 288 result.Translate(offset); 289 return result; 290 } 291 292 DesktopRect DxgiOutputDuplicator::GetUntranslatedDesktopRect() const { 293 return DesktopRect::MakeSize(desktop_size()); 294 } 295 296 void DxgiOutputDuplicator::DetectUpdatedRegion( 297 const DXGI_OUTDUPL_FRAME_INFO& frame_info, 298 DesktopRegion* updated_region) { 299 if (DoDetectUpdatedRegion(frame_info, updated_region)) { 300 // Make sure even a region returned by Windows API is out of the scope of 301 // desktop_rect_, we still won't export it to the target DesktopFrame. 302 updated_region->IntersectWith(GetUntranslatedDesktopRect()); 303 } else { 304 updated_region->SetRect(GetUntranslatedDesktopRect()); 305 } 306 } 307 308 bool DxgiOutputDuplicator::DoDetectUpdatedRegion( 309 const DXGI_OUTDUPL_FRAME_INFO& frame_info, 310 DesktopRegion* updated_region) { 311 RTC_DCHECK(updated_region); 312 updated_region->Clear(); 313 if (frame_info.TotalMetadataBufferSize == 0) { 314 // This should not happen, since frame_info.AccumulatedFrames > 0. 315 RTC_LOG(LS_ERROR) << "frame_info.AccumulatedFrames > 0, " 316 << "but TotalMetadataBufferSize == 0"; 317 return false; 318 } 319 320 if (metadata_.size() < frame_info.TotalMetadataBufferSize) { 321 metadata_.clear(); // Avoid data copy 322 metadata_.resize(frame_info.TotalMetadataBufferSize); 323 } 324 325 UINT buff_size = 0; 326 DXGI_OUTDUPL_MOVE_RECT* move_rects = 327 reinterpret_cast<DXGI_OUTDUPL_MOVE_RECT*>(metadata_.data()); 328 size_t move_rects_count = 0; 329 _com_error error = duplication_->GetFrameMoveRects( 330 static_cast<UINT>(metadata_.size()), move_rects, &buff_size); 331 if (error.Error() != S_OK) { 332 RTC_LOG(LS_ERROR) << "Failed to get move rectangles: " 333 << desktop_capture::utils::ComErrorToString(error); 334 return false; 335 } 336 move_rects_count = buff_size / sizeof(DXGI_OUTDUPL_MOVE_RECT); 337 338 RECT* dirty_rects = reinterpret_cast<RECT*>(metadata_.data() + buff_size); 339 size_t dirty_rects_count = 0; 340 error = duplication_->GetFrameDirtyRects( 341 static_cast<UINT>(metadata_.size()) - buff_size, dirty_rects, &buff_size); 342 if (error.Error() != S_OK) { 343 RTC_LOG(LS_ERROR) << "Failed to get dirty rectangles: " 344 << desktop_capture::utils::ComErrorToString(error); 345 return false; 346 } 347 dirty_rects_count = buff_size / sizeof(RECT); 348 349 while (move_rects_count > 0) { 350 // DirectX capturer API may randomly return unmoved move_rects, which should 351 // be skipped to avoid unnecessary wasting of differing and encoding 352 // resources. 353 // By using testing application it2me_standalone_host_main, this check 354 // reduces average capture time by 0.375% (4.07 -> 4.055), and average 355 // encode time by 0.313% (8.042 -> 8.016) without other impacts. 356 if (move_rects->SourcePoint.x != move_rects->DestinationRect.left || 357 move_rects->SourcePoint.y != move_rects->DestinationRect.top) { 358 updated_region->AddRect( 359 RotateRect(DesktopRect::MakeXYWH(move_rects->SourcePoint.x, 360 move_rects->SourcePoint.y, 361 move_rects->DestinationRect.right - 362 move_rects->DestinationRect.left, 363 move_rects->DestinationRect.bottom - 364 move_rects->DestinationRect.top), 365 unrotated_size_, rotation_)); 366 updated_region->AddRect( 367 RotateRect(DesktopRect::MakeLTRB(move_rects->DestinationRect.left, 368 move_rects->DestinationRect.top, 369 move_rects->DestinationRect.right, 370 move_rects->DestinationRect.bottom), 371 unrotated_size_, rotation_)); 372 } else { 373 RTC_LOG(LS_INFO) << "Unmoved move_rect detected, [" 374 << move_rects->DestinationRect.left << ", " 375 << move_rects->DestinationRect.top << "] - [" 376 << move_rects->DestinationRect.right << ", " 377 << move_rects->DestinationRect.bottom << "]."; 378 } 379 move_rects++; 380 move_rects_count--; 381 } 382 383 while (dirty_rects_count > 0) { 384 updated_region->AddRect(RotateRect( 385 DesktopRect::MakeLTRB(dirty_rects->left, dirty_rects->top, 386 dirty_rects->right, dirty_rects->bottom), 387 unrotated_size_, rotation_)); 388 dirty_rects++; 389 dirty_rects_count--; 390 } 391 392 return true; 393 } 394 395 void DxgiOutputDuplicator::Setup(Context* context) { 396 RTC_DCHECK(context->updated_region.is_empty()); 397 // Always copy entire monitor during the first Duplicate() function call. 398 context->updated_region.AddRect(GetUntranslatedDesktopRect()); 399 RTC_DCHECK(std::find(contexts_.begin(), contexts_.end(), context) == 400 contexts_.end()); 401 contexts_.push_back(context); 402 } 403 404 void DxgiOutputDuplicator::Unregister(const Context* const context) { 405 auto it = std::find(contexts_.begin(), contexts_.end(), context); 406 RTC_DCHECK(it != contexts_.end()); 407 contexts_.erase(it); 408 } 409 410 void DxgiOutputDuplicator::SpreadContextChange(const Context* const source) { 411 for (Context* dest : contexts_) { 412 RTC_DCHECK(dest); 413 if (dest != source) { 414 dest->updated_region.AddRegion(source->updated_region); 415 } 416 } 417 } 418 419 DesktopSize DxgiOutputDuplicator::desktop_size() const { 420 return desktop_rect_.size(); 421 } 422 423 int64_t DxgiOutputDuplicator::num_frames_captured() const { 424 #if !defined(NDEBUG) 425 RTC_DCHECK_EQ(!!last_frame_, num_frames_captured_ > 0); 426 #endif 427 return num_frames_captured_; 428 } 429 430 std::optional<float> DxgiOutputDuplicator::device_scale_factor() const { 431 DEVICE_SCALE_FACTOR device_scale_factor = DEVICE_SCALE_FACTOR_INVALID; 432 HRESULT hr = GetScaleFactorForMonitor(monitor_, &device_scale_factor); 433 if (FAILED(hr)) { 434 RTC_LOG(LS_ERROR) << "Failed to get scale factor for monitor: " << hr; 435 return std::nullopt; 436 } 437 RTC_DCHECK(device_scale_factor != DEVICE_SCALE_FACTOR_INVALID); 438 return static_cast<float>(device_scale_factor) / 100.0f; 439 } 440 441 void DxgiOutputDuplicator::TranslateRect(const DesktopVector& position) { 442 desktop_rect_.Translate(position); 443 RTC_DCHECK_GE(desktop_rect_.left(), 0); 444 RTC_DCHECK_GE(desktop_rect_.top(), 0); 445 } 446 447 } // namespace webrtc