vitkuz573
vitkuz573

Reputation: 113

Why is Captured Image Data All Zeros When Using DXGI Output Duplication and Direct3D?

I am developing an application to capture screenshots from the desktop using DXGI output duplication and Direct3D in C#. My application initializes DXGI, creates a Direct3D device, and duplicates the output. It captures frames, but the captured image data is all zeros. Below is the complete code of my application, which is designed to save a screenshot to a PNG file:

using Silk.NET.Core.Native;
using Silk.NET.Direct3D11;
using Silk.NET.DXGI;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;

namespace TestApp;

public class Program
{
    public const int DXGI_ERROR_NOT_FOUND = unchecked((int)0x887A0002);

    public unsafe static void Main()
    {
        Console.WriteLine("Initializing DXGI...");

        using var dxgi = DXGI.GetApi();
        using var factory = dxgi.CreateDXGIFactory1<IDXGIFactory1>();

        Console.WriteLine("DXGI Factory created.");

        Console.WriteLine("Enumerating adapters and finding active outputs...");

        uint adapterIndex = 0;
        IDXGIAdapter1* adapter = null;
        IDXGIOutput* activeOutput = null;

        var foundActiveOutput = false;

        while (!foundActiveOutput && factory.EnumAdapters1(adapterIndex, &adapter) != DXGI_ERROR_NOT_FOUND)
        {
            AdapterDesc1 desc;
            adapter->GetDesc1(&desc);

            var adapterDescription = Marshal.PtrToStringUni((nint)desc.Description);

            Console.WriteLine($"Adapter {adapterIndex}: {adapterDescription}, VendorId: {desc.VendorId}, DeviceId: {desc.DeviceId}, SubSysId: {desc.SubSysId}, Revision: {desc.Revision}");

            using var adapterCom = new ComPtr<IDXGIAdapter1>(adapter);
            uint outputIndex = 0;
            IDXGIOutput* output = null;

            while (!foundActiveOutput && adapter->EnumOutputs(outputIndex, &output) != DXGI_ERROR_NOT_FOUND)
            {
                using var outputCom = new ComPtr<IDXGIOutput>(output);

                OutputDesc outputDesc;
                output->GetDesc(&outputDesc);

                var outputName = Marshal.PtrToStringUni((nint)outputDesc.DeviceName);

                Console.WriteLine($"Output {outputIndex} on Adapter {adapterIndex}: {outputName}, Attached to Desktop: {(bool)outputDesc.AttachedToDesktop}, Rotation: {outputDesc.Rotation}");

                if (outputDesc.AttachedToDesktop)
                {
                    Console.WriteLine($"Active output found on adapter {adapterIndex}, resolution: {outputDesc.DesktopCoordinates.Max.X - outputDesc.DesktopCoordinates.Min.X} x {outputDesc.DesktopCoordinates.Max.Y - outputDesc.DesktopCoordinates.Min.Y}");

                    activeOutput = output;
                    foundActiveOutput = true;
                }

                outputIndex++;
            }

            adapterIndex++;
        }

        if (!foundActiveOutput)
        {
            Console.WriteLine("No active output found.");

            return;
        }

        using var activeOutputCom = new ComPtr<IDXGIOutput>(activeOutput);

        OutputDesc activeDesc;
        activeOutput->GetDesc(&activeDesc);

        Console.WriteLine($"Selected Output: {Marshal.PtrToStringUni((nint)activeDesc.DeviceName)}, Total Size: {activeDesc.DesktopCoordinates.Max.X - activeDesc.DesktopCoordinates.Min.X} x {activeDesc.DesktopCoordinates.Max.Y - activeDesc.DesktopCoordinates.Min.Y}");

        Console.WriteLine("Creating Direct3D device...");

        D3DFeatureLevel[] featureLevels =
        [
            D3DFeatureLevel.Level121,
            D3DFeatureLevel.Level120,
            D3DFeatureLevel.Level111,
            D3DFeatureLevel.Level110,
            D3DFeatureLevel.Level101,
            D3DFeatureLevel.Level100,
            D3DFeatureLevel.Level93,
            D3DFeatureLevel.Level92,
            D3DFeatureLevel.Level91
        ];

        ID3D11Device* pDevice = null;
        ID3D11DeviceContext* pImmediateContext = null;
        D3DFeatureLevel chosenFeatureLevel;

        var hr = -1;

        foreach (var level in featureLevels)
        {
            hr = D3D11.GetApi().CreateDevice((IDXGIAdapter*)adapter, D3DDriverType.Unknown, IntPtr.Zero, (uint)CreateDeviceFlag.BgraSupport, &level, 1, D3D11.SdkVersion, &pDevice, &chosenFeatureLevel, &pImmediateContext);

            if (hr == 0)
            {
                Console.WriteLine($"Successfully created device with feature level {chosenFeatureLevel}");
                break;
            }
        }

        if (hr != 0)
        {
            Console.WriteLine($"Failed to create device: HR = {hr:X}");

            return;
        }

        var textureDesc = new Texture2DDesc
        {
            Width = (uint)(activeDesc.DesktopCoordinates.Max.X - activeDesc.DesktopCoordinates.Min.X),
            Height = (uint)(activeDesc.DesktopCoordinates.Max.Y - activeDesc.DesktopCoordinates.Min.Y),
            MipLevels = 1,
            ArraySize = 1,
            Format = Format.FormatR8G8B8A8Unorm,
            SampleDesc = new SampleDesc
            {
                Count = 1,
                Quality = 0
            },
            Usage = Usage.Staging,
            BindFlags = 0,
            CPUAccessFlags = (uint)CpuAccessFlag.Read,
            MiscFlags = 0
        };

        ID3D11Texture2D* stagingTexture = null;

        pDevice->CreateTexture2D(&textureDesc, null, &stagingTexture);

        Console.WriteLine("Staging texture created with dimensions: " + textureDesc.Width + "x" + textureDesc.Height + ", Format: " + textureDesc.Format);

        IDXGIOutput1* output1 = (IDXGIOutput1*)activeOutput;
        IDXGIOutputDuplication* duplicatedOutput;

        hr = output1->DuplicateOutput((IUnknown*)pDevice, &duplicatedOutput);

        if (hr < 0)
        {
            Console.WriteLine($"Failed to duplicate output, HR = {hr:X}");

            return;
        }

        Console.WriteLine("Output duplicated successfully.");

        OutduplFrameInfo frameInfo;
        IDXGIResource* desktopResource = null;

        hr = duplicatedOutput->AcquireNextFrame(1000, &frameInfo, &desktopResource);

        if (hr < 0)
        {
            Console.WriteLine($"Failed to acquire next frame, HR = {hr:X}, Status: {frameInfo.LastPresentTime}, TotalFrames: {frameInfo.TotalMetadataBufferSize}");

            return;
        }

        Console.WriteLine($"Next frame acquired, Last Present Time: {frameInfo.LastPresentTime}, Total Metadata Buffer Size: {frameInfo.TotalMetadataBufferSize}");

        ID3D11Texture2D* desktopTexture;

        hr = desktopResource->QueryInterface(SilkMarshal.GuidPtrOf<ID3D11Texture2D>(), (void**)&desktopTexture);

        if (hr < 0)
        {
            Console.WriteLine($"Failed to query interface for ID3D11Texture2D, HR = {hr:X}");

            return;
        }

        Console.WriteLine("ID3D11Texture2D interface obtained.");

        pImmediateContext->CopyResource((ID3D11Resource*)stagingTexture, (ID3D11Resource*)desktopTexture);

        Console.WriteLine("Resources copied.");

        MappedSubresource mappedResource;
        hr = pImmediateContext->Map((ID3D11Resource*)stagingTexture, 0, Map.Read, 0, &mappedResource);

        if (hr >= 0)
        {
            Console.WriteLine("Staging texture mapped successfully.");

            var dataPtr = (byte*)mappedResource.PData;
            var stride = mappedResource.RowPitch;

            Console.WriteLine($"Data pointer: {(long)dataPtr}, Stride: {stride}");

            var allZero = true;

            for (var i = 0; i < 100; i++) 
            {
                if (dataPtr[i] != 0)
                {
                    allZero = false;
                    break;
                }
            }

            Console.WriteLine(allZero ? "Data is all zeros." : "Data contains non-zero values.");

            using (var bitmap = new Bitmap((int)textureDesc.Width, (int)textureDesc.Height, (int)stride, PixelFormat.Format32bppArgb, (nint)dataPtr))
            {
                var filePath = @"C:\Users\vitaly\Desktop\screenshot.png";
                bitmap.Save(filePath, ImageFormat.Png);

                Console.WriteLine($"Screenshot saved to {filePath}.");
            }

            pImmediateContext->Unmap((ID3D11Resource*)stagingTexture, 0);
        }
        else
        {
            Console.WriteLine($"Failed to map staging texture, HR = {hr:X}");
        }

        Console.WriteLine("Releasing resources.");

        if (pDevice != null)
        {
            pDevice->Release();
        }

        if (pImmediateContext != null)
        {
            pImmediateContext->Release();
        }

        Console.WriteLine("Resources released.");
    }
}

