SharpDX ScreenCapture

Posted on

Problem

I’m currently working on a monitor screen capture with C# and the SharpDX library. The code works and allow recording bitmap screenshots (dynamically for DirectX 11 games and desktop) in a loop.

But I have a little performance problem in my loop and I’m trying to get 60 records per second at least (I reach 40 records/sec max on my computer).

As you can see, I was tried to use Tasks to perform this but it’s not enough and I’m not an expert in parallel programming.

I hope that my project will be useful to you and it would be great if someone would have some optimizations to suggest to me.

class DXCapture
{
    PictureBox pictureBox;
    bool ready = false;
    bool Recording = false;
    Task RecordingTask;
    int saveSizeX;
    int saveSizeY;
    int width;
    int height;
    int numAdapter;
    int numOutput;
    Factory1 factory;
    Adapter1 adapter;
    SharpDX.Direct3D11.Device device;
    Output output;
    Output1 output1;
    Texture2DDescription texture2DDescription;
    Texture2D screenTexture;
    OutputDuplication duplicatedOutput;
    SharpDX.DXGI.Resource screenResource;


    public DXCapture(PictureBox pictureBox, BenchMark bench, int sizeX = 1920, int sizeY = 1080, int numAdapter = 0, int numOutput = 0)
    {
        this.pictureBox = pictureBox;
        this.saveSizeX = sizeX;
        this.saveSizeY = sizeY;
        this.numAdapter = numAdapter;
        this.numOutput = numOutput;
        this.bench = bench;
        InitDX();
    }

    public void InitDX()
    {
        try
        {
            factory = new SharpDX.DXGI.Factory1();
            adapter = factory.GetAdapter1(numAdapter);
            device = new SharpDX.Direct3D11.Device(adapter);
            output = adapter.GetOutput(numOutput);
            output1 = output.QueryInterface<Output1>();

            // Width/Height of desktop to capture
            width = output.Description.DesktopBounds.Left + output.Description.DesktopBounds.Right;
            height = output.Description.DesktopBounds.Top + output.Description.DesktopBounds.Bottom;

            texture2DDescription = new Texture2DDescription
            {
                CpuAccessFlags = CpuAccessFlags.Read,
                BindFlags = BindFlags.None,
                Format = Format.B8G8R8A8_UNorm,
                Width = width,
                Height = height,
                OptionFlags = ResourceOptionFlags.None,
                MipLevels = 1,
                ArraySize = 1,
                SampleDescription = { Count = 1, Quality = 0 },
                Usage = ResourceUsage.Staging
            };

            screenTexture = new Texture2D(device, texture2DDescription);
            duplicatedOutput = output1.DuplicateOutput(device);
            screenResource = null;
            ready = true;
        }
        catch
        {
            Console.WriteLine("Error InitDX");
        }
    }

    public void DisposeDX()
    {
        factory.Dispose();
        factory = null;
        adapter.Dispose();
        adapter = null;
        device.Dispose();
        device = null;
        output.Dispose();
        output = null;
        output1.Dispose();
        output1 = null;

        screenTexture.Dispose();
        screenTexture = null;
        duplicatedOutput.Dispose();
        duplicatedOutput = null;
        GC.Collect();
    }

