Doing readback from Direct3D textures and surfaces
Asked Answered
S

1

15

I need to figure out how to get the data from D3D textures and surfaces back to system memory. What's the fastest way to do such things and how?

Also if I only need one subrect, how can one read back only that portion without having to read back the entire thing to system memory?

In short I'm looking for concise descriptions of how to copy the following to system memory:

  1. a texture
  2. a subset of a texture
  3. a surface
  4. a subset of a surface
  5. a D3DUSAGE_RENDERTARGET texture
  6. a subset of a D3DUSAGE_RENDERTARGET texture

This is Direct3D 9, but answers about newer versions of D3D would be appreciated too.

Squinty answered 23/9, 2008 at 9:40 Comment(0)
E
30

The most involved part is reading from some surface that is in video memory ("default pool"). This is most often render targets.

Let's get the easy parts first:

  1. reading from a texture is the same as reading from 0-level surface of that texture. See below.
  2. the same for subset of a texture.
  3. reading from a surface that is in non-default memory pool ("system" or "managed") is just locking it and reading bytes.
  4. the same for subset of surface. Just lock relevant portion and read it.

So now we have left surfaces that are in video memory ("default pool"). This would be any surface/texture marked as render target, or any regular surface/texture that you have created in default pool, or the backbuffer itself. The complex part here is that you can't lock it.

Short answer is: GetRenderTargetData method on D3D device.

Longer answer (a rough outline of the code that will be below):

  1. rt = get render target surface (this can be surface of the texture, or backbuffer, etc.)
  2. if rt is multisampled (GetDesc, check D3DSURFACE_DESC.MultiSampleType), then: a) create another render target surface of same size, same format but without multisampling; b) StretchRect from rt into this new surface; c) rt = this new surface (i.e. proceed on this new surface).
  3. off = create offscreen plain surface (CreateOffscreenPlainSurface, D3DPOOL_SYSTEMMEM pool)
  4. device->GetRenderTargetData( rt, off )
  5. now off contains render target data. LockRect(), read data, UnlockRect() on it.
  6. cleanup

Even longer answer (paste from the codebase I'm working on) follows. This will not compile out of the box, because it uses some classes, functions, macros and utilities from the rest of codebase; but it should get you started. I also ommitted most of error checking (e.g. whether given width/height is out of bounds). I also omitted the part that reads actual pixels and possibly converts them into suitable destination format (that is quite easy, but can get long, depending on number of format conversions you want to support).

bool GfxDeviceD3D9::ReadbackImage( /* params */ )
{
    HRESULT hr;
    IDirect3DDevice9* dev = GetD3DDevice();
    SurfacePointer renderTarget;
    hr = dev->GetRenderTarget( 0, &renderTarget );
    if( !renderTarget || FAILED(hr) )
        return false;

    D3DSURFACE_DESC rtDesc;
    renderTarget->GetDesc( &rtDesc );

    SurfacePointer resolvedSurface;
    if( rtDesc.MultiSampleType != D3DMULTISAMPLE_NONE )
    {
        hr = dev->CreateRenderTarget( rtDesc.Width, rtDesc.Height, rtDesc.Format, D3DMULTISAMPLE_NONE, 0, FALSE, &resolvedSurface, NULL );
        if( FAILED(hr) )
            return false;
        hr = dev->StretchRect( renderTarget, NULL, resolvedSurface, NULL, D3DTEXF_NONE );
        if( FAILED(hr) )
            return false;
        renderTarget = resolvedSurface;
    }

    SurfacePointer offscreenSurface;
    hr = dev->CreateOffscreenPlainSurface( rtDesc.Width, rtDesc.Height, rtDesc.Format, D3DPOOL_SYSTEMMEM, &offscreenSurface, NULL );
    if( FAILED(hr) )
        return false;

    hr = dev->GetRenderTargetData( renderTarget, offscreenSurface );
    bool ok = SUCCEEDED(hr);
    if( ok )
    {
        // Here we have data in offscreenSurface.
        D3DLOCKED_RECT lr;
        RECT rect;
        rect.left = 0;
        rect.right = rtDesc.Width;
        rect.top = 0;
        rect.bottom = rtDesc.Height;
        // Lock the surface to read pixels
        hr = offscreenSurface->LockRect( &lr, &rect, D3DLOCK_READONLY );
        if( SUCCEEDED(hr) )
        {
            // Pointer to data is lt.pBits, each row is
            // lr.Pitch bytes apart (often it is the same as width*bpp, but
            // can be larger if driver uses padding)

            // Read the data here!
            offscreenSurface->UnlockRect();
        }
        else
        {
            ok = false;
        }
    }

    return ok;
}

SurfacePointer in the code above is a smart pointer to a COM object (it releases object on assignment or destructor). Simplifies error handling a lot. This is very similar to _comptr_t things in Visual C++.

The code above reads back whole surface. If you want to read just a part of it efficiently, then I believe fastest way is roughly:

  1. create a default pool surface that is of the needed size.
  2. StretchRect from part of original surface to that smaller one.
  3. proceed as normal with the smaller one.

In fact this is quite similar to what code above does to handle multi-sampled surfaces. If you want to get just a part of a multi-sampled surface, you can do a multisample resolve and get part of it in one StretchRect, I think.

Edit: removed piece of code that does actual read of pixels and format conversions. Was not directly related to question, and the code was long.

Edit: updated to match edited question.

Exospore answered 23/9, 2008 at 10:1 Comment(3)
Thanks. That does point me down the right path. I did find the comment in MSDN soon after posting that you can't LockRect on a D3DUSAGE_RENDERTARGET and must use GetRenderTargetData instead. Bummer. Would be nice to have a cleaner leaner meaner code sample, though. :-)Squinty
Definitely much better without the pixel copying code. How about replacing the non-D3D type, SurfacePointer with the actual D3D name? LPDIRECT3DSURFACE9 or IDirect3DSurface9*Squinty
And what to do if I want to get texture from DEFAULT pool, but it is not rendertarget. Lock doesn't work, I can't use GetRenderTargetData() for non-rendertarget surfaces. Sometimes possible to create intermediate rendertarget, but if original texture in some format like D3DFORMAT_L8 - I can't create rendertarget in this format. So looks like it is not possible to get such texture. Am I right ?Gittel

© 2022 - 2024 — McMap. All rights reserved.