[Direct3D] 实现批次渲染、硬件 T&L 的渲染器和 D3DPipeline
在是否从 D3DRender 提供顶点缓存区操作给流水线时做了一些权衡,最后决定暂时使用 IDirect3DDevice9::DrawPrimitiveUP 来渲染,因为它更容易书写,而且开销是一次顶点拷贝,流水线也不用操心对缓存的使用。
D3DPipeline 并不是完整的,其涉及到从场景管理器中传递的静态场景元素列表,这些元素需要事先被整理到各个子容器以便尽可能少地调整渲染状态和写顶点缓存。这些子容器由场景管理器维护,并在适当的时候调用 Render::DrawPrimitive 进行渲染。
大多数的 los-lib 结构与 D3DX 在内存上兼容的,在保持界面独立的同时不影响性能。例如 los::blaze::Material 与 D3DMATERIAL 即是兼容的。灯光定义则存在差异,主要原因在于 los-lib 使用了各个独立的灯光类型,而 D3DLIGHT9 则放置在统一的结构当中,当然,灯光对象通常并不在多个渲染状态间改变,所以执行两种灯光类型数据的转换并不影响效率。一桢通常仅进行一次这样的转换。
另一个容易犯的错误在于几何体法线列表的索引,法线为每个顶点索引设置独立的值,而不再通过顶点列表的索引形式,尝试使用顶点索引来查找法线将得到非预期的结果。
D3DRender:
virtual int DrawPrimitive(const std::vector<VertexXYZ_N>& listVertex
, const Matrix& matWorld, const Matrix& matView, const Matrix& matProj
, const Material& material)
{
ptrDevice->SetTransform(D3DTS_WORLD, (CONST D3DMATRIX*)&matWorld);
ptrDevice->SetTransform(D3DTS_VIEW, (CONST D3DMATRIX*)&matView);
ptrDevice->SetTransform(D3DTS_PROJECTION, (CONST D3DMATRIX*)&matProj);
ptrDevice->SetFVF(D3DFVF_XYZ | D3DFVF_NORMAL);
ptrDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID);
ptrDevice->SetMaterial((CONST D3DMATERIAL9*)&material);
uint nPrim = (uint)listVertex.size() / 3;
uint nBatch = nPrim / _D3DCaps.MaxPrimitiveCount;
uint nByteBatch =_D3DCaps.MaxPrimitiveCount * (uint)sizeof(VertexXYZ_N) * 3;
for (uint idx = 0; idx < nBatch ; ++idx)
ptrDevice->DrawPrimitiveUP(D3DPT_TRIANGLELIST
, _D3DCaps.MaxPrimitiveCount
, &listVertex.front()
+ idx * nByteBatch
, (uint)sizeof(VertexXYZ_N));
ptrDevice->DrawPrimitiveUP(D3DPT_TRIANGLELIST, nPrim % _D3DCaps.MaxPrimitiveCount
, &listVertex.front()
+ nBatch * nByteBatch
, (uint)sizeof(VertexXYZ_N));
return 0;
}
virtual int SetLights(const Lights& lights)
{
ptrDevice->SetRenderState(D3DRS_AMBIENT
, (lights.globalLight.GetColor()
* lights.globalLight.GetIntensity()).ToColor());
uint idxLight = 0;
for (size_t idx = 0; idx < lights.listPointLight.size(); ++idx)
{
const PointLight& refLight = lights.listPointLight[idx];
D3DLIGHT9 lght;
::memset(&lght, 0, sizeof(D3DLIGHT9));
lght.Type = D3DLIGHT_POINT;
lght.Range = refLight.GetDistance();
lght.Attenuation1 = 1.0f;
Vector3 vPos = refLight.GetPosition();
lght.Position.x = vPos.x;
lght.Position.y = vPos.y;
lght.Position.z = vPos.z;
lght.Diffuse = lght.Specular
= *(D3DCOLORVALUE*)&(refLight.GetColor() * refLight.GetIntensity());
ptrDevice->SetLight(idxLight, &lght);
ptrDevice->LightEnable(idxLight++, true);
}
for (size_t idx = 0; idx < lights.listParallelLight.size(); ++idx)
{
const ParallelLight& refLight = lights.listParallelLight[idx];
D3DLIGHT9 lght;
::memset(&lght, 0, sizeof(D3DLIGHT9));
lght.Type = D3DLIGHT_DIRECTIONAL;
Vector3 vDir = refLight.GetDirection();
lght.Direction.x = vDir.x;
lght.Direction.y = vDir.y;
lght.Direction.z = vDir.z;
lght.Diffuse = lght.Specular
= *(D3DCOLORVALUE*)&(refLight.GetColor() * refLight.GetIntensity());
ptrDevice->SetLight(idxLight, &lght);
ptrDevice->LightEnable(idxLight++, true);
}
for (size_t idx = 0; idx < lights.listSpotLight.size(); ++idx)
{
const SpotLight& refLight = lights.listSpotLight[idx];
D3DLIGHT9 lght;
::memset(&lght, 0, sizeof(D3DLIGHT9));
lght.Type = D3DLIGHT_SPOT;
lght.Range = refLight.GetDistance();
lght.Attenuation1 = 1.0f;
lght.Falloff = 1.0f;
lght.Theta = refLight.GetHotspot().ToRadian();
lght.Phi = refLight.GetFalloff().ToRadian();
Vector3 vDir = refLight.GetDirection();
lght.Direction.x = vDir.x;
lght.Direction.y = vDir.y;
lght.Direction.z = vDir.z;
Vector3 vPos = refLight.GetPosition();
lght.Position.x = vPos.x;
lght.Position.y = vPos.y;
lght.Position.z = vPos.z;
lght.Diffuse = lght.Specular
= *(D3DCOLORVALUE*)&(refLight.GetColor() * refLight.GetIntensity());
ptrDevice->SetLight(idxLight, &lght);
ptrDevice->LightEnable(idxLight++, true);
}
return 0;
}
D3DPipeline:
virtual int ProcessingObject(const Object3D& object)
{
++_DebugInfo.dynamic_object_counter;
const Model& refModel = object.GetModel();
const Vector3& pos = object.GetPosition();
Matrix mat = object.GetTransform()
* object.GetOrientation().ObjectToInertial() * object.GetAxis()
* Matrix().BuildTranslation(pos.x, pos.y, pos.z);
for (size_t gidx = 0; gidx < refModel.listGeometry.size(); ++gidx)
{
const Geometry& refGeom = refModel.listGeometry[gidx];
const Material& refMat = refModel.listMaterial[refGeom.indexMaterial];
//Triangle triangle;
//triangle.bitmap = (DeviceBitmap*)&refModel.listDeviceBitmap[refGeom.indexDeviceBitmap];
std::vector<VertexXYZ_N> listVertex;
listVertex.reserve(refGeom.listIndex.size());
for (size_t iidx = 0; iidx < refGeom.listIndex.size(); iidx += 3)
{
const Vector3& vertex0 = refGeom.listVertex[refGeom.listIndex[iidx]];
const Vector3& vertex1 = refGeom.listVertex[refGeom.listIndex[iidx + 1]];
const Vector3& vertex2 = refGeom.listVertex[refGeom.listIndex[iidx + 2]];
Vector3 normal0 = refGeom.listNormal[iidx];
Vector3 normal1 = refGeom.listNormal[iidx + 1];
Vector3 normal2 = refGeom.listNormal[iidx + 2];
listVertex.push_back(VertexXYZ_N());
VertexXYZ_N& refV0 = listVertex.back();
refV0.x = vertex0.x;
refV0.y = vertex0.y;
refV0.z = vertex0.z;
refV0.normal_x = normal0.x;
refV0.normal_y = normal0.y;
refV0.normal_z = normal0.z;
listVertex.push_back(VertexXYZ_N());
VertexXYZ_N& refV1 = listVertex.back();
refV1.x = vertex1.x;
refV1.y = vertex1.y;
refV1.z = vertex1.z;
refV1.normal_x = normal1.x;
refV1.normal_y = normal1.y;
refV1.normal_z = normal1.z;
listVertex.push_back(VertexXYZ_N());
VertexXYZ_N& refV2 = listVertex.back();
refV2.x = vertex2.x;
refV2.y = vertex2.y;
refV2.z = vertex2.z;
refV2.normal_x = normal2.x;
refV2.normal_y = normal2.y;
refV2.normal_z = normal2.z;
++_DebugInfo.polygon_counter;
}
_PtrRender->DrawPrimitive(listVertex, mat, _ViewMatrix, _PerspectiveMatrix, refMat);
}
return 0;
}
};
这篇看batching段。
GPU性能调试:
通常来说,使用CPU时间事件来调试GPU是低效并且是不准确的。D3D API在多数命令下会阻塞,甚至是Draw函数。它会在一些时间片上做一些真正的工作,而这往往是不可预知的。因此,GPU的性能调试只能用PIX或者是其他专用产品,例如NVIDIA’s NVPerfHUD来进行。
显卡所用的内存:
显卡所用的内存可以分为两大类:本地的和非本地的(相对于显卡来说)。在显卡处理的某些数据类型的时候,需要本地内存,例如 帧缓冲。 非本地内存,有时也成为AGP卡槽内存(AGP aperture),可以被显卡访问的某些数据类型所在的系统内存,例如顶点缓冲。本地内存要比非本地内存快。
本地内存通常是在显卡内的,但是有些显卡可以共享系统内存,这通常是平衡速度和价格之间的选择。在这种情况下,帧缓存可以存在于系统内存中,而不是在本地内存中。这种技术下,显卡处理某些数据的速度比不使用共享内存的要慢,因为数据必须从I/O Bus(例如PCI-Express)上传输过来。但是这可以使显卡成本大大降低。在NVIDIA,这种技术被称为TurboCache,而ATI称之为HyperMemory。
着色器和着色模型:
Shader是运行在GPU上的,处理一些D3D流水管线上一些任务的程序。有三种类型的shader,他们分别对应三种可编程的stage:
Vertex shader (顶点着色器VS) stage, geometry shader (几何着色器GS) stage, 还有pixel shader(像素着色器PS) stage。其中几何着色器只能在DX10平台上使用。
着色模型(shader model)是在GPU上运行的虚拟机。每个虚拟机定义被称为一种shader profile。并且包含了特定的汇编语言。
着色器的职责:
着色器通常是流水管线中描述物体表面的部分。例如,一种看起来像木头的材质被称为木头着色器(wood shader)。而在D3D中,这些着色语言指令集可以做的事情远不止描述物体表面。他们可以用来计算光照,矩阵转换,顶点动画,进行裁切,动态生成新的几何物体,等等。在Mental ray中,shader按照职责可以划分为surface shader, light shader, shader shader, output shader等等。
在D3D中,这三种着色器的职责划分并不是很明确。例如,光照计算过可以在顶点着色器,或者是像素着色器中完成,这取决于应用程序的需求。因此,包含各种着色器的着色器集合应运而生。他们链接起来定义了一个工作流水线。
关于Direct3D 9 资源和内存类型:
D3D支持下列类型的资源:纹理(包括常规的和渲染目标render target),顶点缓冲,索引缓冲,字体,交换链(swap chain),状态组,深度模板缓冲,特效等等。
有四种内存类型(池),资源可以在这里分配:
· 默认Default:在显卡内存中,包括AGP卡槽内存和本地显存。在设备丢失之后,必须被释放,重构。
· 托管Managed:存在于系统内存中,按需拷贝到显存。
· 系统SystemMem:永远存在于系统内存中,并且不能直接用于渲染。可以当作源或者目标拷贝。例如UpdateSurface和UpdateTexture。
· Scrach: 永远存在于系统内存中,并且不会被设备大小或格式限制,例如纹理的2的幂限制。不能把它放到显存中。
查找资源泄露:
在关闭一个基于D3D的应用程序时,D3D调试运行库会报告内存泄露。按照以下步骤定位泄漏点。
1. 在DirectX Control Panel中(通常在DXSDK安装目录中可以找到),启用“Use Debug Version of Direct3D 9”并且将Debug Output Level设置为”More”。确保Break on Memory Leaks被禁用。点击Apply。
2. 在VS中调试运行应用程序。在关闭应用程序之后,查看VS的输出窗口Direct 3D9: (WARN) : Memory Address: 00xxxxxx, IAllocID= xx dwSize = xxxxxxxx;(pid = xxxxx)
3. 每条记录对应了一个资源泄漏,查看并记住ID,然后在DirectX Control Panel中输入ID并且点击Apply。
4. 再次运行程序,重复以上步骤。程序会在分配点中断,你可以检查哪里遗忘释放。
5. 当你调试完成之后,别忘了将Break On AllocID设置为0。
处理设备丢失(Device Lost)
一个D3D设备可以在很多情况下丢失,例如从全屏向窗口转换,一个电源管理事件,按CTRL+DEL+ALT返回Windows Security Dialog。
必须采取措施去检查一个设备是否丢失,丢失了之后如何恢复。
方法:在某些地方调用IDirect3DDevice9::TestCooperativeLevel,例如在每帧开始渲染之前调用。当发现设备丢失之后,采取下列措施:
1. 释放所有在Default内存中的资源
2. 释放其他没有和Default, Managed, SystemMem绑定的资源
3. 调用IDirect3DDevice9::TestCooperativeLevel去确认设备是否可以被重置如果能,那么调用IDirect3DDevice9::Reset 如果不能,继续等待,然后再尝试
4. 重新创建需要的资源
渲染目标和交换链(Render Targets and Swap Chains)
一个渲染目标是一个用于保存在图形流水线输出像素的表面。也就是说,它是一个颜色数组。一个设备可以有一个或者多个活动的渲染目标,可以通过SetRenderTarget来启用。一个用于渲染目标的表面只能放在Default池中,有三种渲染目标:
· 渲染目标表面Render target surfaces(通过CreateRenderTarget创建)
· 渲染目标纹理Render target textures(tongguo D3DUSAGE_RENDERTARGET标识来创建)
· 交换链Swap chains 交换链就是后备缓冲的集合,它们能够相继渲染到前缓冲,也就是屏幕上。一个在交换链中的后备缓冲可以当作一个渲染目标赋给一个设备。但是,不像其他的渲染目标,交换链可以渲染到屏幕上,因为交换链是和窗口/全屏大小绑定的。可以创建多个交换链,注意更改默认交换链大小会造成设备丢失,所以窗口程序会忽略默认的交换链,而使用一个附加的交换链来避免这个问题。渲染目标可以被锁定(用来读取),但是当这个渲染目标是活动的话,会影响系统性能。我们可以根据需要用IDirect3DDevice9::GetRenderTargetData来将一个在Default池中的渲染目标拷贝出来。可以使用IDirect3DDevice9::StrechRectangle在两个在显卡内存中的渲染目标中进行高效拷贝。
批处理(Batching)【重剑注:这个是重点】
D3D的效率在很大程度上受制于传给API的几何模型数据的批次上。一个批处理就是调用一次DrawPrimitive或者DrawIndexPrimitive。在GPU可以处理数据前,CPU花相当长时间来处理每批数据。现在常见的CPU和GPU,可以参考以下数据:
· 使用DX9,CPU每秒可以处理50,000批次;使用DX10,这个数据是200,000。
· 在DX9中,处理2,000个三角形在CPU和GPU所花的时间大致相等。在DX10中,这个数据是500。简单的着色程序使这个数字增加,复杂的着色程序使这个数字减少。在CPU和GPU在同一个批次上花相同时间的情况下,实例化(Instancing)可以提高三角形的输出能力。因为以上原因,每个批次中处理数据的数量越大越好,这样能够将三角形的吞吐量最大化。
在实践中,具体有两种方式:
· Consolidation合并:将相同性质的几何元素合并起来,通常是将一些属性进行排序的结果
· Instancing实例化:将相同的几何物体,经过一些细微的,不同的变换后画出多个实例来。例如世界坐标系的转换和颜色转换。【重剑思考:Q:游戏里角色的护腕部位要同样的模型,不能是一个护腕,一个手套,这个就是为了Instancing?A:非也!两个护腕其实是一个模型,美术画的时候就是画了一对(左右各一个),中间就是断开的】
顶点,索引缓冲Vertex / Index Buffer
顶点和索引缓冲有两种类型:静态和动态的。
一旦创建之后,静态的缓冲使用起来比动态的快一倍。但是,动态缓冲的加锁和解锁要比静态的快,它们是为更改的每一帧设计的,通常被存储在AGP卡槽内存中。经常对静态缓冲加解锁是不明智的,因为只有等驱动完成了所有挂起的命令之后才能返回该缓冲的指针。如果经常这样做,这会导致CPU和GPU很多不必要的同步,这样性能将会变得很差。
为了得到最好的性能,必须采用动态缓存。这样驱动可以继续进行并行渲染。使用DISCARD或者是NOOVERWRITING标志可以实现这一点,这样驱动可以在更新数据的同时继续处理老的数据。
DISCARD:这个标志说明应用程序不关心当前缓冲的内容。所以在缓冲被渲染的同时,驱动可以给应用程序一个全新的缓冲。这个处理称之为“buffer renaming”。注意,在实践中,驱动倾向于不去释放“缓冲重命名”中所用的内存,因此这个标志必须尽量少用。
NOOVERWRITE:这表示,对于之前添加的,不带这个标志的数据,应用程序不会更改它。例如应用程序只会在现有缓冲之后添加数据。所以驱动可以继续使用现有数据进行渲染。
CPU和GPU的并行处理
D3D runtime会将一堆命令做成命令串传给GPU,这就允许GPU和CPU进行并行处理。这样也是硬件加速渲染这么高效的原因之一。但是,在很多情况下,CPU和GPU必须进行同步之后才能做进一步的处理。通常来说,应该尽量避免这种情况,因为这会导致整个流水管线的刷新,大幅降低性能。例如,对静态缓冲加锁,这要求GPU先处理完所有的命令之后,才能返回被锁缓冲的指针。如果用动态缓冲,就可以避免,就像前面讲过的一样。
有一些同步是不可避免的,例如,CPU可能会需要一些GPU还来不及处理的命令结果。在这种情况下,用户会感到画面延迟Lag。要避免这种情况,可以在GPU落后两三帧的情况下调用Present来强迫CPU等待GPU。因此,调用Present可能比较慢,但是正式它处理了必要的同步。
状态的更换State Changes
不管冗余还是不冗余,状态的转换在到达驱动层的时候,开销总是很大。所以在某些层面,状态转换必须被过滤。一个对状态进行更换的函数调用并不一定会开销很大,因为D3D Runtime很有可能缓冲这些转换请求,在真正调用DrawPrimitive函数之前不会去执行它。多次的状态转换也不会加大开销,因为只使用最后一个状态值。尽管如此,状态转换还是应该尽量避免。某些状态转换会比其他的转换的开销更大。例如,对于更改处于活动状态的顶点缓冲和像素缓冲会导致整个流水管线的刷新。因为在某些显卡上,同一时间每个类型只有一个着色器可以处于活动状态。一个图形流水线可以很长,花一段时间才能完成一个像素的渲染。因此,整个流水线的刷新需要尽量避免。在不同的显卡上,某个状态的更新的花费差别可能会很大。另外,D3D的函数调用个数也必须尽量的少,虽然它的开销不如达到驱动层的状态更改那么大。可以使用状态块来减少D3D API的调用,状态块可以将状态的更改集中在一起,并且可以重用。