Desktop duplication (DirectX) screen capture fails to deliver screen updates

5.9k views Asked by At

I'm working on an application that would capture the screen through Desktop duplication APIs (using DirectX 11) (only the diff to the previous screen update) and render it on another window (The viewer might be running on another machine connected via LAN). The code is an improved version of the sample provided in MSDN. Everything works fine except the device did not give any screen update though there is one some times in the mid, that happens around 10% of the time on some machines (mostly on windows 8/8.1 machines and rarely on windows 10 machines). I tried all the possible ways to sort out this problem. Reduced the number of device resets, that provided me some what reliable output but not always work fine for 100%.

The device fails to provide an initial screen (a full screen) some times (This happens 60% of the time on all windows operating systems where Desktop duplication is supported), I came up with a work around that retried for an initial update from the device until it provides one but that too resulted in multiple issues, the device might not even give the initial screen ever.

I have already invested weeks of my efforts to fix the problem but did not figure out a proper solution and there are no forums I know that discusses these kind of issues. Any help would be appreciated.

Below is my code to get the screen diff to the previous one, init the device, populating the adapters and monitors.

Please bear with me for a very long code snippet, Thanks in advance.

To Get the screen update:

INT getChangedRegions(int timeout, rectangles &dirtyRects, std::vector <MOVE_RECT> &moveRects, UINT &rect_count, RECT ScreenRect)
{
UINT diffArea           = 0;
FRAME_DATA currentFrameData;

bool isTimeOut          = false;

TRY
{
    
    m_LastErrorCode = m_DuplicationManager.GetFrame(&currentFrameData, timeout, &isTimeOut);

    if(SUCCEEDED(m_LastErrorCode) && (!isTimeOut))
    {
        if(currentFrameData.FrameInfo.TotalMetadataBufferSize)
        {

            m_CurrentFrameTexture = currentFrameData.Frame;

            if(currentFrameData.MoveCount)
            {
                DXGI_OUTDUPL_MOVE_RECT* moveRectArray = reinterpret_cast<DXGI_OUTDUPL_MOVE_RECT*> (currentFrameData.MetaData);

                if (moveRectArray)
                {
                    for(UINT index = 0; index < currentFrameData.MoveCount; index++)
                    {
                        //WebRTC
                        // DirectX capturer API may randomly return unmoved move_rects, which should
                        // be skipped to avoid unnecessary wasting of differing and encoding
                        // resources.
                        // By using testing application it2me_standalone_host_main, this check
                        // reduces average capture time by 0.375% (4.07 -> 4.055), and average
                        // encode time by 0.313% (8.042 -> 8.016) without other impacts.

                        if (moveRectArray[index].SourcePoint.x != moveRectArray[index].DestinationRect.left || moveRectArray[index].SourcePoint.y != moveRectArray[index].DestinationRect.top) 
                        {

                            if(m_UseD3D11BitmapConversion)
                            {
                                MOVE_RECT moveRect;

                                moveRect.SourcePoint.x =  moveRectArray[index].SourcePoint.x * m_ImageScalingFactor;
                                moveRect.SourcePoint.y =  moveRectArray[index].SourcePoint.y * m_ImageScalingFactor;

                                moveRect.DestinationRect.left = moveRectArray[index].DestinationRect.left * m_ImageScalingFactor;
                                moveRect.DestinationRect.top = moveRectArray[index].DestinationRect.top * m_ImageScalingFactor;
                                moveRect.DestinationRect.bottom = moveRectArray[index].DestinationRect.bottom * m_ImageScalingFactor;
                                moveRect.DestinationRect.right = moveRectArray[index].DestinationRect.right * m_ImageScalingFactor;

                                moveRects.push_back(moveRect);
                                diffArea += abs((moveRect.DestinationRect.right - moveRect.DestinationRect.left) * 
                                        (moveRect.DestinationRect.bottom - moveRect.DestinationRect.top));
                            }
                            else
                            {
                                moveRects.push_back(moveRectArray[index]);
                                diffArea += abs((moveRectArray[index].DestinationRect.right - moveRectArray[index].DestinationRect.left) * 
                                        (moveRectArray[index].DestinationRect.bottom - moveRectArray[index].DestinationRect.top));
                            }
                        }
                    }
                }
                else
                {
                    return -1;
                }
            }

            if(currentFrameData.DirtyCount)
            {
                RECT* dirtyRectArray = reinterpret_cast<RECT*> (currentFrameData.MetaData + (currentFrameData.MoveCount * sizeof(DXGI_OUTDUPL_MOVE_RECT)));

                if (!dirtyRectArray)
                {
                    return -1;
                }

                rect_count = currentFrameData.DirtyCount;

                for(UINT index = 0; index < rect_count; index ++)
                {

                    if(m_UseD3D11BitmapConversion)
                    {
                        RECT dirtyRect;

                        dirtyRect.bottom = dirtyRectArray[index].bottom * m_ImageScalingFactor;
                        dirtyRect.top = dirtyRectArray[index].top * m_ImageScalingFactor;
                        dirtyRect.left = dirtyRectArray[index].left * m_ImageScalingFactor;
                        dirtyRect.right = dirtyRectArray[index].right * m_ImageScalingFactor;

                        diffArea += abs((dirtyRect.right - dirtyRect.left) * 
                        (dirtyRect.bottom - dirtyRect.top));

                        dirtyRects.push_back(dirtyRect);
                    }
                    else
                    {
                        diffArea += abs((dirtyRectArray[index].right - dirtyRectArray[index].left) * 
                        (dirtyRectArray[index].bottom - dirtyRectArray[index].top));

                        dirtyRects.push_back(dirtyRectArray[index]);
                    }
                }

            }

        }

    return diffArea;

}

CATCH_ALL(e)
{ 
    LOG(CRITICAL) << _T("Exception in getChangedRegions");
}
END_CATCH_ALL

return -1;
}

Here is the code to init the device

       //
    // Initialize duplication interfaces
    //
    HRESULT cDuplicationManager::InitDupl(_In_ ID3D11Device* Device, _In_ IDXGIAdapter *_pAdapter, _In_ IDXGIOutput *_pOutput, _In_ UINT Output)
    {
    HRESULT hr = E_FAIL;

    if(!_pOutput || !_pAdapter || !Device)
    {
        return hr;
    }

    m_OutputNumber = Output;
 
    // Take a reference on the device
    m_Device = Device;
    m_Device->AddRef();

    /*
    // Get DXGI device
    IDXGIDevice* DxgiDevice = nullptr;
    HRESULT hr = m_Device->QueryInterface(__uuidof(IDXGIDevice), reinterpret_cast<void**>(&DxgiDevice));
    if (FAILED(hr))
    {
        return ProcessFailure(nullptr, _T("Failed to QI for DXGI Device"), _T("Error"), hr);
    }
 
    // Get DXGI adapter
    IDXGIAdapter* DxgiAdapter = nullptr;
    hr = DxgiDevice->GetParent(__uuidof(IDXGIAdapter), reinterpret_cast<void**>(&DxgiAdapter));
    DxgiDevice->Release();
    DxgiDevice = nullptr;
    if (FAILED(hr))
    {
        return ProcessFailure(m_Device, _T("Failed to get parent DXGI Adapter"), _T("Error"), hr);//, SystemTransitionsExpectedErrors);
    }
 
    // Get output
    IDXGIOutput* DxgiOutput = nullptr;
    hr = DxgiAdapter->EnumOutputs(Output, &DxgiOutput);
    DxgiAdapter->Release();
    DxgiAdapter = nullptr;
    if (FAILED(hr))
    {
        return ProcessFailure(m_Device, _T("Failed to get specified output in DUPLICATIONMANAGER"), _T("Error"), hr);//, EnumOutputsExpectedErrors);
    }

    DxgiOutput->GetDesc(&m_OutputDesc);

     IDXGIOutput1* DxgiOutput1 = nullptr;
    hr = DxgiOutput->QueryInterface(__uuidof(DxgiOutput1), reinterpret_cast<void**>(&DxgiOutput1));

    */

    _pOutput->GetDesc(&m_OutputDesc);
     // QI for Output 1
    IDXGIOutput1* DxgiOutput1 = nullptr;
    hr = _pOutput->QueryInterface(__uuidof(DxgiOutput1), reinterpret_cast<void**>(&DxgiOutput1));

    if (FAILED(hr))
    {
        return ProcessFailure(nullptr, _T("Failed to QI for DxgiOutput1 in DUPLICATIONMANAGER"), _T("Error"), hr);
    }
 
    // Create desktop duplication
    hr = DxgiOutput1->DuplicateOutput(m_Device, &m_DeskDupl);

    DxgiOutput1->Release();
    DxgiOutput1 = nullptr;


    if (FAILED(hr) || !m_DeskDupl)
    {
        if (hr == DXGI_ERROR_NOT_CURRENTLY_AVAILABLE)
        {
            return ProcessFailure(nullptr, _T("Maximum number of applications using Desktop Duplication API"), _T("Error"), hr);
        }
        return ProcessFailure(m_Device, _T("Failed to get duplicate output in DUPLICATIONMANAGER"), _T("Error"), hr);//, CreateDuplicationExpectedErrors);
    }
 
    return S_OK;
}

Finally to get the current frame and difference to the previous one:

   //
// Get next frame and write it into Data
//
_Success_(*Timeout == false && return == DUPL_RETURN_SUCCESS)
HRESULT cDuplicationManager::GetFrame(_Out_ FRAME_DATA* Data, int timeout, _Out_ bool* Timeout)
{
    IDXGIResource* DesktopResource = nullptr;
    DXGI_OUTDUPL_FRAME_INFO FrameInfo;
    
    try
    {
         // Get new frame
        HRESULT hr = m_DeskDupl->AcquireNextFrame(timeout, &FrameInfo, &DesktopResource);

        if (hr == DXGI_ERROR_WAIT_TIMEOUT)
        {
            *Timeout = true;
            return S_OK;
        }

        *Timeout = false;
 
        if (FAILED(hr))
        {
            return ProcessFailure(m_Device, _T("Failed to acquire next frame in DUPLICATIONMANAGER"), _T("Error"), hr);//, FrameInfoExpectedErrors);
        }
 
        // If still holding old frame, destroy it
        if (m_AcquiredDesktopImage)
        {
            m_AcquiredDesktopImage->Release();
            m_AcquiredDesktopImage = nullptr;
        }
 
        if (DesktopResource)
        {
            // QI for IDXGIResource
            hr = DesktopResource->QueryInterface(__uuidof(ID3D11Texture2D), reinterpret_cast<void **>(&m_AcquiredDesktopImage));
            DesktopResource->Release();
            DesktopResource = nullptr;
        }

        if (FAILED(hr))
        {
            return ProcessFailure(nullptr, _T("Failed to QI for ID3D11Texture2D from acquired IDXGIResource in DUPLICATIONMANAGER"), _T("Error"), hr);
        }
 
        // Get metadata
        if (FrameInfo.TotalMetadataBufferSize)
        {
            // Old buffer too small
            if (FrameInfo.TotalMetadataBufferSize > m_MetaDataSize)
            {
                if (m_MetaDataBuffer)
                {
                    delete [] m_MetaDataBuffer;
                    m_MetaDataBuffer = nullptr;
                }

                m_MetaDataBuffer = new (std::nothrow) BYTE[FrameInfo.TotalMetadataBufferSize];

                if (!m_MetaDataBuffer)
                {
                    m_MetaDataSize = 0;
                    Data->MoveCount = 0;
                    Data->DirtyCount = 0;
                    return ProcessFailure(nullptr, _T("Failed to allocate memory for metadata in DUPLICATIONMANAGER"), _T("Error"), E_OUTOFMEMORY);
                }

                m_MetaDataSize = FrameInfo.TotalMetadataBufferSize;
            }
 
            
            UINT BufSize = FrameInfo.TotalMetadataBufferSize;
 
            // Get move rectangles

        
            hr = m_DeskDupl->GetFrameMoveRects(BufSize, reinterpret_cast<DXGI_OUTDUPL_MOVE_RECT*>(m_MetaDataBuffer), &BufSize);

            if (FAILED(hr))
            {
                Data->MoveCount = 0;
                Data->DirtyCount = 0;
                return ProcessFailure(nullptr, L"Failed to get frame move rects in DUPLICATIONMANAGER", L"Error", hr);//, FrameInfoExpectedErrors);
            
            }
        
            Data->MoveCount = BufSize / sizeof(DXGI_OUTDUPL_MOVE_RECT);
 
            BYTE* DirtyRects = m_MetaDataBuffer + BufSize;
            BufSize = FrameInfo.TotalMetadataBufferSize - BufSize;
 
            // Get dirty rectangles
            hr = m_DeskDupl->GetFrameDirtyRects(BufSize, reinterpret_cast<RECT*>(DirtyRects), &BufSize);

            if (FAILED(hr))
            {
                Data->MoveCount = 0;
                Data->DirtyCount = 0;
                return ProcessFailure(nullptr, _T("Failed to get frame dirty rects in DUPLICATIONMANAGER"), _T("Error"), hr);//, FrameInfoExpectedErrors);
            }

            Data->DirtyCount = BufSize / sizeof(RECT);
 
            Data->MetaData = m_MetaDataBuffer;
        }
 
        Data->Frame = m_AcquiredDesktopImage;
        Data->FrameInfo = FrameInfo;

    }
    catch (...)
    {
        return S_FALSE;
    }

    return S_OK;
}

Update :

Failed to acquire next frame in DUPLICATIONMANAGER is getting printed whenever the device has hung (That is in the mid of streaming the screens, Ex: Continuously capturing a video and sending it to the other end)

// Get new frame
    HRESULT hr = m_DeskDupl->AcquireNextFrame(timeout, &FrameInfo, &DesktopResource);

    if (hr == DXGI_ERROR_WAIT_TIMEOUT)
    {
        *Timeout = true;
        return S_OK;
    }

    *Timeout = false;

    if (FAILED(hr))
    {
        return ProcessFailure(m_Device, _T("Failed to acquire next frame in DUPLICATIONMANAGER"), _T("Error"), hr);//, FrameInfoExpectedErrors);
    }

here is the detailed error info :

Id3d11DuplicationManager::ProcessFailure - Error: Failed to acquire next frame in DUPLICATIONMANAGER, Detail: The keyed mutex was abandoned.

Update 2 : I have got the error code whenever the device failed to give screen updates forever, And here is the same

Id3d11DuplicationManager::ProcessFailure - Error: Failed to get duplicate output in DUPLICATIONMANAGER, Detail: Access is denied.

The error code is E_ACCESSDENIED.

I do not understand why I am getting this error as I am running in SYSTEM mode already and the SetThreadDesktop had been executed twice (One during the init and another after detecting a failure)

This is what the explanation of the error on MSDN : E_ACCESSDENIED if the application does not have access privilege to the current desktop image. For example, only an application that runs at LOCAL_SYSTEM can access the secure desktop.

Is there anything else that would result in this kind of issue?

1

There are 1 answers

0
iamrameshkumar On BEST ANSWER

It's always good to check the return codes and immediately fall back to GDI or any other available screen capturing approach in case of non-recoverable errors. Retrying doesn't work most of the time for certain hardware errors like max limit reached, out of memory, device removed, etc, I learned it in a hard way. Furthermore, DirectX device takes a few iterations before producing an initial frame on rare occasions. It wouldn't be useful to retry more than 10 times, you can safely fallback or try re-initializing the device to check one more time before falling back.

Here are some basic checks to do:

Handle DXGI_ERROR_NOT_CURRENTLY_AVAILABLE error:

_pOutput->GetDesc(&m_OutputDesc);
// QI for Output 1
IDXGIOutput1* DxgiOutput1 = nullptr;
hr = _pOutput->QueryInterface(__uuidof(DxgiOutput1), reinterpret_cast<void**>(&DxgiOutput1));

if (FAILED(hr))
{
    return ProcessFailure(nullptr, _T("Failed to QI for DxgiOutput1 in DUPLICATIONMANAGER"), _T("Error"), hr);
}

// Create desktop duplication
hr = DxgiOutput1->DuplicateOutput(m_Device, &m_DeskDupl);

DxgiOutput1->Release();
DxgiOutput1 = nullptr;

if (FAILED(hr) || !m_DeskDupl)
{
    if (hr == DXGI_ERROR_NOT_CURRENTLY_AVAILABLE)
    {
        return ProcessFailure(nullptr, _T("Maximum number of applications using Desktop Duplication API"), _T("Error"), hr);
    }

    return ProcessFailure(m_Device, _T("Failed to get duplicate output in DUPLICATIONMANAGER"), _T("Error"), hr);//, CreateDuplicationExpectedErrors);
}

Check for device removed(DXGI_ERROR_DEVICE_REMOVED) or Device reset(DXGI_ERROR_DEVICE_RESET) & Out of memory(E_OUTOFMEMORY) error codes (I have received E_OUTOFMEMORY sometimes, though it's uncommon):

 HRESULT ProcessFailure(_In_opt_ ID3D11Device* Device, _In_ LPCWSTR Str, _In_ LPCWSTR Title, HRESULT hr)//, _In_opt_z_ HRESULT* ExpectedErrors = NULL)
 {
    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;
  }

_com_error err(TranslatedHr);
LPCTSTR errMsg = err.ErrorMessage();

return TranslatedHr;
}

Furthermore, Desktop duplication requires a real graphics device to be active in order to work. You may get E_ACCESSDENIED otherwise.

There are also other scenarios you may get this error, like, Desktop switch cases, abandoned keyed mutex. You can try reinitializing the device in such cases.

I have also uploaded my sample project here.