user2655904
user2655904

Reputation:

Capturing all Screens with DirectX GetFrontBufferData

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:

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.

Upvotes: 3

Views: 1680

Answers (1)

user2655904
user2655904

Reputation:

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.

Upvotes: 1

Related Questions