The output shows that everything initializes correctly, and the frame is supposedly captured with the DXGI output duplication API. However, when I map the staging texture and check the data, all bytes are zero. This happens despite the fact that the texture description and copying seem to be set up correctly.

Output from the console indicates that the device and output are found, and no errors are reported during the creation and execution process:

Initializing DXGI...
DXGI Factory created.
Enumerating adapters and finding active outputs...
Adapter 0: Intel(R) HD Graphics 5500, VendorId: 32902, DeviceId: 5654, SubSysId: 443355203, Revision: 9
Output 0 on Adapter 0: \\.\DISPLAY1, Attached to Desktop: True, Rotation: ModeRotationIdentity
Active output found on adapter 0, resolution: 1366 x 768
Selected Output: \\.\DISPLAY1, Total Size: 1366 x 768
Creating Direct3D device...
Successfully created device with feature level D3DFeatureLevel111
Staging texture created with dimensions: 1366x768, Format: FormatR8G8B8A8Unorm
Output duplicated successfully.
Next frame acquired, Last Present Time: 0, Total Metadata Buffer Size: 0
ID3D11Texture2D interface obtained.
Resources copied.
Staging texture mapped successfully.
Data pointer: 2005097365504, Stride: 5504
Data is all zeros.
Screenshot saved to C:\Users\vitaly\Desktop\screenshot.png.
Releasing resources.
Resources released.

