15

我需要弄清楚如何将数据从 D3D 纹理和表面返回到系统内存。做这些事情的最快方法是什么以及如何做?

此外,如果我只需要一个子区域,如何只读回该部分而不必将整个内容读回系统内存?

简而言之,我正在寻找有关如何将以下内容复制到系统内存的简明描述:

  1. 纹理_
  2. 纹理
  3. 一个表面
  4. 表面
  5. 一个D3DUSAGE_RENDERTARGET 纹理
  6. D3DUSAGE_RENDERTARGET 纹理

这是 Direct3D 9,但也将不胜感激有关较新版本的 D3D 的答案。

4

1 回答 1

30

最涉及的部分是从视频内存(“默认池”)中的某个表面读取。这通常是渲染目标。

让我们先来看看简单的部分:

  1. 从纹理读取与从该纹理的 0 级表面读取相同。见下文。
  2. 对于纹理的子集也是如此。
  3. 从非默认内存池(“系统”或“托管”)中的表面读取只是锁定它并读取字节。
  4. 对于表面的子集也是如此。只需锁定相关部分并阅读即可。

所以现在我们留下了在显存中的表面(“默认池”)。这将是标记为渲染目标的任何表面/纹理,或您在默认池中创建的任何常规表面/纹理,或后备缓冲区本身。这里的复杂部分是你不能锁定它。

简短的回答是: D3D 设备上的GetRenderTargetData方法。

更长的答案(下面是代码的粗略轮廓):

  1. rt = 获取渲染目标表面(这可以是纹理的表面,或者后备缓冲区等)
  2. 如果rt是多重采样的(GetDesc,检查 D3DSURFACE_DESC.MultiSampleType),那么: a) 创建另一个相同大小、相同格式但没有多重采样的渲染目标表面;b) 将rt中的 StretchRect 延伸到这个新的表面;c) rt = 这个新表面(即在这个新表面上进行)。
  3. off = 创建屏幕外平面(CreateOffscreenPlainSurface,D3DPOOL_SYSTEMMEM 池)
  4. 设备->GetRenderTargetData( rt , off )
  5. 现在off包含渲染目标数据。LockRect(),读取数据,UnlockRect()就可以了。
  6. 清理

更长的答案(从我正在处理的代码库中粘贴)如下。这不会开箱即用,因为它使用了其他代码库中的一些类、函数、宏和实用程序;但它应该让你开始。我还省略了大部分错误检查(例如,给定的宽度/高度是否超出范围)。我还省略了读取实际像素并可能将它们转换为合适的目标格式的部分(这很容易,但可能会变长,具体取决于您想要支持的格式转换数量)。

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在上面的代码中是一个指向 COM 对象的智能指针(它在赋值或析构函数时释放对象)。大大简化了错误处理。_comptr_t这与Visual C++ 中的内容非常相似。

上面的代码读回整个表面。如果您想有效地阅读其中的一部分,那么我相信最快的方法大致是:

  1. 创建一个所需大小的默认池表面。
  2. StretchRect 从原始表面的一部分到那个较小的表面。
  3. 与较小的一起正常进行。

事实上,这与上面处理多采样表面的代码非常相似。我认为,如果您只想获取多采样表面的一部分,则可以进行多采样解析并在一个 StretchRect 中获取其中的一部分。

编辑:删除了实际读取像素和格式转换的代码。与问题没有直接关系,代码很长。

编辑:更新以匹配已编辑的问题。

于 2008-09-23T10:01:40.863 回答