    private void GetShot()
    {
        try
        {
            OutputDuplicateFrameInformation duplicateFrameInformation;
            SharpDX.DXGI.Resource screenResource = null;
            // Try to get duplicated frame within given time
            duplicatedOutput.AcquireNextFrame(10, out duplicateFrameInformation, out screenResource);

            // copy resource into memory that can be accessed by the CPU
            using (var screenTexture2D = screenResource.QueryInterface<Texture2D>())
            {
                device.ImmediateContext.CopyResource(screenTexture2D, screenTexture);
            }

            // Get the desktop capture texture
            var mapSource = device.ImmediateContext.MapSubresource(screenTexture, 0, MapMode.Read, SharpDX.Direct3D11.MapFlags.None);
            var boundsRect = new System.Drawing.Rectangle(0, 0, width, height);

            Task.Factory.StartNew(() =>
            {
                // Create Drawing.Bitmap
                using (var bitmap = new System.Drawing.Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format32bppArgb))
                {
                    // Copy pixels from screen capture Texture to GDI bitmap
                    var bitmapData = bitmap.LockBits(boundsRect, ImageLockMode.WriteOnly, bitmap.PixelFormat);
                    var sourcePtr = mapSource.DataPointer;
                    var destinationPtr = bitmapData.Scan0;
                    for (int y = 0; y < height; y++)
                    {
                        // Copy a single line 
                        Utilities.CopyMemory(destinationPtr, sourcePtr, width * 4);

                        // Advance pointers
                        sourcePtr = IntPtr.Add(sourcePtr, mapSource.RowPitch);
                        destinationPtr = IntPtr.Add(destinationPtr, bitmapData.Stride);
                    }

                    // Release source and dest locks
                    bitmap.UnlockBits(bitmapData);

                    device.ImmediateContext.UnmapSubresource(screenTexture, 0);

                    // instant preview in picture box
                    if (pictureBox != null)
                    {
                        pictureBox.Invoke(new Action(() =>
                        {
                            pictureBox.Image = (System.Drawing.Bitmap)bitmap.Clone();
                        }));
                    }

                    // save the bitmap image
                    // ...
                }

            });// end task bitmap creation

        }
        catch (SharpDXException e)
        {
            if(e.ResultCode.Code == SharpDX.DXGI.ResultCode.AccessLost.Result.Code)
            {
                Console.WriteLine("Error GetShot ACCESS LOST - relaunch in 2s !");
                Thread.Sleep(2000);
                DisposeDX();
                GC.Collect();
                InitDX();
            }
            else if (e.ResultCode.Code != SharpDX.DXGI.ResultCode.WaitTimeout.Result.Code)
            {
                Console.WriteLine("Error GetShot");
                throw;
            }
        }
        finally
        {
            try
            {
                // Dispose manually
                if (screenResource != null)
                {
                    screenResource.Dispose();
                    screenResource = null;
                    duplicatedOutput.ReleaseFrame();
                }

                // force the Garbage Collector to cleanup memory to prevent memory leaks
                Task.Factory.StartNew(() => { GC.Collect(); });
            }
            catch(Exception e)
            {
                Console.WriteLine("Error GetShot finnaly - relaunch in 2s !");
                Thread.Sleep(2000);
                DisposeDX();
                GC.Collect();
                InitDX();
            }

        }
    }

    public void ScreenShot()
    {
        if(ready)
        {
            GetShot();
        }
    }

    public void StartRecord(int limitFPS = 0)
    {
        if(!Recording)
        {
            Recording = true;
            RecordingTask = new Task(() =>
            {
                while (Recording)
                {
                    GetShot();
                }

            });

            RecordingTask.Start();
        }

    }

    public void StopRecord()
    {
        Recording = false;
    }

}

Solution

Console.WriteLine("Error GetShot ACCESS LOST - relaunch in 2s !");
Thread.Sleep(2000);

This second line might be killing the performance but even if it’s hit only occasionally it’s usually a bad practice. Thread.Sleep is blocking a thread completely. Nothing else can be handled by it during the wait period. You are using Tasks in other places so try to use a Task.Delay here or even better a timer that will trigger a relaunch.

Thread.Sleep(2000);
DisposeDX();
GC.Collect();
InitDX();

I also think you should dispose everything right away and then wait if necessary.


You could also try to make the GetShot method a real async one by changing its signature to

private async Task GetShot()

this will allow you to wait like this

Console.WriteLine("Error GetShot ACCESS LOST - relaunch in 2s !");
await Task.Delay(TimeSpan.FromSeconds(2000));

or to replace

Task.Factory.StartNew(() =>

with

await Task.Run(() =>

which is actaully a shortcut for the StartNew where you add the await keyword.


You’ll also need to adjust the StartRecord method to something like this:

public void StartRecord(int limitFPS = 0)
{
    if (!Recording)
    {
        Recording = true;
        RecordingTask = Task.Run(async () =>
        {
            while (Recording)
            {
                await GetShot();
            }
        });
        RecordingTask.Start();
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *