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 Task
s 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 Task
s 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();
}
}