3

I'm trying to create a Screenshot of all Screens on my PC. In the past I've been using the GDI Method, but due to performance issues I'm trying the DirectX way.

I can take a Screenshot of a single Screen without issues, with a code like this:

using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;
using System.Windows.Forms;
using System.Drawing;    

class Capture : Form
{
    private Device device;
    private Surface surface;

    public Capture()
    {
        PresentParameters p = new PresentParameters();
        p.Windowed = true;
        p.SwapEffect = SwapEffect.Discard;
        device = new Device(0, DeviceType.Hardware, this, CreateFlags.HardwareVertexProcessing, p);
        surface = device.CreateOffscreenPlainSurface(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height, Format.A8B8G8R8, Pool.Scratch);
    }

    public Bitmap Frame()
    {
        GraphicsStream gs = SurfaceLoader.SaveToStream(ImageFileFormat.Jpg, surface);
        return new Bitmap(gs);
    }
}

(Lets ignore deleting the Bitmap from memory for this question)

With that Code I can take a Screenshot of my Primary Screen. Changing the first parameter of the Device constructor to a different number corresponds to a different Screen. If I have 3 Screens and I pass 2 as a parameter, I get a Screenshot of my third Screen.

The issue I have is how to handle capturing all Screens. I came up with the following:

class CaptureScreen : Form
{
    private int index;
    private Screen screen;
    private Device device;
    private Surface surface;
    public Rectangle ScreenBounds { get { return screen.Bounds; } }
    public Device Device { get { return device; } }

    public CaptureScreen(int index, Screen screen, PresentParameters p)
    {
        this.screen = screen; this.index = index;

        device = new Device(index, DeviceType.Hardware, this, CreateFlags.HardwareVertexProcessing, p);
        surface = device.CreateOffscreenPlainSurface(screen.Bounds.Width, screen.Bounds.Height, Format.A8R8G8B8, Pool.Scratch);
    }

    public Bitmap Frame()
    {
        device.GetFrontBufferData(0, surface);
        GraphicsStream gs = SurfaceLoader.SaveToStream(ImageFileFormat.Jpg, surface);
        return new Bitmap(gs);
    }
}

class CaptureDirectX : Form
{
    private CaptureScreen[] screens;
    private int width = 0;
    private int height = 0;

    public CaptureDirectX()
    {
        PresentParameters p = new PresentParameters();
        p.Windowed = true;
        p.SwapEffect = SwapEffect.Discard;
        screens = new CaptureScreen[Screen.AllScreens.Length];
        for (int i = 0; i < Screen.AllScreens.Length; i++)
        {
            screens[i] = new CaptureScreen(i, Screen.AllScreens[i], p);
            //reset previous devices
            if (i > 0)
            {
                for(int j = 0; j < i; j++)
                {
                    screens[j].Device.Reset(p);
                }
            }
            width += Screen.AllScreens[i].Bounds.Width;
            if (Screen.AllScreens[i].Bounds.Height > height)
            {
                height = Screen.AllScreens[i].Bounds.Height;
            }
        }
    }

    public Bitmap Frame()
    {
        Bitmap result = new Bitmap(width, height);
        using (var g = Graphics.FromImage(result))
        {
            for (int i = 0; i < screens.Length; i++)
            {
                Bitmap frame = screens[i].Frame();
                g.DrawImage(frame, screens[i].Bounds);
            }
        }
        return result;
    }
}

As you can see, I iterate though the available Screens and create multiple devices and surfaces in a seperate Class. But calling Frame() of the CaptureDirectX class throws the following error:

An unhandled exception of type 'Microsoft.DirectX.Direct3D.InvalidCallException' occurred in Microsoft.DirectX.Direct3D.dll

At the line

device.GetFrontBufferData(0, surface);

I've been researching this a bit but didn't have a whole lot of success. I'm not really sure what the issue is. I've found a link that offers a solution that's talking about resetting the Device Objects. But as you can see in my code above, I've been trying to reset all previously created Device objects, sadly without success.