Upvotes: 2

Views: 131

Answers (1)

Simon Mourier
Simon Mourier

Reputation: 139157

What you must do first when you program DirectX (D3D, DXGI, Direct2D, etc.) is enable the DirectX debug layer. Once that is done, when running your code in debug mode, you will see this in debug output:

D3D11 ERROR: ID3D11DeviceContext::CopyResource: Cannot invoke CopyResource when the Formats of each Resource are not the same or at least castable to each other, unless one format is compressed (DXGI_FORMAT_R9G9B9E5_SHAREDEXP, or DXGI_FORMAT_BC[1,2,3,4,5,6,7]_* ) and the source format is similar to the dest according to: BC[1|4] ~= R16G16B16A16|R32G32, BC[2|3|5|6|7] ~= R32G32B32A32, R9G9B9E5_SHAREDEXP ~= R32. [ RESOURCE_MANIPULATION ERROR #284: COPYRESOURCE_INVALIDSOURCE]

This is precious information. You should never copy a texture (desktopTexture) to another (stagingTexture) without being certain they have the same formats, and in fact they don't.

So, change this:

var textureDesc = new Texture2DDesc
{
    ...
    Format = Format.FormatR8G8B8A8Unorm,
    ...
};

into this:

var textureDesc = new Texture2DDesc
{
    ...
    Format = Format.FormatB8G8R8A8Unorm,
    ...
};

Now the error is gone but ... you still have all zeroes in your resource.

The reason is DXGI Output Duplication is not a screenshot API, it's an API that acquires (aka copies) a new desktop frame when one is available, so it's really something that is meaningful during "some elapsed amount of time". For example, when nothing moves on the screen, no frame is acquired (and you'll get a timeout, which is expected).

So, in your code the easiest way to see it working is either to run the acquiring code in a loop, or just add a timer before the AcquireNextFrame call.

Thread.Sleep(200); // wait a bit here
hr = duplicatedOutput->AcquireNextFrame(1000, &frameInfo, &desktopResource);

PS: the code is missing Release calls (desktopResource, desktopTexture, etc.)

Upvotes: 2

Related Questions