multimedia/client/qt_gl_/yuvgl/media/DXGICapture.cpp

684 lines
18 KiB
C++
Raw Normal View History

2021-10-04 14:45:36 +00:00
/*****************************************************************************
* DXGICapture.cpp
*
* Copyright (C) 2020 Gokhan Erdogdu <gokhan_erdogdu - at - yahoo - dot - com>
*
* DXGICapture is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* DXGICapture is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*
******************************************************************************/
#include "DXGICapture.h"
#include "DXGICaptureHelper.h"
#include <chrono>
#pragma comment(lib, "D3D11.lib")
#pragma comment(lib, "d2d1.lib")
#pragma comment(lib, "windowscodecs.lib")
#pragma comment(lib, "shcore.lib") // SetProcessDpiAwareness
// Driver types supported
const D3D_DRIVER_TYPE g_DriverTypes[] =
{
D3D_DRIVER_TYPE_HARDWARE,
D3D_DRIVER_TYPE_WARP,
D3D_DRIVER_TYPE_REFERENCE,
};
const UINT g_NumDriverTypes = ARRAYSIZE(g_DriverTypes);
// Feature levels supported
const D3D_FEATURE_LEVEL g_FeatureLevels[] =
{
D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_10_0,
D3D_FEATURE_LEVEL_9_1
};
const UINT g_NumFeatureLevels = ARRAYSIZE(g_FeatureLevels);
#define AUTOLOCK() ATL::CComCritSecLock<ATL::CComAutoCriticalSection> auto_lock((ATL::CComAutoCriticalSection&)(m_csLock))
//
// class CDXGICapture
//
CDXGICapture::CDXGICapture()
: m_csLock()
, m_bInitialized(FALSE)
, m_lD3DFeatureLevel(D3D_FEATURE_LEVEL_INVALID)
{
RtlZeroMemory(&m_rendererInfo, sizeof(m_rendererInfo));
RtlZeroMemory(&m_mouseInfo, sizeof(m_mouseInfo));
RtlZeroMemory(&m_tempMouseBuffer, sizeof(m_tempMouseBuffer));
RtlZeroMemory(&m_desktopOutputDesc, sizeof(m_desktopOutputDesc));
}
CDXGICapture::~CDXGICapture()
{
this->Terminate();
}
HRESULT CDXGICapture::loadMonitorInfos(ID3D11Device *pDevice)
{
CHECK_POINTER(pDevice);
HRESULT hr = S_OK;
CComPtr<ID3D11Device> ipDevice(pDevice);
// Get DXGI device
CComPtr<IDXGIDevice> ipDxgiDevice;
hr = ipDevice->QueryInterface(IID_PPV_ARGS(&ipDxgiDevice));
if (FAILED(hr)) {
return hr;
}
// Get DXGI adapter
CComPtr<IDXGIAdapter> ipDxgiAdapter;
hr = ipDxgiDevice->GetParent(IID_PPV_ARGS(&ipDxgiAdapter));
if (FAILED(hr)) {
return hr;
}
ipDxgiDevice = nullptr;
CComPtr<IDXGIOutput> ipDxgiOutput;
for (UINT i = 0; SUCCEEDED(hr); ++i)
{
ipDxgiOutput = nullptr;
hr = ipDxgiAdapter->EnumOutputs(i, &ipDxgiOutput);
if ((nullptr != ipDxgiOutput) && (hr != DXGI_ERROR_NOT_FOUND))
{
DXGI_OUTPUT_DESC DesktopDesc;
hr = ipDxgiOutput->GetDesc(&DesktopDesc);
if (FAILED(hr)) {
continue;
}
tagDublicatorMonitorInfo *pInfo;
pInfo = new (std::nothrow) tagDublicatorMonitorInfo;
if (nullptr == pInfo) {
return E_OUTOFMEMORY;
}
hr = DXGICaptureHelper::ConvertDxgiOutputToMonitorInfo(&DesktopDesc, i, pInfo);
if (FAILED(hr)) {
delete pInfo;
continue;
}
m_monitorInfos.push_back(pInfo);
}
}
ipDxgiOutput = nullptr;
ipDxgiAdapter = nullptr;
return S_OK;
}
void CDXGICapture::freeMonitorInfos()
{
size_t nCount = m_monitorInfos.size();
if (nCount == 0) {
return;
}
DublicatorMonitorInfoVec::iterator it = m_monitorInfos.begin();
DublicatorMonitorInfoVec::iterator end = m_monitorInfos.end();
for (size_t i = 0; (i < nCount) && (it != end); i++, it++) {
tagDublicatorMonitorInfo *pInfo = *it;
if (nullptr != pInfo) {
delete pInfo;
}
}
m_monitorInfos.clear();
}
HRESULT CDXGICapture::createDeviceResource(
const tagScreenCaptureFilterConfig *pConfig,
const tagDublicatorMonitorInfo *pSelectedMonitorInfo
)
{
HRESULT hr = S_OK;
CComPtr<IDXGIOutputDuplication> ipDxgiOutputDuplication;
CComPtr<ID3D11Texture2D> ipCopyTexture2D;
CComPtr<ID2D1Device> ipD2D1Device;
CComPtr<ID2D1DeviceContext> ipD2D1DeviceContext;
CComPtr<ID2D1Factory> ipD2D1Factory;
CComPtr<IWICImagingFactory> ipWICImageFactory;
CComPtr<IWICBitmap> ipWICOutputBitmap;
CComPtr<ID2D1RenderTarget> ipD2D1RenderTarget;
DXGI_OUTPUT_DESC dgixOutputDesc;
tagRendererInfo rendererInfo;
RtlZeroMemory(&dgixOutputDesc, sizeof(dgixOutputDesc));
RtlZeroMemory(&rendererInfo, sizeof(rendererInfo));
// copy configuration to renderer info
rendererInfo.MonitorIdx = pConfig->MonitorIdx;
rendererInfo.ShowCursor = pConfig->ShowCursor;
rendererInfo.RotationMode = pConfig->RotationMode;
rendererInfo.SizeMode = pConfig->SizeMode;
rendererInfo.OutputSize = pConfig->OutputSize;
// default
rendererInfo.ScaleX = 1.0f;
rendererInfo.ScaleY = 1.0f;
do
{
// Get DXGI factory
CComPtr<IDXGIDevice> ipDxgiDevice;
hr = m_ipD3D11Device->QueryInterface(IID_PPV_ARGS(&ipDxgiDevice));
CHECK_HR_BREAK(hr);
CComPtr<IDXGIAdapter> ipDxgiAdapter;
hr = ipDxgiDevice->GetParent(IID_PPV_ARGS(&ipDxgiAdapter));
CHECK_HR_BREAK(hr);
// Get output
CComPtr<IDXGIOutput> ipDxgiOutput;
hr = ipDxgiAdapter->EnumOutputs(rendererInfo.MonitorIdx, &ipDxgiOutput);
CHECK_HR_BREAK(hr);
// Get output description
hr = ipDxgiOutput->GetDesc(&dgixOutputDesc);
CHECK_HR_BREAK(hr);
tagDublicatorMonitorInfo curMonInfo;
hr = DXGICaptureHelper::ConvertDxgiOutputToMonitorInfo(&dgixOutputDesc, rendererInfo.MonitorIdx, &curMonInfo);
CHECK_HR_BREAK(hr);
if (!DXGICaptureHelper::IsEqualMonitorInfo(pSelectedMonitorInfo, &curMonInfo)) {
hr = E_INVALIDARG; // Monitor settings have changed ???
break;
}
// QI for Output 1
CComPtr<IDXGIOutput1> ipDxgiOutput1;
hr = ipDxgiOutput->QueryInterface(IID_PPV_ARGS(&ipDxgiOutput1));
CHECK_HR_BREAK(hr);
// Create desktop duplication
hr = ipDxgiOutput1->DuplicateOutput(m_ipD3D11Device, &ipDxgiOutputDuplication);
CHECK_HR_BREAK(hr);
DXGI_OUTDUPL_DESC dxgiOutputDuplDesc;
ipDxgiOutputDuplication->GetDesc(&dxgiOutputDuplDesc);
hr = DXGICaptureHelper::CalculateRendererInfo(&dxgiOutputDuplDesc, &rendererInfo);
CHECK_HR_BREAK(hr);
// Create CPU access texture
D3D11_TEXTURE2D_DESC desc;
desc.Width = rendererInfo.SrcBounds.Width;
desc.Height = rendererInfo.SrcBounds.Height;
desc.Format = rendererInfo.SrcFormat;
desc.ArraySize = 1;
desc.BindFlags = 0;
desc.MiscFlags = 0;
desc.SampleDesc.Count = 1;
desc.SampleDesc.Quality = 0;
desc.MipLevels = 1;
desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ | D3D11_CPU_ACCESS_WRITE;
desc.Usage = D3D11_USAGE_STAGING;
hr = m_ipD3D11Device->CreateTexture2D(&desc, NULL, &ipCopyTexture2D);
CHECK_HR_BREAK(hr);
if (nullptr == ipCopyTexture2D)
{
hr = E_OUTOFMEMORY;
break;
}
#pragma region <For_2D_operations>
// Create D2D1 device
UINT uiFlags = m_ipD3D11Device->GetCreationFlags();
D2D1_CREATION_PROPERTIES d2d1Props = D2D1::CreationProperties
(
(uiFlags & D3D11_CREATE_DEVICE_SINGLETHREADED)
? D2D1_THREADING_MODE_SINGLE_THREADED
: D2D1_THREADING_MODE_MULTI_THREADED,
D2D1_DEBUG_LEVEL_NONE,
(uiFlags & D3D11_CREATE_DEVICE_SINGLETHREADED)
? D2D1_DEVICE_CONTEXT_OPTIONS_NONE
: D2D1_DEVICE_CONTEXT_OPTIONS_ENABLE_MULTITHREADED_OPTIMIZATIONS
);
hr = D2D1CreateDevice(ipDxgiDevice, d2d1Props, &ipD2D1Device);
CHECK_HR_BREAK(hr);
// Get D2D1 factory
ipD2D1Device->GetFactory(&ipD2D1Factory);
if (nullptr == ipD2D1Factory)
{
hr = D2DERR_INVALID_CALL;
break;
}
//create WIC factory
hr = CoCreateInstance(
CLSID_WICImagingFactory,
NULL,
CLSCTX_INPROC_SERVER,
IID_IWICImagingFactory,
reinterpret_cast<void **>(&ipWICImageFactory)
);
CHECK_HR_BREAK(hr);
// create D2D1 target bitmap for render
hr = ipWICImageFactory->CreateBitmap(
(UINT)rendererInfo.OutputSize.Width,
(UINT)rendererInfo.OutputSize.Height,
GUID_WICPixelFormat32bppPBGRA,
WICBitmapCacheOnDemand,
&ipWICOutputBitmap);
CHECK_HR_BREAK(hr);
if (nullptr == ipWICOutputBitmap)
{
hr = E_OUTOFMEMORY;
break;
}
// create a D2D1 render target (for D2D1 drawing)
D2D1_RENDER_TARGET_PROPERTIES d2d1RenderTargetProp = D2D1::RenderTargetProperties
(
D2D1_RENDER_TARGET_TYPE_DEFAULT,
D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED),
0.0f, // default dpi
0.0f, // default dpi
D2D1_RENDER_TARGET_USAGE_GDI_COMPATIBLE
);
hr = ipD2D1Factory->CreateWicBitmapRenderTarget(
ipWICOutputBitmap,
d2d1RenderTargetProp,
&ipD2D1RenderTarget
);
CHECK_HR_BREAK(hr);
#pragma endregion </For_2D_operations>
} while (false);
if (SUCCEEDED(hr))
{
// copy output parameters
memcpy_s((void*)&m_rendererInfo, sizeof(m_rendererInfo), (const void*)&rendererInfo, sizeof(m_rendererInfo));
// set parameters
m_desktopOutputDesc = dgixOutputDesc;
m_ipDxgiOutputDuplication = ipDxgiOutputDuplication;
m_ipCopyTexture2D = ipCopyTexture2D;
m_ipD2D1Device = ipD2D1Device;
m_ipD2D1Factory = ipD2D1Factory;
m_ipWICImageFactory = ipWICImageFactory;
m_ipWICOutputBitmap = ipWICOutputBitmap;
m_ipD2D1RenderTarget = ipD2D1RenderTarget;
}
return S_OK;
}
void CDXGICapture::terminateDeviceResource()
{
m_ipDxgiOutputDuplication = nullptr;
m_ipCopyTexture2D = nullptr;
m_ipD2D1Device = nullptr;
m_ipD2D1Factory = nullptr;
m_ipWICImageFactory = nullptr;
m_ipWICOutputBitmap = nullptr;
m_ipD2D1RenderTarget = nullptr;
// clear config parameters
RtlZeroMemory(&m_rendererInfo, sizeof(m_rendererInfo));
// clear mouse information parameters
if (m_mouseInfo.PtrShapeBuffer != nullptr) {
delete[] m_mouseInfo.PtrShapeBuffer;
m_mouseInfo.PtrShapeBuffer = nullptr;
}
RtlZeroMemory(&m_mouseInfo, sizeof(m_mouseInfo));
// clear temp temp buffer
if (m_tempMouseBuffer.Buffer != nullptr) {
delete[] m_tempMouseBuffer.Buffer;
m_tempMouseBuffer.Buffer = nullptr;
}
RtlZeroMemory(&m_tempMouseBuffer, sizeof(m_tempMouseBuffer));
// clear desktop output desc
RtlZeroMemory(&m_desktopOutputDesc, sizeof(m_desktopOutputDesc));
}
HRESULT CDXGICapture::Initialize()
{
AUTOLOCK();
if (m_bInitialized) {
return HRESULT_FROM_WIN32(ERROR_ALREADY_INITIALIZED); // already initialized
}
HRESULT hr = S_OK;
D3D_FEATURE_LEVEL lFeatureLevel;
CComPtr<ID3D11Device> ipDevice;
CComPtr<ID3D11DeviceContext> ipDeviceContext;
// required for monitor dpi problem (???)
SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE);
// Create device
for (UINT i = 0; i < g_NumDriverTypes; ++i)
{
hr = D3D11CreateDevice(
nullptr,
g_DriverTypes[i],
nullptr,
/* D3D11_CREATE_DEVICE_BGRA_SUPPORT
* This flag adds support for surfaces with a different
* color channel ordering than the API default.
* You need it for compatibility with Direct2D. */
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
g_FeatureLevels,
g_NumFeatureLevels,
D3D11_SDK_VERSION,
&ipDevice,
&lFeatureLevel,
&ipDeviceContext);
if (SUCCEEDED(hr))
{
// Device creation success, no need to loop anymore
break;
}
ipDevice = nullptr;
ipDeviceContext = nullptr;
}
if (FAILED(hr)) {
return hr;
}
if (nullptr == ipDevice) {
return E_UNEXPECTED;
}
// load all monitor informations
hr = loadMonitorInfos(ipDevice);
if (FAILED(hr)) {
return hr;
}
// set common fields
m_lD3DFeatureLevel = lFeatureLevel;
m_ipD3D11Device = ipDevice;
m_ipD3D11DeviceContext = ipDeviceContext;
m_bInitialized = TRUE;
return S_OK;
}
HRESULT CDXGICapture::Terminate()
{
AUTOLOCK();
if (!m_bInitialized) {
return S_FALSE; // already terminated
}
this->terminateDeviceResource();
m_ipD3D11Device = nullptr;
m_ipD3D11DeviceContext = nullptr;
m_lD3DFeatureLevel = D3D_FEATURE_LEVEL_INVALID;
freeMonitorInfos();
m_bInitialized = FALSE;
return S_OK;
}
HRESULT CDXGICapture::SetConfig(const tagScreenCaptureFilterConfig *pConfig)
{
AUTOLOCK();
if (!m_bInitialized) {
return D2DERR_NOT_INITIALIZED;
}
if (nullptr == pConfig) {
return E_INVALIDARG;
}
// terminate old resources
this->terminateDeviceResource();
HRESULT hr = S_OK;
const tagDublicatorMonitorInfo *pSelectedMonitorInfo = nullptr;
pSelectedMonitorInfo = this->FindDublicatorMonitorInfo(pConfig->MonitorIdx);
if (nullptr == pSelectedMonitorInfo) {
return E_INVALIDARG;
}
hr = this->createDeviceResource(pConfig, pSelectedMonitorInfo);
if (FAILED(hr)) {
return hr;
}
return hr;
}
HRESULT CDXGICapture::SetConfig(const tagScreenCaptureFilterConfig &config)
{
return this->SetConfig(&config);
}
BOOL CDXGICapture::IsInitialized() const
{
AUTOLOCK();
return m_bInitialized;
}
D3D_FEATURE_LEVEL CDXGICapture::GetD3DFeatureLevel() const
{
AUTOLOCK();
return m_lD3DFeatureLevel;
}
int CDXGICapture::GetDublicatorMonitorInfoCount() const
{
AUTOLOCK();
return (int)m_monitorInfos.size();
}
const tagDublicatorMonitorInfo* CDXGICapture::GetDublicatorMonitorInfo(int index) const
{
AUTOLOCK();
size_t nCount = m_monitorInfos.size();
if ((index < 0) || (index >= (int)nCount)) {
return nullptr;
}
return m_monitorInfos[index];
} // GetDublicatorMonitorInfo
const tagDublicatorMonitorInfo* CDXGICapture::FindDublicatorMonitorInfo(int monitorIdx) const
{
AUTOLOCK();
size_t nCount = m_monitorInfos.size();
if (nCount == 0) {
return nullptr;
}
DublicatorMonitorInfoVec::const_iterator it = m_monitorInfos.begin();
DublicatorMonitorInfoVec::const_iterator end = m_monitorInfos.end();
for (size_t i = 0; (i < nCount) && (it != end); i++, it++) {
tagDublicatorMonitorInfo *pInfo = *it;
if (monitorIdx == pInfo->Idx) {
return pInfo;
}
}
return nullptr;
} // FindDublicatorMonitorInfo
//
// CaptureToFile
//
HRESULT CDXGICapture::CaptureToFile(_In_ LPCWSTR lpcwOutputFileName, _Out_opt_ BOOL *pRetIsTimeout /*= NULL*/, _Out_opt_ UINT *pRetRenderDuration /*= NULL*/)
{
AUTOLOCK();
if (nullptr != pRetIsTimeout) {
*pRetIsTimeout = FALSE;
}
if (nullptr != pRetRenderDuration) {
*pRetRenderDuration = 0xFFFFFFFF;
}
if (!m_bInitialized) {
return D2DERR_NOT_INITIALIZED;
}
CHECK_POINTER_EX(m_ipDxgiOutputDuplication, E_INVALIDARG);
CHECK_POINTER_EX(lpcwOutputFileName, E_INVALIDARG);
HRESULT hr = S_OK;
hr = DXGICaptureHelper::IsRendererInfoValid(&m_rendererInfo);
if (FAILED(hr)) {
return hr;
}
// is valid?
hr = DXGICaptureHelper::GetContainerFormatByFileName(lpcwOutputFileName);
if (FAILED(hr)) {
return hr;
}
DXGI_OUTDUPL_FRAME_INFO FrameInfo;
CComPtr<IDXGIResource> ipDesktopResource;
CComPtr<ID3D11Texture2D> ipAcquiredDesktopImage;
CComPtr<ID2D1Bitmap> ipD2D1SourceBitmap;
std::chrono::system_clock::time_point startTick;
if (nullptr != pRetRenderDuration) {
startTick = std::chrono::system_clock::now();
}
// Get new frame
hr = m_ipDxgiOutputDuplication->AcquireNextFrame(1000, &FrameInfo, &ipDesktopResource);
if (hr == DXGI_ERROR_WAIT_TIMEOUT)
{
if (nullptr != pRetIsTimeout) {
*pRetIsTimeout = TRUE;
}
return S_FALSE;
}
else if (FAILED(hr))
{
return hr;
}
// QI for ID3D11Texture2D
hr = ipDesktopResource->QueryInterface(IID_PPV_ARGS(&ipAcquiredDesktopImage));
ipDesktopResource = nullptr;
CHECK_HR_RETURN(hr);
if (nullptr == ipAcquiredDesktopImage)
{
// release frame
m_ipDxgiOutputDuplication->ReleaseFrame();
return E_OUTOFMEMORY;
}
// Copy needed full part of desktop image
m_ipD3D11DeviceContext->CopyResource(m_ipCopyTexture2D, ipAcquiredDesktopImage);
if (m_rendererInfo.ShowCursor) {
hr = DXGICaptureHelper::GetMouse(m_ipDxgiOutputDuplication, &m_mouseInfo, &FrameInfo, (UINT)m_rendererInfo.MonitorIdx, m_desktopOutputDesc.DesktopCoordinates.left, m_desktopOutputDesc.DesktopCoordinates.top);
if (SUCCEEDED(hr) && m_mouseInfo.Visible) {
hr = DXGICaptureHelper::DrawMouse(&m_mouseInfo, &m_desktopOutputDesc, &m_tempMouseBuffer, m_ipCopyTexture2D);
}
if (FAILED(hr)) {
// release frame
m_ipDxgiOutputDuplication->ReleaseFrame();
return hr;
}
}
// release frame
hr = m_ipDxgiOutputDuplication->ReleaseFrame();
CHECK_HR_RETURN(hr);
// create D2D1 source bitmap
hr = DXGICaptureHelper::CreateBitmap(m_ipD2D1RenderTarget, m_ipCopyTexture2D, &ipD2D1SourceBitmap);
CHECK_HR_RETURN(hr);
D2D1_RECT_F rcSource = D2D1::RectF(
(FLOAT)m_rendererInfo.SrcBounds.X,
(FLOAT)m_rendererInfo.SrcBounds.Y,
(FLOAT)(m_rendererInfo.SrcBounds.X + m_rendererInfo.SrcBounds.Width),
(FLOAT)(m_rendererInfo.SrcBounds.Y + m_rendererInfo.SrcBounds.Height));
D2D1_RECT_F rcTarget = D2D1::RectF(
(FLOAT)m_rendererInfo.DstBounds.X,
(FLOAT)m_rendererInfo.DstBounds.Y,
(FLOAT)(m_rendererInfo.DstBounds.X + m_rendererInfo.DstBounds.Width),
(FLOAT)(m_rendererInfo.DstBounds.Y + m_rendererInfo.DstBounds.Height));
D2D1_POINT_2F ptTransformCenter = D2D1::Point2F(m_rendererInfo.OutputSize.Width / 2.0f, m_rendererInfo.OutputSize.Height / 2.0f);
// Apply the rotation transform to the render target.
D2D1::Matrix3x2F rotate = D2D1::Matrix3x2F::Rotation(
m_rendererInfo.RotationDegrees,
ptTransformCenter
);
D2D1::Matrix3x2F scale = D2D1::Matrix3x2F::Scale(
D2D1::SizeF(m_rendererInfo.ScaleX, m_rendererInfo.ScaleY),
ptTransformCenter
);
// Priority: first rotate, after scale...
m_ipD2D1RenderTarget->SetTransform(rotate * scale);
m_ipD2D1RenderTarget->BeginDraw();
// clear background color
m_ipD2D1RenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::Black, 1.0f));
m_ipD2D1RenderTarget->DrawBitmap(ipD2D1SourceBitmap, rcTarget, 1.0f, D2D1_BITMAP_INTERPOLATION_MODE_LINEAR, rcSource);
// Reset transform
//m_ipD2D1RenderTarget->SetTransform(D2D1::Matrix3x2F::Identity());
// Logo draw sample
//m_ipD2D1RenderTarget->DrawBitmap(ipBmpLogo, D2D1::RectF(0, 0, 2 * 200, 2 * 46));
hr = m_ipD2D1RenderTarget->EndDraw();
if (FAILED(hr)) {
return hr;
}
// calculate render time without save
if (nullptr != pRetRenderDuration) {
*pRetRenderDuration = (UINT)((std::chrono::system_clock::now() - startTick).count() / 10000);
}
hr = DXGICaptureHelper::SaveImageToFile(m_ipWICImageFactory, m_ipWICOutputBitmap, lpcwOutputFileName);
if (FAILED(hr)) {
return hr;
}
return S_OK;
} // CaptureToFile
#undef AUTOLOCK