How to create bitmap from Surface (SharpDX)
Asked Answered
C

2

6

I am new to DirectX and trying to use SharpDX to capture a screen shot using the Desktop Duplication API.

I am wondering if there is any easy way to create bitmap that I can use in CPU (i.e. save on file, etc.)

I am using the following code the get the desktop screen shot:

var factory = new SharpDX.DXGI.Factory1();
var adapter = factory.Adapters1[0];
var output = adapter.Outputs[0];

var device = new SharpDX.Direct3D11.Device(SharpDX.Direct3D.DriverType.Hardware,
                                                       DeviceCreationFlags.BgraSupport |
                                                       DeviceCreationFlags.Debug);

var dev1 = device.QueryInterface<SharpDX.DXGI.Device1>();

var output1 = output.QueryInterface<Output1>();
var duplication = output1.DuplicateOutput(dev1);
OutputDuplicateFrameInformation frameInfo;
SharpDX.DXGI.Resource desktopResource;
duplication.AcquireNextFrame(50, out frameInfo, out desktopResource);

var desktopSurface = desktopResource.QueryInterface<Surface>();

can anyone please give me some idea on how can I create a bitmap object from the desktopSurface (DXGI.Surface instance)?

Clausewitz answered 15/4, 2013 at 17:16 Comment(0)
M
3

The MSDN page for the Desktop Duplication API tells us the format of the image:

DXGI provides a surface that contains a current desktop image through the new IDXGIOutputDuplication::AcquireNextFrame method. The format of the desktop image is always DXGI_FORMAT_B8G8R8A8_UNORM no matter what the current display mode is.

You can use the Surface.Map(MapFlags, out DataStream) method get access to the data on the CPU.

The code should look like* this:

DataStream dataStream;
desktopSurface.Map(MapFlags.Read, out dataStream);
for(int y = 0; y < surface.Description.Width; y++) {
    for(int x = 0; x < surface.Description.Height; x++) {
        // read DXGI_FORMAT_B8G8R8A8_UNORM pixel:
        byte b = dataStream.Read<byte>();
        byte g = dataStream.Read<byte>();
        byte r = dataStream.Read<byte>();
        byte a = dataStream.Read<byte>();
        // color (r, g, b, a) and pixel position (x, y) are available
        // TODO: write to bitmap or process otherwise
    }
}
desktopSurface.Unmap();

*Disclaimer: I don't have a Windows 8 installation at hand, I'm only following the documentation. I hope this works :)

Mousterian answered 23/4, 2013 at 1:1 Comment(2)
Thanks Jens. However, the desktopSurface.Map(...) function throws the following exception: HRESULT: [0x80070057], Module: [Unknown], ApiCode: [Unknown/Unknown], Message: The parameter is incorrect. I am using win8.Clausewitz
@Clausewitz When I turn on DeviceCreationFlags.Debug for device, I get message that IDXGISurface::Map: This object was not created with CPUAccess flags that allow CPU access.. It seems you have to first copy duplicated surface to intermediate texture with CPUAccess and only then you can read it from CPU (see Dracanus answer)Wavelength
T
7

I've just completed this myself although I am not going to say much about this code!

public byte[] GetScreenData()
    {
        // We want to copy the texture from the back buffer so 
        // we don't hog it.
        Texture2DDescription desc = BackBuffer.Description;
        desc.CpuAccessFlags = CpuAccessFlags.Read;
        desc.Usage = ResourceUsage.Staging;
        desc.OptionFlags = ResourceOptionFlags.None;
        desc.BindFlags = BindFlags.None;

        byte[] data = null;

        using (var texture = new Texture2D(DeviceDirect3D, desc))
        {
            DeviceContextDirect3D.CopyResource(BackBuffer, texture);

            using (Surface surface = texture.QueryInterface<Surface>())
            {
                DataStream dataStream;
                var map = surface.Map(SharpDX.DXGI.MapFlags.Read, out dataStream);
                int lines = (int)(dataStream.Length / map.Pitch);
                data = new byte[surface.Description.Width * surface.Description.Height * 4];

                int dataCounter = 0;
                // width of the surface - 4 bytes per pixel.
                int actualWidth = surface.Description.Width * 4;
                for (int y = 0; y < lines; y++)
                {
                    for (int x = 0; x < map.Pitch; x++)
                    {
                        if (x < actualWidth)
                        {
                            data[dataCounter++] = dataStream.Read<byte>();
                        }
                        else
                        {
                            dataStream.Read<byte>();
                        }
                    }
                }
                dataStream.Dispose();
                surface.Unmap();
            }
        }

        return data;
    }

This will get you a byte[] which can then be used to generate a bitmap.

The following is how I saved to a png Image.

 using (var stream = await file.OpenAsync( Windows.Storage.FileAccessMode.ReadWrite ))
            {
                BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream);
                double dpi = DisplayProperties.LogicalDpi;

                encoder.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Straight,
                    (uint)width, (uint)height, dpi, dpi, pixelData);
                encoder.BitmapTransform.ScaledWidth = (uint)newWidth;
                encoder.BitmapTransform.ScaledHeight = (uint)newHeight;
                await encoder.FlushAsync();
                waiter.Set();
            }

I know this was answered a while ago, and maybe you figured it out by now :3 but if someone else gets stuck I hope this helps!

Towandatoward answered 30/7, 2013 at 16:27 Comment(1)
Hi, your solution doesn't work in my case, I know i' a couple years late for this article but I opened a new Question citing your code. Here is the link if you could help me out : #44909367Bleb
M
3

The MSDN page for the Desktop Duplication API tells us the format of the image:

DXGI provides a surface that contains a current desktop image through the new IDXGIOutputDuplication::AcquireNextFrame method. The format of the desktop image is always DXGI_FORMAT_B8G8R8A8_UNORM no matter what the current display mode is.

You can use the Surface.Map(MapFlags, out DataStream) method get access to the data on the CPU.

The code should look like* this:

DataStream dataStream;
desktopSurface.Map(MapFlags.Read, out dataStream);
for(int y = 0; y < surface.Description.Width; y++) {
    for(int x = 0; x < surface.Description.Height; x++) {
        // read DXGI_FORMAT_B8G8R8A8_UNORM pixel:
        byte b = dataStream.Read<byte>();
        byte g = dataStream.Read<byte>();
        byte r = dataStream.Read<byte>();
        byte a = dataStream.Read<byte>();
        // color (r, g, b, a) and pixel position (x, y) are available
        // TODO: write to bitmap or process otherwise
    }
}
desktopSurface.Unmap();

*Disclaimer: I don't have a Windows 8 installation at hand, I'm only following the documentation. I hope this works :)

Mousterian answered 23/4, 2013 at 1:1 Comment(2)
Thanks Jens. However, the desktopSurface.Map(...) function throws the following exception: HRESULT: [0x80070057], Module: [Unknown], ApiCode: [Unknown/Unknown], Message: The parameter is incorrect. I am using win8.Clausewitz
@Clausewitz When I turn on DeviceCreationFlags.Debug for device, I get message that IDXGISurface::Map: This object was not created with CPUAccess flags that allow CPU access.. It seems you have to first copy duplicated surface to intermediate texture with CPUAccess and only then you can read it from CPU (see Dracanus answer)Wavelength

© 2022 - 2024 — McMap. All rights reserved.