当我每帧写入单个顶点缓冲区数千次时,如何提高Direct3D的性能?

5
我正在尝试编写一个OpenGL包装器,使我能够使用所有现有的图形代码(针对OpenGL编写),并将OpenGL调用路由到Direct3D等效项。到目前为止,这个工作令人惊讶地顺利,但性能却成为了一个相当大的问题。
现在,我承认我很可能正在以一种它从未设计过的方式使用D3D。每次渲染循环,我都会更新单个顶点缓冲区数千次。每次绘制“精灵”,我都会向GPU发送4个带纹理坐标等信息的顶点,当屏幕上的“精灵”数量达到约1k至1.5k时,我的应用程序的FPS下降到10fps以下。
使用VS2012性能分析(顺便说一句,这很棒),我可以看到ID3D11DeviceContext->Draw方法占用了大部分时间: 此处为截图

在设置顶点缓冲区或绘制方法时,我是否有没有正确使用的设置?如果为所有精灵使用相同的顶点缓冲区真的很糟糕吗?如果是这样,我还有哪些选项可以不会大幅改变我现有图形代码库的架构(这是围绕OpenGL范例构建的...每帧将所有内容发送到GPU!)

在我的游戏中,最大的FPS杀手是当我在屏幕上显示大量文本时。每个字符都是一个纹理四边形,每个字符都需要单独更新顶点缓冲区并调用单独的Draw。如果D3D或硬件不喜欢多次调用Draw,那么如何才能一次性向屏幕绘制大量文本呢?

如果需要更多的代码来帮助我诊断此问题,请告诉我。

谢谢!

这是我正在运行的硬件:

  • Core i7 @ 3.5GHz
  • 16 GB内存
  • GeForce GTX 560 Ti

这是我正在运行的软件:

  • Windows 8 Release Preview
  • VS 2012
  • DirectX 11

这里是绘制方法:

void OpenGL::Draw(const std::vector<OpenGLVertex>& vertices)
{
   auto matrix = *_matrices.top();
   _constantBufferData.view = DirectX::XMMatrixTranspose(matrix);
   _context->UpdateSubresource(_constantBuffer, 0, NULL, &_constantBufferData, 0, 0);

   _context->IASetInputLayout(_inputLayout);
   _context->VSSetShader(_vertexShader, nullptr, 0);
   _context->VSSetConstantBuffers(0, 1, &_constantBuffer);

   D3D11_PRIMITIVE_TOPOLOGY topology = D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP;
   ID3D11ShaderResourceView* texture = _textures[_currentTextureId];

   // Set shader texture resource in the pixel shader.
   _context->PSSetShader(_pixelShaderTexture, nullptr, 0);
   _context->PSSetShaderResources(0, 1, &texture);

   D3D11_MAPPED_SUBRESOURCE mappedResource;
   D3D11_MAP mapType = D3D11_MAP::D3D11_MAP_WRITE_DISCARD;
   auto hr = _context->Map(_vertexBuffer, 0, mapType, 0, &mappedResource);
   if (SUCCEEDED(hr))
   {
      OpenGLVertex *pData = reinterpret_cast<OpenGLVertex *>(mappedResource.pData);
      memcpy(&(pData[_currentVertex]), &vertices[0], sizeof(OpenGLVertex) * vertices.size());
      _context->Unmap(_vertexBuffer, 0);
   }

   UINT stride = sizeof(OpenGLVertex);
   UINT offset = 0;
   _context->IASetVertexBuffers(0, 1, &_vertexBuffer, &stride, &offset);
   _context->IASetPrimitiveTopology(topology);
   _context->Draw(vertices.size(), _currentVertex);
   _currentVertex += (int)vertices.size();
}

以下是创建顶点缓冲区的方法:

void OpenGL::CreateVertexBuffer()
{
   D3D11_BUFFER_DESC bd;
   ZeroMemory(&bd, sizeof(bd));
   bd.Usage = D3D11_USAGE_DYNAMIC;
   bd.ByteWidth = _maxVertices * sizeof(OpenGLVertex);
   bd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
   bd.CPUAccessFlags = D3D11_CPU_ACCESS_FLAG::D3D11_CPU_ACCESS_WRITE;
   bd.MiscFlags = 0;
   bd.StructureByteStride = 0;
   D3D11_SUBRESOURCE_DATA initData;
   ZeroMemory(&initData, sizeof(initData));
   _device->CreateBuffer(&bd, NULL, &_vertexBuffer);
}

