1

When capturing the desktop using Vortice with the code below it works just fine. Although, when I try to use any other value than 0 for the X or Y location of the rectangle it only returns frame as a black bitmap. The code I used for reference is located here: https://github.com/diogotr7/DesktopDuplicationSamples/blob/master/VorticeCore/Program.cs, but it suffers from the same problem. I have tried altering most values in the var frame and var mapDest with not much luck.

public void Start()
{
    _isCapturing = true;
    DXGI.CreateDXGIFactory1<IDXGIFactory1>(out var factory);
    if (factory == null)
    {
        return;
    }
    var adapter = factory.GetAdapter(0);
    var output = adapter.GetOutput(0);
    var output1 = output.QueryInterface<IDXGIOutput1>();
    //D3D12.D3D12CreateDevice(adapter, FeatureLevel.Level_12_0, out ID3D12Device? device);
    D3D11.D3D11CreateDevice(adapter, DriverType.Unknown, DeviceCreationFlags.None, _featureLevels, out var device);
    if (device == null)
        throw new Exception("Unable to Locate Device.");

    // Width/Height of desktop to capture
    Rectangle rectangle = new Rectangle(0, 0,
            output.Description.DesktopCoordinates.Right,
            output.Description.DesktopCoordinates.Bottom);

    // Create Staging texture CPU-accessible
    var texture2dDescription = new Texture2DDescription
    {
        ArraySize = 1,
        BindFlags = BindFlags.None,
        CPUAccessFlags = CpuAccessFlags.Read | CpuAccessFlags.Write,
        Format = Format.B8G8R8A8_UNorm,
        Height = rectangle.Bottom,
        MipLevels = 1,
        SampleDescription = { Count = 1, Quality = 0 },
        Usage = ResourceUsage.Staging,
        Width = rectangle.Right
    };
    
    Task.Factory.StartNew(() =>
    {
        // Duplicate the output
        using var duplicatedOutput = output1.DuplicateOutput(device);
        while (_isCapturing)
        {
            try
            {
                var currentFrame = device.CreateTexture2D(texture2dDescription);

                Thread.Sleep(50);
                rectangle.X = 0;
                duplicatedOutput.AcquireNextFrame(100, out var frameInfo, out var desktopResource);
                if (desktopResource == null)
                    continue;
                var tempTexture = desktopResource.QueryInterface<ID3D11Texture2D>();
                device.ImmediateContext.CopyResource(currentFrame, tempTexture);
                var dataBox = device.ImmediateContext.Map(currentFrame, 0);
                var frame = new Bitmap(rectangle.Right, rectangle.Bottom, PixelFormat.Format32bppRgb);
                var mapDest = frame.LockBits(rectangle, ImageLockMode.WriteOnly, frame.PixelFormat);
                for (int y = rectangle.Y, sizeInBytesToCopy = rectangle.Width * 4; y < rectangle.Height; y++)
                {
                    MemoryHelpers.CopyMemory(mapDest.Scan0 + y * rectangle.Right * 4,
                        dataBox.DataPointer + y * dataBox.RowPitch, sizeInBytesToCopy);
                }

                frame.UnlockBits(mapDest);
                ScreenRefreshed?.Invoke(this, frame);
                desktopResource.Dispose();
                frame.Dispose();
                tempTexture.Dispose();
                currentFrame.Dispose();
            }
            catch (Exception e)
            {
                if (e.HResult != Vortice.DXGI.ResultCode.WaitTimeout.Code)
                {
                    Trace.TraceError(e.Message);
                    Trace.TraceError(e.StackTrace);
                }
            }

            duplicatedOutput.ReleaseFrame();
        }
    });
}
Simon Mourier
  • 132,049
  • 21
  • 248
  • 298

1 Answers1

1

There are issues with your code. Here are the issue that explain why it doesn't work more than once and requires a strange Sleep call:

  • You don't dispose everything (you should, I guess this is releasing underlying COM interfaces)
  • You don't Unmap what you've mapped.
  • You don't ReleaseFrame acquired frames.

But there are other issues

  • You can use the device context created by D3D11CreateDevice, you don't need to use this ImmediateContext property.
  • If you want to maximize duplication performance, you shouldn't use CPU mapped textures directly in the acquiring frames loop which you must keep very fast. You should copy them (like you do) and pass them to the CPU using another thread for example
  • The Duplication API is not a screen-capture API, it's a screen duplication API (!) which means it will copy rendered frames to like a "mailbox" you query when you can and want. The timeout can be kept relatively important. When you get a timeout, it just means nothing has changed on the screen. The frameInfo parameter has useful information about all this.

