DXGI를 이용한 윈도우 화면 캡처 소스 코드(Visual C++)
화면 캡처 글을 검색해 보면 다양한 글이 나옵니다.
Various methods for capturing the screen
; http://www.codeproject.com/Articles/5051/Various-methods-for-capturing-the-screen
일단 위의 글에서는 3가지 방법을 소개하는데 그중 DirectX 예제만 따라 해 보았지만 Windows 10에서 동작하지 않았습니다. 왜냐하면 GetFrontBufferData 호출에서,
hr = g_pd3dDevice->GetFrontBufferData(0, pSurface);
// hr == 0x8876086c (D3DERR_INVALIDCALL)
D3DERR_INVALIDCALL 오류가 발생하는데 이유를 모르겠습니다. (혹시 아시는 분 덧글 부탁드립니다.)
게다가 d3dx9tex.h 헤더 파일 및 d3dx9.lib 파일이 없다고 오류가 발생하는데 이를 정상적으로 컴파일하기 위해서는 다음의 과정을 거쳐야 합니다.
DXSDK_Jun10.exe 설치 시 "Error Code: S1023" 오류 해결하는 방법
; https://www.sysnet.pe.kr/2/0/1195
귀찮으니 ^^; 다음 캡처 방식으로 넘어가 보겠습니다. Windows 8부터 지원하는 DXGI를 이용하면,
DXGI fast screen capture
; http://www.pavelgurenko.com/2013/12/dxgi-outputs-enumeration-and-fast.html
The second way of performing capture is DirectX Graphics Infrastructure - you command video card to store the whole screen or one/several monitors's contents inside separate part of its memory (surface).
속도는 물론이고 일반적인 GDI API로는 할 수 없는 DirectX 게임 화면까지도 안정적으로 캡처할 수 있다고 합니다. 친절하게도 위의 블로그를 쓴 사람이 github에 소스 코드도 공개한 것이 있고,
pgurenko/DXGICaptureSample
; https://github.com/pgurenko/DXGICaptureSample
또한 다음과 같은 식으로 사용하면, 3840 * 2160 해상도에서 30 fps로 캡처할 수 있었다고 합니다.
// https://github.com/pgurenko/DXGICaptureSample/issues/2
#include "stdafx.h"
#include "DXGIManager.h"
#include <time.h>
DXGIManager g_DXGIManager;
int capture(RECT& rcDim, vector<BYTE>& buf, CComPtr<IWICImagingFactory>& spWICFactory) {
DWORD dwWidth = rcDim.right - rcDim.left;
DWORD dwHeight = rcDim.bottom - rcDim.top;
DWORD dwBufSize = buf.size();
HRESULT hr = g_DXGIManager.GetOutputBits(buf.data(), rcDim);
if (FAILED(hr))
{
printf("GetOutputBits failed with hr=0x%08x\n", hr);
return hr;
}
return 0;
}
int _tmain(int argc, _TCHAR* argv[]) {
CoInitialize(NULL);
g_DXGIManager.SetCaptureSource(CSDesktop);
RECT rcDim;
g_DXGIManager.GetOutputRect(rcDim);
DWORD dwWidth = rcDim.right - rcDim.left;
DWORD dwHeight = rcDim.bottom - rcDim.top;
printf("dwWidth=%d dwHeight=%d\n", dwWidth, dwHeight);
DWORD dwBufSize = dwWidth*dwHeight * 4;
vector<BYTE> buf(dwBufSize);
CComPtr<IWICImagingFactory> spWICFactory = NULL;
HRESULT hr = spWICFactory.CoCreateInstance(CLSID_WICImagingFactory);
if (FAILED(hr))
return hr;
clock_t t1 = clock();
int i;
int iterations = 100;
for (i = 0; i < iterations; i++) {
capture(rcDim, buf, spWICFactory);
}
clock_t t2 = clock();
printf("%d iterations: %0.0f fps\n", iterations, iterations / ((double)(t2 - t1) / CLOCKS_PER_SEC));
return 0;
}
하지만 실제로 해보면 iterations 중에 capture 함수 내의 "GetOutputBits failed with hr=0x887a0027" 호출 실패가 있기 때문에 이를 감안하면 성능이 더 낮을 것입니다. 그래도 이 정도면 훌륭하죠. ^^
여기서 한 가지 중요한 점은, GetOutputBits 함수 내에서 호출하는 IDXGIOutputDuplication::AcquireNextFrame의 동작 방식입니다.
AcquireNextFrame은, 캡처 대상이 되는 화면에서 변화가 있어야 그것을 이미지 데이터로 반환합니다. 만약 지정한 시간 내에 변화가 없으면 DXGI_ERROR_WAIT_TIMEOUT 값을 반환합니다. 상당히 효율적이죠? ^^
실제로, 30fps 동영상을 바탕화면에서 재생하면서 IDXGIOutputDuplication::AcquireNextFrame을 호출하면 1초에 30번 정도 호출이 됩니다. 참고로, 바탕화면의 변화에는 (실제 캡처된 이미지에는 나오지 않지만) 마우스 커서 움직임도 포함이 됩니다.
DXGIOutputDuplication::AcquireNextFrame 내의 AcquireNextFrame 호출을 좀 더 살펴볼까요?
HRESULT DXGIOutputDuplication::AcquireNextFrame(IDXGISurface1** pDXGISurface, DXGIPointerInfo*& pDXGIPointer)
{
DXGI_OUTDUPL_FRAME_INFO fi;
CComPtr<IDXGIResource> spDXGIResource;
HRESULT hr = m_DXGIOutputDuplication->AcquireNextFrame(50, &fi, &spDXGIResource);
if(FAILED(hr))
{
__L_INFO("m_DXGIOutputDuplication->AcquireNextFrame failed with hr=0x%08x", hr);
return hr;
}
// ... [생략] ...
}
인자로 전달한 50은 50ms 시간 내에서 다음 화면 데이터를 얻어내라는 것입니다. 만약 그 시간 내에 데이터를 얻을 수 없었다면 hr == 0x887a0027 (The timeout value has elapsed and the resource is not yet available) 오류가 발생합니다. 따라서 저 시간 값을 너무 낮추면 (변화가 없는 화면인 경우) 호출에 대한 오류가 쓸데없이 많이 발생할 수 있습니다.
그 외에, 모니터 3대인 경우에는 CSMonitor3 스위치 문을 다음과 같이 DXGIManager::GetOutputDuplication 함수에 추가하면 됩니다.
case CSMonitor3:
{
int secondary = 0;
// Return the first with !IsPrimary
for (vector<DXGIOutputDuplication>::iterator iter = m_vOutputs.begin();
iter != m_vOutputs.end();
iter++)
{
DXGIOutputDuplication& out = *iter;
if (!out.IsPrimary())
{
if (secondary == 0)
{
secondary = 1;
continue;
}
outputs.push_back(out);
break;
}
}
}
break;
위의 소개 글에서
pgurenko/DXGICaptureSample로 공개된 캡처 예제를 버퍼 그대로 Bitmap으로 바꿔 화면에 출력하는 코드를 다음과 같이 작성해 봤습니다.
#include "stdafx.h"
#include "DXGIManager.h"
#include <time.h>
DXGIManager g_DXGIManager;
LRESULT WINAPI MsgProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
WNDCLASSEX g_wc = { sizeof(WNDCLASSEX), CS_CLASSDC, MsgProc, 0L, 0L, GetModuleHandle(NULL), NULL, NULL, NULL, NULL, L"Foo", NULL };
HWND g_hWnd;
HBITMAP g_hbm;
int hDC;
int g_screenX, g_screenY;
int _tmain(int argc, _TCHAR* argv[])
{
CoInitialize(NULL);
g_DXGIManager.SetCaptureSource(CaptureSource::CSMonitor1);
RECT rcDim;
g_DXGIManager.GetOutputRect(rcDim);
DWORD dwWidth = rcDim.right - rcDim.left;
DWORD dwHeight = rcDim.bottom - rcDim.top;
g_screenX = dwWidth;
g_screenY = dwHeight;
RegisterClassEx(&g_wc);
g_hWnd = CreateWindow(L"Foo", L"Foo", WS_OVERLAPPEDWINDOW, 0, 0, dwWidth, dwHeight, NULL, NULL, g_wc.hInstance, NULL);
::ShowWindow(g_hWnd, SW_SHOW);
printf("dwWidth=%d dwHeight=%d\n", dwWidth, dwHeight);
HDC hdcWin = GetDC(g_hWnd);
RECT rc = { 0, 0, dwWidth, dwHeight };
BITMAPINFO bmi = { 0 };
bmi.bmiHeader.biSize = sizeof(bmi.bmiHeader);
bmi.bmiHeader.biWidth = dwWidth;
bmi.bmiHeader.biHeight = -dwHeight;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
RGBQUAD *prgbBits;
g_hbm = CreateDIBSection(hdcWin, &bmi, DIB_RGB_COLORS, &reinterpret_cast<void*&>(prgbBits), NULL, 0);
DWORD dwBufSize = dwWidth*dwHeight * 4;
vector<BYTE> buf(dwBufSize);
BYTE *pBuf = buf.data();
MSG msg;
while (1)
{
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) == TRUE)
{
if (msg.message == WM_QUIT)
{
break;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else
{
HRESULT hr = g_DXGIManager.GetOutputBits(pBuf, rcDim);
if (hr != S_OK)
{
continue;
}
memcpy(prgbBits, pBuf, dwWidth * dwHeight * 4);
::InvalidateRect(g_hWnd, nullptr, TRUE);
}
}
CoUninitialize();
return msg.wParam;
}
LRESULT WINAPI MsgProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
if (msg == WM_CREATE)
{
return 0;
}
if (msg == WM_PAINT)
{
PAINTSTRUCT paint;
HDC hDC = ::BeginPaint(hWnd, &paint);
HDC hdcMem = CreateCompatibleDC(hDC);
HBITMAP hbmPrev = (HBITMAP)::SelectObject(hdcMem, g_hbm);
BitBlt(hDC, 0, 0, g_screenX, g_screenY, hdcMem, 0, 0, SRCCOPY);
SelectObject(hdcMem, hbmPrev);
DeleteDC(hdcMem);
::EndPaint(hWnd, &paint);
return 0;
}
return DefWindowProc(hWnd, msg, wParam, lParam);
}
첨부 파일은 위의 예제 코드를 포함합니다.
실제로 실행해서 프로그램을 돌려보면... 부드러운 캡처 성능에 반하실 것입니다. ^^
시간 있으시면 다음의 글도 보시면 좋겠지요. ^^
Desktop Screen Capture on Windows via Windows Desktop Duplication API with Drawing of Cursor's Image
; https://www.codeproject.com/Tips/1116253/Desktop-Screen-Capture-on-Windows-via-Windows-Desk
DXGI desktop duplication sample
; https://code.msdn.microsoft.com/windowsdesktop/Desktop-Duplication-Sample-da4c696a
The reboot of Coding4Fun
; https://blogs.windows.com/windowsdeveloper/2015/12/21/the-reboot-of-coding4fun/
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]