I know that in OpenGL I can do something like this
glReadBuffer( GL_FRONT );
glReadPixels( 0, 0, _width, _height, GL_RGB, GL_UNSIGNED_BYTE, _buffer );
And its pretty fast, I get the raw bitmap in _buffer. When I try to do this in DirectX. Assuming that I have a D3DDevice object I can do something like this
if (SUCCEEDED(D3DDevice->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &pBackbuffer))) {
HResult hr = D3DXSaveSurfaceToFileA(filename, D3DXIFF_BMP, pBackbuffer, NULL, NULL);
But D3DXSaveSurfaceToFile is pretty slow, and I don't need to write the capture to disk anyway, so I was wondering if there was a faster way to do this.
EDIT: I am targeting only DirectX applications. Actually DX9 apps. I am doing this through a hook to Present. (I am using Detours if that is relevant), and I am doing this for every frame. I don't need (or want) a particular bitmap format like BMP PNG JPEG etc, and a colorspace of RGB24 is OK. Getting it as YUV480p would be better but definitely not necessary. Thanks everyone for the very helpful answers
Answer
The way I do this is as follows.
IDirect3DDevice9::GetBackBuffer: Get access to the IDirect3DSurface9 representing the back buffer, same as you've currently got. Don't forget to Release this surface when done as this call will increment the reference count!
IDirect3DSurface::GetDesc: Get the description of the back buffer surface, which will give you it's width, height and format.
IDirect3DDevice9::CreateOffscreenPlainSurface: Create a new surface object in D3DPOOL_SCRATCH; you commonly want to use the same width, height and format (but you don't actually have to with this method). Again, Release when done. If you're doing this operation every frame (in which case you're better off looking at alternatives such as a shader-based approach to what you're trying to do) you could just create the offscreen plain surface once at startup and reuse it, instead of creating it every frame.
D3DXLoadSurfaceFromSurface: Copy from the back buffer surface to the offsceen plain surface. This will do a resize and format conversion automatically for you. Alternatively, if you don't want to or need to resize or change the format you could use IDirect3DDevice9::GetRenderTargetData, but if so then create the offscreen plain surface in D3DPOOL_SYSTEMMEM instead.
IDirect3DSurface9::LockRect: Get access to the data in the offscreen plain surface and have your own evil way with it; UnlockRect when done.
This looks like a lot more code but you'll find that it's just as fast as glReadPixels, and can even be faster if you don't need to do a format conversion (which glReadPixels using GL_RGB almost certainly does).
Edit to add: some (rought 'n' ready) helper functions I also have which may be useful for using this method for screenshots:
// assumes pitch is measured in 32-bit texels, not bytes; use locked_rect.Pitch >> 2
void CollapseRowPitch (unsigned *data, int width, int height, int pitch)
{
if (width != pitch)
{
unsigned *out = data;
// as a minor optimization we can skip the first row
// since out and data point to the same this is OK
out += width;
data += pitch;
for (int h = 1; h < height; h++)
{
for (int w = 0; w < width; w++)
out[w] = data[w];
out += width;
data += pitch;
}
}
}
void Compress32To24 (byte *data, int width, int height)
{
byte *out = data;
for (int h = 0; h < height; h++)
{
for (int w = 0; w < width; w++, data += 4, out += 3)
{
out[0] = data[0];
out[1] = data[1];
out[2] = data[2];
}
}
}
// bpp is bits, not bytes
void WriteDataToTGA (char *name, void *data, int width, int height, int bpp)
{
if ((bpp == 24 || bpp == 8) && name && data && width > 0 && height > 0)
{
FILE *f = fopen (name, "wb");
if (f)
{
byte header[18];
memset (header, 0, 18);
header[2] = 2;
header[12] = width & 255;
header[13] = width >> 8;
header[14] = height & 255;
header[15] = height >> 8;
header[16] = bpp;
header[17] = 0x20;
fwrite (header, 18, 1, f);
fwrite (data, (width * height * bpp) >> 3, 1, f);
fclose (f);
}
}
}
No comments:
Post a Comment