So my questions are:

  • Is what I'm trying to achieve even possible through this method (i.e. GetFrontBufferData) ?
  • What am I doing wrong? What am I missing?
  • Do you see any performance issues when capturing the Screen at a high rate, like say 30 fps? (Capturing a single screen with a target of 30fps gave me a rate of about 25 - 30fps, compared with the GDI methology which sinks to like 15fps sometimes)

FYI it's a WPF application, i.e. .NET 4.5

Edit: I should mention that I'm aware of IDXGI_DesktopDuplication but sadly it doesn't fit my requirements. As far as I know, that API is only available from Windows 8 onwards, but I'm trying to get a solution that works from Windows 7 onwards because of my clients.

  • https://stackoverflow.com/questions/25681915/c-direct3d-multiple-screen-capture – VuVirt Aug 02 '17 at 12:14
  • @VuVirt Yeah, i've seen that question, but his solution doesn't work for me. I can create multiple devices just fine, but getting the front buffer from them is the problem. –  Aug 02 '17 at 12:16
  • I think you should call GetFrontBufferData for each screen (providing correct coords) separately. – VuVirt Aug 02 '17 at 13:17
  • @VuVirt Isn't that what I'm doing though? I iterate though `CaptureScreen` Objects, each of them have their own `Device` and `Surface` assigned. Although according to the link I posted, registering a second Device effectively destroys the previous Device I created. –  Aug 02 '17 at 13:22
  • You are creating a new device on each iteration. It seems that the old device is not getting destroyed before the creation of next one. You might need to do screens[j].Device.Dispose on all screens BEFORE the creation of a new device by screens[i] = new CaptureScreen(i, Screen.AllScreens[i], p); You might also need to call GC.Collect after each Dispose or before the creation of the new device. – VuVirt Aug 02 '17 at 13:43
  • You are using the legacy Managed DirectX 1.1 assemblies which have been deprecated for a very long time. You should use SharpDX, SlimDX, or native code instead. See [DirectX and .NET](https://blogs.msdn.microsoft.com/chuckw/2010/12/09/directx-and-net/) – Chuck Walbourn Aug 02 '17 at 23:06
  • Desktop screen capture should be implemented with the `IDXGI_DesktopDuplication` API to achieve the best performance and better reliability. – galop1n Aug 02 '17 at 23:28
  • @galop1n as far as I know, `IDXGI_DesktopDuplication` only works from Windows 8 onwards, but I'm looking for a solution that works with Windows 7. @ChuckWalbourn Thanks, I will give those a look-see. –  Aug 03 '17 at 05:05

1 Answers1

1

Well, in the end the solution was something completely different. The System.Windows.Forms.Screen Class doesn't play nicely with the DirectX Classes. Why? Because the indexes don't match up. The first object in AllScreens does not necessarly have to be index 0 in the Device instatiation.

Now usually this isn't a problem, except when you have a "strange" monitor setup like mine. On the desk I have 3 screens, one vertical (1200,1920), one horizontal (1920, 1200) and another horizontal laptop screen (1920, 1080).

What happened in my case: The first object in AllScreens was the vertical monitor on the left. I try to create a device for index 0, 1200 width and 1920 height. Index 0 corresponds to my main monitor though, i.e. the horizontal monitor in the middle. So I'm essentially going out of the screen bounds with my instatiation. The instatiation doesn't throw an exception and at some point later I try to read the front buffer data. Bam, Exception because I'm trying to take a 1200x1920 screenshot of a monitor that's 1920x1200.

Sadly, even after I got this working, the performance was no good. A single frame of all 3 monitors takes about 300 to 500ms. Even with a single monitor, the execution time was something like 100ms. Not good enough for my usecase. Didn't get the Backbuffer to work either, it just produces black images.

I went back to the GDI method and enhanced it by only updating specific chunks of the bitmap on each Frame() call. You want to capture a 1920x1200 region, which gets cut into 480x300 Rectangles.