DXGI/DesktopDuplication.cpp
2024-09-08 12:21:59 +08:00

645 lines
18 KiB
C++

// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//
// Copyright (c) Microsoft Corporation. All rights reserved
#include <limits.h>
#include "DisplayManager.h"
#include "DuplicationManager.h"
#include "OutputManager.h"
#include "ThreadManager.h"
//#include <D3Dx11tex.h>
#include <algorithm>
#include <d3d11_1.h>
#include <memory>
//
// Globals
//
OUTPUTMANAGER OutMgr;
// Below are lists of errors expect from Dxgi API calls when a transition event like mode change, PnpStop, PnpStart
// desktop switch, TDR or session disconnect/reconnect. In all these cases we want the application to clean up the threads that process
// the desktop updates and attempt to recreate them.
// If we get an error that is not on the appropriate list then we exit the application
// These are the errors we expect from general Dxgi API due to a transition
HRESULT SystemTransitionsExpectedErrors[] = {
DXGI_ERROR_DEVICE_REMOVED,
DXGI_ERROR_ACCESS_LOST,
static_cast<HRESULT>(WAIT_ABANDONED),
S_OK // Terminate list with zero valued HRESULT
};
// These are the errors we expect from IDXGIOutput1::DuplicateOutput due to a transition
HRESULT CreateDuplicationExpectedErrors[] = {
DXGI_ERROR_DEVICE_REMOVED,
static_cast<HRESULT>(E_ACCESSDENIED),
DXGI_ERROR_UNSUPPORTED,
DXGI_ERROR_SESSION_DISCONNECTED,
S_OK // Terminate list with zero valued HRESULT
};
// These are the errors we expect from IDXGIOutputDuplication methods due to a transition
HRESULT FrameInfoExpectedErrors[] = {
DXGI_ERROR_DEVICE_REMOVED,
DXGI_ERROR_ACCESS_LOST,
S_OK // Terminate list with zero valued HRESULT
};
// These are the errors we expect from IDXGIAdapter::EnumOutputs methods due to outputs becoming stale during a transition
HRESULT EnumOutputsExpectedErrors[] = {
DXGI_ERROR_NOT_FOUND,
S_OK // Terminate list with zero valued HRESULT
};
//
// Forward Declarations
//
DWORD WINAPI DDProc(_In_ void* Param);
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
bool ProcessCmdline(_Out_ INT* Output);
void ShowHelp();
//
// Class for progressive waits
//
typedef struct {
UINT WaitTime;
UINT WaitCount;
}WAIT_BAND;
#define WAIT_BAND_COUNT 3
#define WAIT_BAND_STOP 0
class DYNAMIC_WAIT {
public:
DYNAMIC_WAIT();
~DYNAMIC_WAIT();
void Wait();
private:
static const WAIT_BAND m_WaitBands[WAIT_BAND_COUNT];
// Period in seconds that a new wait call is considered part of the same wait sequence
static const UINT m_WaitSequenceTimeInSeconds = 2;
UINT m_CurrentWaitBandIdx;
UINT m_WaitCountInCurrentBand;
LARGE_INTEGER m_QPCFrequency;
LARGE_INTEGER m_LastWakeUpTime;
BOOL m_QPCValid;
};
const WAIT_BAND DYNAMIC_WAIT::m_WaitBands[WAIT_BAND_COUNT] = {
{250, 20},
{2000, 60},
{5000, WAIT_BAND_STOP} // Never move past this band
};
DYNAMIC_WAIT::DYNAMIC_WAIT() : m_CurrentWaitBandIdx(0), m_WaitCountInCurrentBand(0) {
m_QPCValid = QueryPerformanceFrequency(&m_QPCFrequency);
m_LastWakeUpTime.QuadPart = 0L;
}
DYNAMIC_WAIT::~DYNAMIC_WAIT() {
}
void DYNAMIC_WAIT::Wait() {
LARGE_INTEGER CurrentQPC = { 0 };
// Is this wait being called with the period that we consider it to be part of the same wait sequence
QueryPerformanceCounter(&CurrentQPC);
if (m_QPCValid && (CurrentQPC.QuadPart <= (m_LastWakeUpTime.QuadPart + (m_QPCFrequency.QuadPart * m_WaitSequenceTimeInSeconds)))) {
// We are still in the same wait sequence, lets check if we should move to the next band
if ((m_WaitBands[m_CurrentWaitBandIdx].WaitCount != WAIT_BAND_STOP) && (m_WaitCountInCurrentBand > m_WaitBands[m_CurrentWaitBandIdx].WaitCount)) {
m_CurrentWaitBandIdx++;
m_WaitCountInCurrentBand = 0;
}
}
else {
// Either we could not get the current time or we are starting a new wait sequence
m_WaitCountInCurrentBand = 0;
m_CurrentWaitBandIdx = 0;
}
// Sleep for the required period of time
Sleep(m_WaitBands[m_CurrentWaitBandIdx].WaitTime);
// Record the time we woke up so we can detect wait sequences
QueryPerformanceCounter(&m_LastWakeUpTime);
m_WaitCountInCurrentBand++;
}
//
// Program entry point
//
int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ INT nCmdShow) {
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
INT SingleOutput;
// Synchronization
HANDLE UnexpectedErrorEvent = nullptr;
HANDLE ExpectedErrorEvent = nullptr;
HANDLE TerminateThreadsEvent = nullptr;
// Window
HWND WindowHandle = nullptr;
bool CmdResult = ProcessCmdline(&SingleOutput);
if (!CmdResult) {
ShowHelp();
return 0;
}
// Event used by the threads to signal an unexpected error and we want to quit the app
UnexpectedErrorEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
if (!UnexpectedErrorEvent) {
ProcessFailure(nullptr, L"UnexpectedErrorEvent creation failed", L"Error", E_UNEXPECTED);
return 0;
}
// Event for when a thread encounters an expected error
ExpectedErrorEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
if (!ExpectedErrorEvent) {
ProcessFailure(nullptr, L"ExpectedErrorEvent creation failed", L"Error", E_UNEXPECTED);
return 0;
}
// Event to tell spawned threads to quit
TerminateThreadsEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
if (!TerminateThreadsEvent) {
ProcessFailure(nullptr, L"TerminateThreadsEvent creation failed", L"Error", E_UNEXPECTED);
return 0;
}
// Load simple cursor
HCURSOR Cursor = nullptr;
Cursor = LoadCursor(nullptr, IDC_ARROW);
if (!Cursor) {
ProcessFailure(nullptr, L"Cursor load failed", L"Error", E_UNEXPECTED);
return 0;
}
// Register class
WNDCLASSEXW Wc;
Wc.cbSize = sizeof(WNDCLASSEXW);
Wc.style = CS_HREDRAW | CS_VREDRAW;
Wc.lpfnWndProc = WndProc;
Wc.cbClsExtra = 0;
Wc.cbWndExtra = 0;
Wc.hInstance = hInstance;
Wc.hIcon = nullptr;
Wc.hCursor = Cursor;
Wc.hbrBackground = nullptr;
Wc.lpszMenuName = nullptr;
Wc.lpszClassName = L"ddasample";
Wc.hIconSm = nullptr;
if (!RegisterClassExW(&Wc)) {
ProcessFailure(nullptr, L"Window class registration failed", L"Error", E_UNEXPECTED);
return 0;
}
// Create window
RECT WindowRect = { 0, 0, 960, 540 };
AdjustWindowRect(&WindowRect, WS_OVERLAPPEDWINDOW, FALSE);
WindowHandle = CreateWindowW(L"ddasample", L"DXGI desktop duplication sample",
WS_OVERLAPPEDWINDOW,
0, 0,
WindowRect.right - WindowRect.left, WindowRect.bottom - WindowRect.top,
nullptr, nullptr, hInstance, nullptr);
if (!WindowHandle) {
ProcessFailure(nullptr, L"Window creation failed", L"Error", E_FAIL);
return 0;
}
DestroyCursor(Cursor);
ShowWindow(WindowHandle, nCmdShow);
UpdateWindow(WindowHandle);
THREADMANAGER ThreadMgr;
RECT DeskBounds;
UINT OutputCount;
// Message loop (attempts to update screen when no other messages to process)
MSG msg = { 0 };
bool FirstTime = true;
bool Occluded = true;
DYNAMIC_WAIT DynamicWait;
while (WM_QUIT != msg.message) {
DUPL_RETURN Ret = DUPL_RETURN_SUCCESS;
if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
if (msg.message == OCCLUSION_STATUS_MSG) {
// Present may not be occluded now so try again
Occluded = false;
}
else {
// Process window messages
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
else if (WaitForSingleObjectEx(UnexpectedErrorEvent, 0, FALSE) == WAIT_OBJECT_0) {
// Unexpected error occurred so exit the application
break;
}
else if (FirstTime || WaitForSingleObjectEx(ExpectedErrorEvent, 0, FALSE) == WAIT_OBJECT_0) {
if (!FirstTime) {
// Terminate other threads
SetEvent(TerminateThreadsEvent);
ThreadMgr.WaitForThreadTermination();
ResetEvent(TerminateThreadsEvent);
ResetEvent(ExpectedErrorEvent);
// Clean up
ThreadMgr.Clean();
OutMgr.CleanRefs();
// As we have encountered an error due to a system transition we wait before trying again, using this dynamic wait
// the wait periods will get progressively long to avoid wasting too much system resource if this state lasts a long time
DynamicWait.Wait();
}
else {
// First time through the loop so nothing to clean up
FirstTime = false;
}
// Re-initialize
Ret = OutMgr.InitOutput(WindowHandle, SingleOutput, &OutputCount, &DeskBounds);
if (Ret == DUPL_RETURN_SUCCESS) {
HANDLE SharedHandle = OutMgr.GetSharedHandle();
if (SharedHandle) {
Ret = ThreadMgr.Initialize(SingleOutput, OutputCount, UnexpectedErrorEvent, ExpectedErrorEvent, TerminateThreadsEvent, SharedHandle, &DeskBounds);
}
else {
DisplayMsg(L"Failed to get handle of shared surface", L"Error", S_OK);
Ret = DUPL_RETURN_ERROR_UNEXPECTED;
}
}
// We start off in occluded state and we should immediate get a occlusion status window message
Occluded = true;
}
else {
// Nothing else to do, so try to present to write out to window if not occluded
if (!Occluded) {
Ret = OutMgr.UpdateApplicationWindow(ThreadMgr.GetPointerInfo(), &Occluded);
}
}
// Check if for errors
if (Ret != DUPL_RETURN_SUCCESS) {
if (Ret == DUPL_RETURN_ERROR_EXPECTED) {
// Some type of system transition is occurring so retry
SetEvent(ExpectedErrorEvent);
}
else {
// Unexpected error so exit
break;
}
}
}
// Make sure all other threads have exited
if (SetEvent(TerminateThreadsEvent)) {
ThreadMgr.WaitForThreadTermination();
}
// Clean up
CloseHandle(UnexpectedErrorEvent);
CloseHandle(ExpectedErrorEvent);
CloseHandle(TerminateThreadsEvent);
if (msg.message == WM_QUIT) {
// For a WM_QUIT message we should return the wParam value
return static_cast<INT>(msg.wParam);
}
return 0;
}
//
// Shows help
//
void ShowHelp() {
DisplayMsg(L"The following optional parameters can be used -\n /output [all | n]\t\tto duplicate all outputs or the nth output\n /?\t\t\tto display this help section",
L"Proper usage", S_OK);
}
//
// Process command line parameters
//
bool ProcessCmdline(_Out_ INT* Output) {
*Output = -1;
// __argv and __argc are global vars set by system
for (UINT i = 1; i < static_cast<UINT>(__argc); ++i) {
if ((strcmp(__argv[i], "-output") == 0) ||
(strcmp(__argv[i], "/output") == 0)) {
if (++i >= static_cast<UINT>(__argc)) {
return false;
}
if (strcmp(__argv[i], "all") == 0) {
*Output = -1;
}
else {
*Output = atoi(__argv[i]);
}
continue;
}
else {
return false;
}
}
return true;
}
//
// Window message processor
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
switch (message) {
case WM_DESTROY:
{
PostQuitMessage(0);
break;
}
case WM_SIZE:
{
// Tell output manager that window size has changed
OutMgr.WindowResize();
break;
}
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
//
// Entry point for new duplication threads
//
DWORD WINAPI DDProc(_In_ void* Param) {
// Classes
DISPLAYMANAGER DispMgr;
DUPLICATIONMANAGER DuplMgr;
// D3D objects
ID3D11Texture2D* SharedSurf = nullptr;
IDXGIKeyedMutex* KeyMutex = nullptr;
// Data passed in from thread creation
THREAD_DATA* TData = reinterpret_cast<THREAD_DATA*>(Param);
// Get desktop
DUPL_RETURN Ret;
HDESK CurrentDesktop = nullptr;
CurrentDesktop = OpenInputDesktop(0, FALSE, GENERIC_ALL);
if (!CurrentDesktop) {
// We do not have access to the desktop so request a retry
SetEvent(TData->ExpectedErrorEvent);
Ret = DUPL_RETURN_ERROR_EXPECTED;
goto Exit;
}
// Attach desktop to this thread
bool DesktopAttached;
DesktopAttached = SetThreadDesktop(CurrentDesktop) != 0;
CloseDesktop(CurrentDesktop);
CurrentDesktop = nullptr;
if (!DesktopAttached) {
// We do not have access to the desktop so request a retry
Ret = DUPL_RETURN_ERROR_EXPECTED;
goto Exit;
}
// New display manager
DispMgr.InitD3D(&TData->DxRes);
// Obtain handle to sync shared Surface
HRESULT hr;
hr = TData->DxRes.Device->OpenSharedResource(TData->TexSharedHandle, __uuidof(ID3D11Texture2D),
reinterpret_cast<void **>(&SharedSurf));
if (FAILED(hr)) {
Ret = ProcessFailure(TData->DxRes.Device, L"Opening shared texture failed", L"Error", hr, SystemTransitionsExpectedErrors);
goto Exit;
}
hr = SharedSurf->QueryInterface(__uuidof(IDXGIKeyedMutex), reinterpret_cast<void**>(&KeyMutex));
if (FAILED(hr)) {
Ret = ProcessFailure(nullptr, L"Failed to get keyed mutex interface in spawned thread", L"Error", hr);
goto Exit;
}
// Make duplication manager
Ret = DuplMgr.InitDupl(TData->DxRes.Device, TData->Output);
if (Ret != DUPL_RETURN_SUCCESS) {
goto Exit;
}
// Get output description
DXGI_OUTPUT_DESC DesktopDesc;
RtlZeroMemory(&DesktopDesc, sizeof(DXGI_OUTPUT_DESC));
DuplMgr.GetOutputDesc(&DesktopDesc);
// Main duplication loop
bool WaitToProcessCurrentFrame;
WaitToProcessCurrentFrame = false;
FRAME_DATA CurrentData;
while ((WaitForSingleObjectEx(TData->TerminateThreadsEvent, 0, FALSE) == WAIT_TIMEOUT)) {
if (!WaitToProcessCurrentFrame) {
// Get new frame from desktop duplication
bool TimeOut;
Ret = DuplMgr.GetFrame(&CurrentData, &TimeOut);
if (Ret != DUPL_RETURN_SUCCESS) {
// An error occurred getting the next frame drop out of loop which
// will check if it was expected or not
break;
}
// Check for timeout
if (TimeOut) {
// No new frame at the moment
continue;
}
}
static int tim = 0;
if (tim == 0) {
tim = 1;
ID3D11Texture2D* t2d = CurrentData.Frame;
ID3D11DeviceContext* ctx;
DuplMgr.m_Device->GetImmediateContext(&ctx);
//extractImageData(DuplMgr.m_DeskDupl, ctx, t2d);
}
// We have a new frame so try and process it
// Try to acquire keyed mutex in order to access shared surface
hr = KeyMutex->AcquireSync(0, 1000);
if (hr == static_cast<HRESULT>(WAIT_TIMEOUT)) {
// Can't use shared surface right now, try again later
WaitToProcessCurrentFrame = true;
continue;
}
else if (FAILED(hr)) {
// Generic unknown failure
Ret = ProcessFailure(TData->DxRes.Device, L"Unexpected error acquiring KeyMutex", L"Error", hr, SystemTransitionsExpectedErrors);
DuplMgr.DoneWithFrame();
break;
}
// We can now process the current frame
WaitToProcessCurrentFrame = false;
// Get mouse info
Ret = DuplMgr.GetMouse(TData->PtrInfo, &(CurrentData.FrameInfo), TData->OffsetX, TData->OffsetY);
if (Ret != DUPL_RETURN_SUCCESS) {
DuplMgr.DoneWithFrame();
KeyMutex->ReleaseSync(1);
break;
}
// Process new frame
Ret = DispMgr.ProcessFrame(&CurrentData, SharedSurf, TData->OffsetX, TData->OffsetY, &DesktopDesc);
if (Ret != DUPL_RETURN_SUCCESS) {
DuplMgr.DoneWithFrame();
KeyMutex->ReleaseSync(1);
break;
}
// Release acquired keyed mutex
hr = KeyMutex->ReleaseSync(1);
if (FAILED(hr)) {
Ret = ProcessFailure(TData->DxRes.Device, L"Unexpected error releasing the keyed mutex", L"Error", hr, SystemTransitionsExpectedErrors);
DuplMgr.DoneWithFrame();
break;
}
// Release frame back to desktop duplication
Ret = DuplMgr.DoneWithFrame();
if (Ret != DUPL_RETURN_SUCCESS) {
break;
}
}
Exit:
if (Ret != DUPL_RETURN_SUCCESS) {
if (Ret == DUPL_RETURN_ERROR_EXPECTED) {
// The system is in a transition state so request the duplication be restarted
SetEvent(TData->ExpectedErrorEvent);
}
else {
// Unexpected error so exit the application
SetEvent(TData->UnexpectedErrorEvent);
}
}
if (SharedSurf) {
SharedSurf->Release();
SharedSurf = nullptr;
}
if (KeyMutex) {
KeyMutex->Release();
KeyMutex = nullptr;
}
return 0;
}
_Post_satisfies_(return != DUPL_RETURN_SUCCESS)
DUPL_RETURN ProcessFailure(_In_opt_ ID3D11Device * Device, _In_ LPCWSTR Str, _In_ LPCWSTR Title, HRESULT hr, _In_opt_z_ HRESULT * ExpectedErrors) {
HRESULT TranslatedHr;
// On an error check if the DX device is lost
if (Device) {
HRESULT DeviceRemovedReason = Device->GetDeviceRemovedReason();
switch (DeviceRemovedReason) {
case DXGI_ERROR_DEVICE_REMOVED:
case DXGI_ERROR_DEVICE_RESET:
case static_cast<HRESULT>(E_OUTOFMEMORY) :
{
// Our device has been stopped due to an external event on the GPU so map them all to
// device removed and continue processing the condition
TranslatedHr = DXGI_ERROR_DEVICE_REMOVED;
break;
}
case S_OK:
{
// Device is not removed so use original error
TranslatedHr = hr;
break;
}
default:
{
// Device is removed but not a error we want to remap
TranslatedHr = DeviceRemovedReason;
}
}
}
else {
TranslatedHr = hr;
}
// Check if this error was expected or not
if (ExpectedErrors) {
HRESULT* CurrentResult = ExpectedErrors;
while (*CurrentResult != S_OK) {
if (*(CurrentResult++) == TranslatedHr) {
return DUPL_RETURN_ERROR_EXPECTED;
}
}
}
// Error was not expected so display the message box
DisplayMsg(Str, Title, TranslatedHr);
return DUPL_RETURN_ERROR_UNEXPECTED;
}
//
// Displays a message
//
void DisplayMsg(_In_ LPCWSTR Str, _In_ LPCWSTR Title, HRESULT hr) {
if (SUCCEEDED(hr)) {
MessageBoxW(nullptr, Str, Title, MB_OK);
return;
}
const UINT StringLen = (UINT)(wcslen(Str) + sizeof(" with HRESULT 0x########."));
wchar_t* OutStr = new wchar_t[StringLen];
if (!OutStr) {
return;
}
INT LenWritten = swprintf_s(OutStr, StringLen, L"%s with 0x%X.", Str, hr);
if (LenWritten != -1) {
MessageBoxW(nullptr, OutStr, Title, MB_OK);
}
delete[] OutStr;
}