Taking Screenshots with Direct3D 8

by Michael Fötsch
January 26, 2001

This article was originally published at Gary Simmons’ Mr GameMaker.

Screenshot  of the sample application

Abstract

This article walks you through the code of the Screenshot function, which you can use in your Direct3D 8 applications to store a rendered scene to a Windows bitmap file. The full source code of the Screenshot function is available on this site. You can either view the source files (Screenshot.h and Screenshot.cpp) online, or you can download the files along with a sample application as a ZIP file. (The sample application comes with project files for Visual C++ 6.0 and C++Builder 5.)

Note: If you are looking for a screenshot function for DirectDraw or an earlier version of Direct3D, see the tutorial “Using the GDI to Take DirectDraw Screenshots” on this site.

An Overview

In earlier versions of DirectX, one could use the GDI to easily convert the output of a Direct3D application to a device-independent bitmap, which could then be saved to a BMP file. In Direct3D 8, it is no longer possible to retrieve a GDI device context for the front or back buffer surfaces. In fact, Direct3D applications are usually not given any access to the front and back buffer surfaces–by default, the back buffer is not lockable and there is no way to retrieve a pointer to the front buffer interface. However, you can have Direct3D fill a system-memory surface with a copy of the front buffer’s contents. This is what we’ll do in our Screenshot function. The function will have the following declaration:

HRESULT Screenshot(
    LPDIRECT3DDEVICE8 lpDevice,
    const char* lpszFileName = NULL
);

These are the steps that are required to save the output of a Direct3D 8 application to a BMP file:

  1. Query the display mode and create a system-memory surface of the same dimensions as the front buffer.
  2. Have the IDirect3DDevice8::GetFrontBuffer method fill the system-memory surface with a copy of the front buffer’s contents. The GetFrontBuffer method also converts the data into a 32-bit ARGB format.
  3. Lock the system-memory surface by calling the LockRect method and convert the 32-bit bitmap into a 24-bit bottom-up DIB.
  4. Prepare the bitmap file headers and write everything to disk.

Accessing the Front Buffer

The IDirect3DDevice8 interface provides the GetFrontBuffer and GetBackBuffer methods to gain access to the swap chain of a Direct3D 8 application. These methods have the following characteristics:

  • GetBackBuffer–By using this method you can obtain an IDirect3DSurface8 interface pointer for each of the buffers in the swap chain. However, unless you explicitily requested a lockable back buffer when creating the device (by using the D3DPRESENTFLAG_LOCKABLE_BACKBUFFER flag), you are not allowed to lock the surface. The SDK docs mention a “performance cost” when using lockable back buffers, even if they’re not actually locked. Our Screenshot function should not require you to rewrite your device creation code, let alone degrade performance, therefore we won’t use the GetBackBuffer method here.
  • GetFrontBuffer–This method copies the contents of the front buffer to a system-memory surface that is provided by the application. What makes the GetFrontBuffer method especially useful for our purposes is that it converts the data into a 32-bit ARGB format so that we don’t have to handle different formats manually.

Note: When using the GetFrontBuffer method, we’ll always capture the entire screen, which might be undesired in a windowed application. However, the majority of applications are full-screen. Therefore, we’ll ignore this issue.

The GetFrontBuffer method requires us to provide a system-memory surface of the same dimensions as the screen. This surface will be filled with a copy of the front buffer’s contents. So, how do we retrieve the screen dimensions when all we have is a pointer to the IDirect3DDevice8 interface? We can use the GetAdapterDisplayMode method of the IDirect3D8 interface to query information about the current display mode of a given adapter. A pointer to the IDirect3D8 interface can be obtained by calling the GetDirect3D method of the IDirect3DDevice8 interface. The adapter identifier that is expected by the GetAdapterDisplayMode method can be obtained by using the IDirect3DDevice8::GetCreationParameters method. To summarize, these are the required steps to retrieve the screen dimensions:

  1. Call the IDirect3DDevice8::GetDirect3D method to retrieve a pointer to an IDirect3D8 interface.
  2. Call the IDirect3DDevice8::GetCreationParameters method, which returns the identifier of the adapter that the Direct3D device uses.
  3. Call the IDirect3D8::GetAdapterDisplayMode with the adapter identifier that we retrieved in Step 2.

