[Direct3D] 實現(xiàn)批次渲染、硬件 T&L 的渲染器和 D3DPipeline

在是否從 D3DRender 提供頂點緩存區(qū)操作給流水線時做了一些權(quán)衡,最后決定暫時使用 IDirect3DDevice9::DrawPrimitiveUP 來渲染,因為它更容易書寫,而且開銷是一次頂點拷貝,流水線也不用操心對緩存的使用。
D3DPipeline 并不是完整的,其涉及到從場景管理器中傳遞的靜態(tài)場景元素列表,這些元素需要事先被整理到各個子容器以便盡可能少地調(diào)整渲染狀態(tài)和寫頂點緩存。這些子容器由場景管理器維護,并在適當?shù)臅r候調(diào)用 Render::DrawPrimitive 進行渲染。
大多數(shù)的 los-lib 結(jié)構(gòu)與 D3DX 在內(nèi)存上兼容的,在保持界面獨立的同時不影響性能。例如 los::blaze::Material 與 D3DMATERIAL 即是兼容的。燈光定義則存在差異,主要原因在于 los-lib 使用了各個獨立的燈光類型,而 D3DLIGHT9 則放置在統(tǒng)一的結(jié)構(gòu)當中,當然,燈光對象通常并不在多個渲染狀態(tài)間改變,所以執(zhí)行兩種燈光類型數(shù)據(jù)的轉(zhuǎn)換并不影響效率。一楨通常僅進行一次這樣的轉(zhuǎn)換。
另一個容易犯的錯誤在于幾何體法線列表的索引,法線為每個頂點索引設(shè)置獨立的值,而不再通過頂點列表的索引形式,嘗試使用頂點索引來查找法線將得到非預(yù)期的結(jié)果。
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性能調(diào)試:
通常來說,使用CPU時間事件來調(diào)試GPU是低效并且是不準確的。D3D API在多數(shù)命令下會阻塞,甚至是Draw函數(shù)。它會在一些時間片上做一些真正的工作,而這往往是不可預(yù)知的。因此,GPU的性能調(diào)試只能用PIX或者是其他專用產(chǎn)品,例如NVIDIA’s NVPerfHUD來進行。
顯卡所用的內(nèi)存:
顯卡所用的內(nèi)存可以分為兩大類:本地的和非本地的(相對于顯卡來說)。在顯卡處理的某些數(shù)據(jù)類型的時候,需要本地內(nèi)存,例如 幀緩沖。 非本地內(nèi)存,有時也成為AGP卡槽內(nèi)存(AGP aperture),可以被顯卡訪問的某些數(shù)據(jù)類型所在的系統(tǒng)內(nèi)存,例如頂點緩沖。本地內(nèi)存要比非本地內(nèi)存快。
本地內(nèi)存通常是在顯卡內(nèi)的,但是有些顯卡可以共享系統(tǒng)內(nèi)存,這通常是平衡速度和價格之間的選擇。在這種情況下,幀緩存可以存在于系統(tǒng)內(nèi)存中,而不是在本地內(nèi)存中。這種技術(shù)下,顯卡處理某些數(shù)據(jù)的速度比不使用共享內(nèi)存的要慢,因為數(shù)據(jù)必須從I/O Bus(例如PCI-Express)上傳輸過來。但是這可以使顯卡成本大大降低。在NVIDIA,這種技術(shù)被稱為TurboCache,而ATI稱之為HyperMemory。
著色器和著色模型:
Shader是運行在GPU上的,處理一些D3D流水管線上一些任務(wù)的程序。有三種類型的shader,他們分別對應(yīng)三種可編程的stage:
Vertex shader (頂點著色器VS) stage, geometry shader (幾何著色器GS) stage, 還有pixel shader(像素著色器PS) stage。其中幾何著色器只能在DX10平臺上使用。
著色模型(shader model)是在GPU上運行的虛擬機。每個虛擬機定義被稱為一種shader profile。并且包含了特定的匯編語言。
著色器的職責:
著色器通常是流水管線中描述物體表面的部分。例如,一種看起來像木頭的材質(zhì)被稱為木頭著色器(wood shader)。而在D3D中,這些著色語言指令集可以做的事情遠不止描述物體表面。他們可以用來計算光照,矩陣轉(zhuǎn)換,頂點動畫,進行裁切,動態(tài)生成新的幾何物體,等等。在Mental ray中,shader按照職責可以劃分為surface shader, light shader, shader shader, output shader等等。
在D3D中,這三種著色器的職責劃分并不是很明確。例如,光照計算過可以在頂點著色器,或者是像素著色器中完成,這取決于應(yīng)用程序的需求。因此,包含各種著色器的著色器集合應(yīng)運而生。他們鏈接起來定義了一個工作流水線。
關(guān)于Direct3D 9 資源和內(nèi)存類型:
D3D支持下列類型的資源:紋理(包括常規(guī)的和渲染目標render target),頂點緩沖,索引緩沖,字體,交換鏈(swap chain),狀態(tài)組,深度模板緩沖,特效等等。
有四種內(nèi)存類型(池),資源可以在這里分配:
· 默認Default:在顯卡內(nèi)存中,包括AGP卡槽內(nèi)存和本地顯存。在設(shè)備丟失之后,必須被釋放,重構(gòu)。
· 托管Managed:存在于系統(tǒng)內(nèi)存中,按需拷貝到顯存。
· 系統(tǒng)SystemMem:永遠存在于系統(tǒng)內(nèi)存中,并且不能直接用于渲染??梢援斪髟椿蛘吣繕丝截?。例如UpdateSurface和UpdateTexture。
· Scrach: 永遠存在于系統(tǒng)內(nèi)存中,并且不會被設(shè)備大小或格式限制,例如紋理的2的冪限制。不能把它放到顯存中。
查找資源泄露:
在關(guān)閉一個基于D3D的應(yīng)用程序時,D3D調(diào)試運行庫會報告內(nèi)存泄露。按照以下步驟定位泄漏點。
1. 在DirectX Control Panel中(通常在DXSDK安裝目錄中可以找到),啟用“Use Debug Version of Direct3D 9”并且將Debug Output Level設(shè)置為”More”。確保Break on Memory Leaks被禁用。點擊Apply。
2. 在VS中調(diào)試運行應(yīng)用程序。在關(guān)閉應(yīng)用程序之后,查看VS的輸出窗口Direct 3D9: (WARN) : Memory Address: 00xxxxxx, IAllocID= xx dwSize = xxxxxxxx;(pid = xxxxx)
3. 每條記錄對應(yīng)了一個資源泄漏,查看并記住ID,然后在DirectX Control Panel中輸入ID并且點擊Apply。
4. 再次運行程序,重復(fù)以上步驟。程序會在分配點中斷,你可以檢查哪里遺忘釋放。
5. 當你調(diào)試完成之后,別忘了將Break On AllocID設(shè)置為0。
處理設(shè)備丟失(Device Lost)
一個D3D設(shè)備可以在很多情況下丟失,例如從全屏向窗口轉(zhuǎn)換,一個電源管理事件,按CTRL+DEL+ALT返回Windows Security Dialog。
必須采取措施去檢查一個設(shè)備是否丟失,丟失了之后如何恢復(fù)。
方法:在某些地方調(diào)用IDirect3DDevice9::TestCooperativeLevel,例如在每幀開始渲染之前調(diào)用。當發(fā)現(xiàn)設(shè)備丟失之后,采取下列措施:
1. 釋放所有在Default內(nèi)存中的資源
2. 釋放其他沒有和Default, Managed, SystemMem綁定的資源
3. 調(diào)用IDirect3DDevice9::TestCooperativeLevel去確認設(shè)備是否可以被重置如果能,那么調(diào)用IDirect3DDevice9::Reset 如果不能,繼續(xù)等待,然后再嘗試
4. 重新創(chuàng)建需要的資源
渲染目標和交換鏈(Render Targets and Swap Chains)
一個渲染目標是一個用于保存在圖形流水線輸出像素的表面。也就是說,它是一個顏色數(shù)組。一個設(shè)備可以有一個或者多個活動的渲染目標,可以通過SetRenderTarget來啟用。一個用于渲染目標的表面只能放在Default池中,有三種渲染目標:
· 渲染目標表面Render target surfaces(通過CreateRenderTarget創(chuàng)建)
· 渲染目標紋理Render target textures(tongguo D3DUSAGE_RENDERTARGET標識來創(chuàng)建)
· 交換鏈Swap chains 交換鏈就是后備緩沖的集合,它們能夠相繼渲染到前緩沖,也就是屏幕上。一個在交換鏈中的后備緩沖可以當作一個渲染目標賦給一個設(shè)備。但是,不像其他的渲染目標,交換鏈可以渲染到屏幕上,因為交換鏈是和窗口/全屏大小綁定的??梢詣?chuàng)建多個交換鏈,注意更改默認交換鏈大小會造成設(shè)備丟失,所以窗口程序會忽略默認的交換鏈,而使用一個附加的交換鏈來避免這個問題。渲染目標可以被鎖定(用來讀?。?,但是當這個渲染目標是活動的話,會影響系統(tǒng)性能。我們可以根據(jù)需要用IDirect3DDevice9::GetRenderTargetData來將一個在Default池中的渲染目標拷貝出來。可以使用IDirect3DDevice9::StrechRectangle在兩個在顯卡內(nèi)存中的渲染目標中進行高效拷貝。
批處理(Batching)【重劍注:這個是重點】
D3D的效率在很大程度上受制于傳給API的幾何模型數(shù)據(jù)的批次上。一個批處理就是調(diào)用一次DrawPrimitive或者DrawIndexPrimitive。在GPU可以處理數(shù)據(jù)前,CPU花相當長時間來處理每批數(shù)據(jù)?,F(xiàn)在常見的CPU和GPU,可以參考以下數(shù)據(jù):
· 使用DX9,CPU每秒可以處理50,000批次;使用DX10,這個數(shù)據(jù)是200,000。
· 在DX9中,處理2,000個三角形在CPU和GPU所花的時間大致相等。在DX10中,這個數(shù)據(jù)是500。簡單的著色程序使這個數(shù)字增加,復(fù)雜的著色程序使這個數(shù)字減少。在CPU和GPU在同一個批次上花相同時間的情況下,實例化(Instancing)可以提高三角形的輸出能力。因為以上原因,每個批次中處理數(shù)據(jù)的數(shù)量越大越好,這樣能夠?qū)⑷切蔚耐掏铝孔畲蠡?/p>
在實踐中,具體有兩種方式:
· Consolidation合并:將相同性質(zhì)的幾何元素合并起來,通常是將一些屬性進行排序的結(jié)果
· Instancing實例化:將相同的幾何物體,經(jīng)過一些細微的,不同的變換后畫出多個實例來。例如世界坐標系的轉(zhuǎn)換和顏色轉(zhuǎn)換?!?span style="COLOR: #0000ff">重劍思考:Q:游戲里角色的護腕部位要同樣的模型,不能是一個護腕,一個手套,這個就是為了Instancing