这是我的顶点着色器代码:

cbuffer ModelViewProjectionConstantBuffer : register(b0)
{
    matrix model;
    matrix view;
    matrix projection;
};

struct VertexShaderInput
{
    float3 pos : POSITION;
    float4 color : COLOR0;
    float2 tex : TEXCOORD0;
};

struct VertexShaderOutput
{
    float4 pos : SV_POSITION;
    float4 color : COLOR0;
    float2 tex : TEXCOORD0;
};

VertexShaderOutput main(VertexShaderInput input)
{
    VertexShaderOutput output;
    float4 pos = float4(input.pos, 1.0f);

    // Transform the vertex position into projected space.
    pos = mul(pos, model);
    pos = mul(pos, view);
    pos = mul(pos, projection);
    output.pos = pos;

    // Pass through the color without modification.
    output.color = input.color;
    output.tex = input.tex;

    return output;
}
1个回答

6
你需要尽可能地批量处理顶点,然后进行大块绘制。我已经成功地将这种方法应用于旧的立即模式OpenGL游戏中。不幸的是,这种方法有些麻烦。
最简单的解决方案是使用某种设备状态(你可能已经在跟踪),为特定的一组顶点创建一个唯一的标记。像混合模式和绑定纹理这样的东西是一个好的选择。如果你能找到一个快速的哈希算法来运行这个结构体,你可以很有效地存储它。
接下来,你需要进行顶点缓存。有两种处理方法,都有优势。最激进、最复杂、在许多具有相似属性的顶点集合的情况下最有效的方法是制作一个设备状态的结构体,分配一个大的(比如4KB)缓冲区,并在其中存储具有匹配状态的顶点。然后,你可以将整个数组倒入顶点缓冲区,并绘制缓冲区的块(以重新创建原始顺序)。然而,跟踪所有的缓冲区、状态和顺序是困难的。
更简单的方法是,在设备状态改变之前,将顶点缓存在一个大缓冲区中。此时,在实际更改状态之前,将数组倒入顶点缓冲区并进行绘制。然后重置数组索引,提交状态更改,并再次开始。
如果你的应用程序有大量相似的顶点,这是非常可能的,尤其是在使用精灵(纹理坐标和颜色可能会改变,但好的精灵将使用单个纹理图集和少量混合模式)时,即使第二种方法也可以提供一些性能提升。
关键在于在系统内存中建立一个缓存,最好是预分配的大块内存,然后在绘制之前将其转储到视频内存中。这样可以减少对视频内存的写入和绘制调用,这些操作往往是昂贵的(尤其是在一起)。正如你所看到的,你所做的调用数量变得很慢,而批处理有很大的帮助作用。关键是尽可能不要在每一帧中分配内存,批量处理足够大的块,并为每次绘制维护正确的设备状态和顺序。

我刚刚也添加了我正在使用的顶点着色器。请注意,我正在使用模型-视图-投影矩阵将每个顶点进行乘法运算。如果我朝着像您建议的批处理算法走,那么在将这些顶点发送到GPU和VS之前,我是否需要在应用程序代码中对它们进行乘法运算呢? - Andrew Garrison
这取决于它们持续的时间。如果其中一个矩阵在每次绘制时都会改变(例如你正在转换精灵),那么你可能需要考虑预乘;削减掉的状态更改可以提高性能。如果它们很少改变,那么只需将它们作为批处理的一部分,并在矩阵更改时结束/绘制批处理即可。 - ssube
好的,我想我开始明白了。感谢你的回答。我正在处理这个问题,处理完后会告诉你结果。如果批处理确实提高了性能,我会将你的回答标记为正确的。 - Andrew Garrison
太棒了!出于好奇,你最终获得了多少提升?似乎调整缓冲区/批处理大小以及与批处理保持的状态量可以在一定程度上影响它,较大的缓冲区通常会提供更好的性能。即使使用相对较小的缓冲区,也有可能获得不错的性能。 - ssube
最大的改进是一个屏幕每帧调用DeviceContext->Draw 350次,运行速度为20FPS。通过批处理,绘制计数减少到每帧3次,现在以60FPS(最大化)运行。 - Andrew Garrison
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接