The following code snippet performs these three steps:

D3DDEVICE_CREATION_PARAMETERS dcp;
dcp.AdapterOrdinal = D3DADAPTER_DEFAULT;
lpDevice->GetCreationParameters(&dcp);

D3DDISPLAYMODE dm;
dm.Width = dm.Height = 0;

// retrieve pointer to IDirect3D8 interface,
// which provides the GetAdapterDisplayMode method
LPDIRECT3D8 lpD3D = NULL;
lpDevice->GetDirect3D(&lpD3D);
if (lpD3D)
{
    // query the screen dimensions of the current adapter
    lpD3D->GetAdapterDisplayMode(dcp.AdapterOrdinal, &dm);
    SAFERELEASE(lpD3D);
}

Now we can pass the values in dm.Width and dm.Height to the IDirect3DDevice8::CreateImageSurface method to create a system-memory surface that can be used by the GetFrontBuffer method. The expected format of the surface is D3DFMT_A8R8G8B8, which means that there are 8 bits each for the blue, green, red, and alpha components of the colors. The following code snippet creates the surface and calls the GetFrontBuffer method to fill the surface:

LPDIRECT3DSURFACE8 lpSurface = NULL;
lpDevice->CreateImageSurface(
    dm.Width, dm.Height,
    D3DFMT_A8R8G8B8,
    &lpSurface
);

lpDevice->GetFrontBuffer(lpSurface);

Converting the Surface Data to a DIB

In the next step, we’ll convert the data in the system-memory surface pointed to by the lpSurface variable to a 24-bit bottom-up device-independent bitmap (DIB). Although the current format of the surface data would be perfectly valid for a DIB, many bitmap viewers might not support 32-bit top-down BMP files. (And I don’t want to hear “Your code is broken. Paint Shop Pro doesn’t read your files” this time. ;-) So, there are two things that we should do–flip the bitmap vertically and strip the 8 (unused) alpha bits from every pixel.

First of all, we need to gain direct access to the surface data. We do this by calling the IDirect3DSurface8::LockRect method, which returns a pointer to the bitmap bits and the pitch of the surface in a D3DLOCKED_RECT structure. There are dm.Height rows of dm.Width pixels in the array of bitmap bits. Each pixel takes up 4 bytes (a DWORD). It is guaranteed that there are no additional bytes at the end of each scan line, which means we can ignore the Pitch member of the D3DLOCKED_RECT structure this time.

The following code reserves memory for the resulting 24-bit DIB, locks the system-memory surface, and converts the data:

LPBYTE Bits = NULL;
Bits = new BYTE[dm.Width * dm.Height * 3 + 1];
if (!Bits)
{
    throw runtime_error("Function \"Screenshot\": "
        "Could not allocate memory for bitmap bits.");
}

// lock the surface for reading
D3DLOCKED_RECT LockedRect;
if (FAILED(lpSurface->LockRect(&LockedRect, NULL, D3DLOCK_READONLY)))
{
    throw runtime_error("Function \"Screenshot\": "
        "IDirect3DSurface8::LockRect failed.");
}

// flip the bitmap vertically (because that's how DIBs are stored)
// and convert it from 32-bits to 24-bits (some bitmap viewers can't
// handle 32-bit bitmaps, although it's a valid format)

LPDWORD lpSrc;
LPBYTE lpDest = Bits;

// read pixels beginning with the bottom scan line
for (int y = dm.Height - 1; y >= 0; y--)
{
    // calculate address of the current source scan line
    lpSrc = reinterpret_cast<LPDWORD>(LockedRect.pBits) + y * dm.Width;
    for (int x = 0; x < dm.Width; x++)
    {
        // store the source pixel in the bitmap bits array
        *reinterpret_cast<LPDWORD>(lpDest) = *lpSrc;
        lpSrc++;        // increment source pointer by 1 DWORD
        lpDest += 3;    // increment destination pointer by 3 bytes
    }
}

// we can unlock and release the surface
lpSurface->UnlockRect();
SAFERELEASE(lpSurface);

Storing the DIB to a Bitmap File