Here is a version that seems to work (acquires 10 frames and saves them as bitmap files):

class Program
{
    private static readonly FeatureLevel[] _featureLevels = new[]
    {
        FeatureLevel.Level_11_0,
        FeatureLevel.Level_10_1,
        FeatureLevel.Level_10_0,
        FeatureLevel.Level_9_3,
        FeatureLevel.Level_9_2,
        FeatureLevel.Level_9_1,
    };

    static void Main()
    {
        DXGI.CreateDXGIFactory1<IDXGIFactory1>(out var factory);
        if (factory == null)
            return;

        using (factory)
        {
            using var adapter = factory.GetAdapter(0);
            using var output = adapter.GetOutput(0);
            using var output1 = output.QueryInterface<IDXGIOutput1>();
            D3D11.D3D11CreateDevice(adapter, DriverType.Unknown, DeviceCreationFlags.None, _featureLevels, out var device, out var deviceContext);
            if (device == null)
                return;

            using (device)
            {
                var rectangle = new Rectangle(0, 0, output.Description.DesktopCoordinates.Right, output.Description.DesktopCoordinates.Bottom);
                var texture2dDescription = new Texture2DDescription
                {
                    ArraySize = 1,
                    CPUAccessFlags = CpuAccessFlags.Read | CpuAccessFlags.Write,
                    Format = Format.B8G8R8A8_UNorm,
                    MipLevels = 1,
                    SampleDescription = { Count = 1, Quality = 0 },
                    Usage = ResourceUsage.Staging,
                    Height = rectangle.Bottom,
                    Width = rectangle.Right
                };

                using var currentFrame = device.CreateTexture2D(texture2dDescription);
                using var duplicatedOutput = output1.DuplicateOutput(device);
                using var frame = new Bitmap(rectangle.Right, rectangle.Bottom, PixelFormat.Format32bppRgb);
                var index = 0;
                rectangle.X = 0;
                do
                {
                    duplicatedOutput.AcquireNextFrame(500, out var frameInfo, out var desktopResource);
                    if (desktopResource != null)
                    {
                        using (desktopResource)
                        {
                            using var tempTexture = desktopResource.QueryInterface<ID3D11Texture2D>();
                            deviceContext.CopyResource(currentFrame, tempTexture);
                            var dataBox = deviceContext.Map(currentFrame, 0, MapMode.Read);
                            var mapDest = frame.LockBits(rectangle, ImageLockMode.WriteOnly, frame.PixelFormat);
                            for (int y = rectangle.Y, sizeInBytesToCopy = rectangle.Width * 4; y < rectangle.Height; y++)
                            {
                                MemoryHelpers.CopyMemory(mapDest.Scan0 + y * rectangle.Right * 4, dataBox.DataPointer + y * dataBox.RowPitch, sizeInBytesToCopy);
                            }
                            deviceContext.Unmap(currentFrame, 0);
                            frame.UnlockBits(mapDest);
                            frame.Save("bitmap" + index++ + ".png", ImageFormat.Png);
                        }
                    }

                    duplicatedOutput.ReleaseFrame();
                }
                while (index < 10);
            }
        }
    }
}
Simon Mourier
  • 132,049
  • 21
  • 248
  • 298
  • Thank you for the code. I understand that my code suffers from some memory issues, and the code you provided will help fix that. Unfortunately though. the code provided works to the same effect as the code I provided when it comes to capturing the whole desktop. The problem arises when you try to capture a portion of the screen. If you make the X or the Y something other than 0 in the var rectangle, it will only produce a black screen. I apologize for not making my initial question precise enough in that regard. – Jon Huthsing Jun 21 '22 at 18:56
  • The code you provided doesn't work at all. You can crop what you've captured directly in the GPU pretty easily. If you want to capture a given window you can use WinRT Windows.Graphics.Capture namespace API https://learn.microsoft.com/en-us/windows/uwp/audio-video-camera/screen-capture which works in Win32 desktop apps too – Simon Mourier Jun 21 '22 at 21:42
  • The code does work to capture the entirety of the desktop in my application, which can be seen here: https://i.ibb.co/NV5738V/image.png I can rewrite my question to be a standalone if you like. All you have to do to test my code is replace ScreenRefreshed Event with your frame.Save. If you would like to test it yourself. I was originally using Windows.Graphics.Capture to capture a specific application, but unfortunately it is not as performant as I would like which is why I switched to using IDXGI. I believe my issue lies within within CopyMemory, I am just unsure of how to fix it. – Jon Huthsing Jun 21 '22 at 22:10