Finally, we have to prepare a bitmap file header and a bitmap info header so that we can write the DIB to a Windows bitmap (BMP) file. The following code fills a BITMAPFILEHEADER structure, which is the first structure in a BMP file:

// prepare the bitmap file header
BITMAPFILEHEADER bmfh;
bmfh.bfType = BITMAP_FILE_SIGNATURE;
bmfh.bfSize = sizeof(bmfh) + sizeof(bmih) + bmih.biSizeImage;
bmfh.bfReserved1 = bmfh.bfReserved2 = 0;
bmfh.bfOffBits = sizeof(bmfh) + sizeof(bmih);

BITMAP_FILE_SIGNATURE is defined as ‘BM’ for Borland compilers and as ‘MB’ for Visual C++. In both cases, BITMAP_FILE_SIGNATURE ends up in the file as ‘BM’. The bfSize member of the structure contains the size, in bytes, of the entire file. The bfOffBits member specifies the distance, in bytes, from the beginning of the file to the array of bitmap bits.

Next, we fill a BITMAPINFOHEADER structure, which will be stored to the file immediately following the file header:

// prepare the bitmap info header
BITMAPINFOHEADER bmih;
bmih.biSize = sizeof(bmih);
bmih.biWidth = dm.Width;
bmih.biHeight = dm.Height;
bmih.biPlanes = 1;
bmih.biBitCount = 24;
bmih.biCompression = BI_RGB;
bmih.biSizeImage = dm.Width * dm.Height * 3;
bmih.biXPelsPerMeter = 0;
bmih.biYPelsPerMeter = 0;
bmih.biClrUsed = 0;
bmih.biClrImportant = 0;

These two structures and the array of bitmap bits (as pointed to by the Bits variable) can now be written to a file. The following code does this:

// create the BMP file
f = fopen(FileName.c_str(), "wb");
if (!f)
{
    char Msg[MAX_PATH + 256];
    wsprintf(
        Msg,
        "Function \"Screenshot\": "
         "The file \"%s\" could not be created.",
        FileName.c_str()
    );
    throw runtime_error(Msg);
}

// dump the file header
if (!fwrite(reinterpret_cast<void*>(&bmfh), sizeof(bmfh), 1, f))
{   THROW_FWRITE_EXCEPTION(FileName.c_str()); }
// dump the info header
if (!fwrite(reinterpret_cast<void*>(&bmih), sizeof(bmih), 1, f))
{   THROW_FWRITE_EXCEPTION(FileName.c_str()); }
// dump the bitmap bits
if (fwrite(reinterpret_cast<void*>(Bits), sizeof(BYTE),
    bmih.biSizeImage, f) < bmih.biSizeImage)
{   THROW_FWRITE_EXCEPTION(FileName.c_str()); }

// close the file
fclose(f);

Note: THROW_FWRITE_EXCEPTION is an application-defined function that formats an error message and throws an exception of type std::runtime_error.

Wrapping It Up

That’s all that’s required to store a screenshot. However, it would be a nice addition to have the Screenshot function automatically generate a filename if none is given. The Screenshot function does this by extracting the path information from the filename of the application’s executable file and by numbering the screenshots in that directory consecutively.

To use the Screenshot function in your application, use the following function call:

#include <Screenshot.h>
//...
Screenshot(lpDevice);

lpDevice is a pointer to an IDirect3DDevice8 interface. You can optionally pass an output file name to the function as the second paramater. By default, the function throws an exception object that is derived from std::exception when an error occurs. To prevent this, specify #define SCREENSHOT_DONT_THROW when compiling Screenshot.cpp.

You are free to use the code in your own programs. (Of course, giving us credit is always appreciated.) If you have any questions, suggestions, or comments, feel free to e-mail me at foetsch@yahoo.com. Visit my personal homepage: www.geocities.com/foetsch.


Michael Fötsch is the co-author of the upcoming Windows 2000 Graphics API Black Book, a definitive, no nonsense reference to creating robust and efficient graphics-based applications for Windows 95, 98, ME, NT, and 2000. The book has in-depth coverage of the GDI, Image Color Matching (ICM), DirectDraw, image processing (warping, morphing, etc.), and more.

Google+