avpacket 判断i帧_DX12绘制篇:帧资源

首先我们回顾下,在第一篇初始化基础模块中,我们实现了FlushCmdQueue函数。它的作用是保证CPU和GPU的同步。实现的大致思路是:创建一个CPU端围栏值mCurrentFence=0,再创建一个GPU端围栏值Fence=0,每当CPU将当前帧的绘制命令提交至GPU后,mCurrentFence++,而每当GPU处理完当前帧的命令后,Fence++,通过我们判定这两个围栏值的大小,来判断GPU是否已经处理完命令,只有处理完后,CPU才能继续下一帧的命令提交工作。代码如下:

void FlushCmdQueue()
{
    int mCurrentFence = 0;	//初始CPU上的围栏点为0
    mCurrentFence++;	//CPU传完命令并关闭后,将当前围栏值+1
    cmdQueue->Signal(fence.Get(), mCurrentFence);	//当GPU处理完CPU传入的命令后,将fence接口中的围栏值+1,即fence->GetCompletedValue()+1
    if (fence->GetCompletedValue() < mCurrentFence)	//如果小于,说明GPU没有处理完所有命令
    {
	HANDLE eventHandle = CreateEvent(nullptr, false, false, L"FenceSetDone");	//创建事件
	fence->SetEventOnCompletion(mCurrentFence, eventHandle);//当围栏达到mCurrentFence值(即执行到Signal()指令修改了围栏值)时触发的eventHandle事件
	WaitForSingleObject(eventHandle, INFINITE);//等待GPU命中围栏,激发事件(阻塞当前线程直到事件触发,注意此Enent需先设置再等待,
						   //如果没有Set就Wait,就死锁了,Set永远不会调用,所以也就没线程可以唤醒这个线程)
	CloseHandle(eventHandle);
    }
}

但是,这样的同步操作虽然简单易行,弊端也很明显,那就是CPU和GPU的互相等待时间较长,整体效率偏低。于是我们改进一下同步机制,让CPU连续处理3帧的绘制命令,并提交至GPU,当GPU处理完一帧命令后,CPU迅速提交命令,始终保证有3帧的命令储配。这样的话,如果CPU提交命令的速度高于GPU处理命令的速度,那GPU就会一直处于忙碌的状态,而空闲的CPU可以处理游戏逻辑、AI、物理模拟等等的计算。我们将每一帧提交的命令称为帧资源,而3帧的命令储备相当于一个3个帧资源元素的环形数组,随着GPU的进程,CPU不断更新环形数组中的数据元素。

1.创建帧资源

首先我们新建个文件,并创建FrameResources类,这个类中包括每帧绘制所需要的数据,它们是:每帧独立的命令分配器、每帧独立的常量缓冲区、每帧的围栏值。顺带的,我们把每帧需要的顶点索引以及常量数据结构体也一并移过来。

#pragma once
#include "../../common/ToolFunc.h"
#include "../../common//UploadBufferResource.h"
using namespace DirectX::PackedVector;
using namespace DirectX;

//定义顶点结构体
struct Vertex
{
	XMFLOAT3 Pos;
	XMCOLOR Color;
};

//单个物体的物体常量数据(不变的)
struct ObjectConstants
{
	XMFLOAT4X4 world = MathHelper::Identity4x4();
};
//单个物体的过程常量数据(每帧变化)
struct PassConstants
{
	XMFLOAT4X4 viewProj = MathHelper::Identity4x4();
};

struct FrameResources
{
public:
	FrameResources(ID3D12Device* device, UINT passCount, UINT objCount);
	FrameResources(const FrameResources& rhs) = delete;
	FrameResources& operator = (const FrameResources& rhs) = delete;
	~FrameResources();

	//每帧资源都需要独立的命令分配器
	ComPtr<ID3D12CommandAllocator> cmdAllocator;
	//每帧都需要单独的资源缓冲区(此案例仅为2个常量缓冲区)
	std::unique_ptr<UploadBufferResource<ObjectConstants>> objCB = nullptr;
	std::unique_ptr<UploadBufferResource<PassConstants>> passCB = nullptr;
	//CPU端的围栏值
	UINT64 fenceCPU = 0;
};

接着在.cpp文件中赋值类中变量。为什么每个帧资源需要单独创建一个commandAllocator呢?因为在绘制开始时,我们需要重置commandAllocator,为了保证其他两帧的绘制命令不被提前释放,所以我们每个帧资源都设置一个commandAllocator。

#include"FrameResources.h"

FrameResources::FrameResources(ID3D12Device* device, UINT passCount, UINT objCount)
{
	ThrowIfFailed(device->CreateCommandAllocator(
		D3D12_COMMAND_LIST_TYPE_DIRECT, 
		IID_PPV_ARGS(&cmdAllocator)));

	objCB = std::make_unique<UploadBufferResource<ObjectConstants>>(device, objCount, true);
	passCB = std::make_unique<UploadBufferResource<PassConstants>>(device, passCount, true);
}

FrameResources::~FrameResources(){}

然后我们在主文件中,填充帧资源数组。

int frameResourcesCount = 3;
void ShapesApp::BuildFrameResources()
{
	for (int i = 0; i < frameResourcesCount; i++)
	{
		FrameResourcesArray.push_back(std::make_unique<FrameResources>(
			d3dDevice.Get(),
			1,     //passCount
			(UINT)allRitems.size()));	//objCount
	}
}

2.同步CPU和GPU

同步原理开篇已经叙述,我们只需在Update函数中检查GPU是否命中围栏点,如果命中则GPU空闲,如果没有命中,则令CPU等待。可以看到,一开始我们就利用取模运算,循环遍历环形数组中的三个元素。

//每帧遍历一个帧资源(多帧的话就是环形遍历)
currFrameResourcesIndex = (currFrameResourcesIndex + 1) % frameResourcesCount;
currFrameResources = FrameResourcesArray[currFrameResourcesIndex].get();
//如果GPU端围栏值小于CPU端围栏值,即CPU速度快于GPU,则令CPU等待
if (currFrameResources->fenceCPU != 0 && fence->GetCompletedValue() < currFrameResources->fenceCPU)
{
	HANDLE eventHandle = CreateEvent(nullptr, false, false, L"FenceSetDone");
	ThrowIfFailed(fence->SetEventOnCompletion(currFrameResources->fenceCPU, eventHandle));
	WaitForSingleObject(eventHandle, INFINITE);
	CloseHandle(eventHandle);
}

以上代码判定的依据是什么?自然是fence了。我们在Draw函数里提交完命令后,需要将CPU端的fence++,并且当GPU处理完命令后将GPU端的fence++。这样,我们就可以将FlushCmdQueue函数去掉了。

void ShapesApp::Draw()
{
        ......

	UINT currentFence = 0;
	currFrameResources->fenceCPU = ++currentFence;
	cmdQueue->Signal(fence.Get(), currentFence);
}

3.传输数据

接着我们将objConstants传至GPU,可以看到,当传完3个帧资源的数据后,numFramesDirty=0,就不会继续传值了。这样就保证了数据及时更新到帧资源中。

ObjectConstants objConstants;
PassConstants passConstants;
for (auto& e : allRitems)
{
	if (e->numFramesDirty > 0)
	{
		world = e->world;
		XMMATRIX w = XMLoadFloat4x4(&world);
		//XMMATRIX赋值给XMFLOAT4X4
		XMStoreFloat4x4(&objConstants.world, XMMatrixTranspose(w));
		//将数据拷贝至GPU缓存
		currFrameResources->objCB->CopyData(e->objCBIndex, objConstants);

		e->numFramesDirty--;
	}
}

然后将passConstants也传至GPU,这里的代码跟之前并没有变化,因为在一个帧资源中,passConstants只有一个。

float x = radius * sinf(phi) * cosf(theta);
float y = radius * cosf(phi);
float z = radius * sinf(phi) * sinf(theta);
XMVECTOR pos = XMVectorSet(x, y, z, 1.0f);
XMVECTOR target = XMVectorZero();
XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);
XMMATRIX v = XMMatrixLookAtLH(pos, target, up);
//投影矩阵数据传递至GPU
XMMATRIX p = XMLoadFloat4x4(&proj);
//矩阵计算
XMMATRIX VP_Matrix = v * p;
XMStoreFloat4x4(&passConstants.viewProj, XMMatrixTranspose(VP_Matrix));
currFrameResources->passCB->CopyData(0, passConstants);

3.修改CBV

首先我们来理下思绪,现在的情况是:一个常量缓冲区堆中存放着objCB和passCB两种类型的常量缓冲区。objCB中存放着objectCount个(22个)子缓冲区,passCB中存放着1个子缓冲区,一共23个子缓冲区。现在有3个帧资源,所以我们要将23*3,一共是69个子缓冲区,即需要69个CBV。

我们创建CBV堆,其中的描述符数量应该就是69。

UINT objectCount = (UINT)allRitems.size();
int frameResourcesCount = 3;

cbHeapDesc.NumDescriptors = (objectCount + 1) * frameResourcesCount;

接着创建objCBV,现在我们要考虑帧资源因素,所以套2层循环,外层循环控制帧资源,内层循环控制渲染项。注意:此时的堆中地址要考虑第几个帧资源元素,所以heapIndex的值是objectCount * frameIndex + i。

std::vector<std::unique_ptr<FrameResources>> FrameResourcesArray;
for (int frameIndex = 0; frameIndex < frameResourcesCount; frameIndex++)
{
	for (int i = 0; i < objectCount; i++)
	{
		D3D12_GPU_VIRTUAL_ADDRESS objCB_Address;
		objCB_Address = FrameResourcesArray[frameIndex]->objCB->Resource()->GetGPUVirtualAddress();
		objCB_Address += i * objConstSize;//子物体在常量缓冲区中的地址
		int heapIndex = objectCount * frameIndex + i;	//CBV堆中的CBV元素索引
	        auto handle = CD3DX12_CPU_DESCRIPTOR_HANDLE(cbvHeap->GetCPUDescriptorHandleForHeapStart());
		handle.Offset(heapIndex, cbv_srv_uavDescriptorSize);	//CBV句柄(CBV堆中的CBV元素地址)
		//创建CBV描述符
		D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc;
		cbvDesc.BufferLocation = objCB_Address;
		cbvDesc.SizeInBytes = objConstSize;
		d3dDevice->CreateConstantBufferView(&cbvDesc, handle);
	}
}

然后创建passCBV,因为有了帧资源因素,所以我们要加一层循环,来生成3个passCBV。这里需要修改heapIndex值,因为passCBV堆中的地址是在objCBV后面的,即要在22*3个的objCBV之后。

for (int frameIndex = 0; frameIndex < frameResourcesCount; frameIndex++)
{
	D3D12_GPU_VIRTUAL_ADDRESS passCB_Address;
	passCB_Address = FrameResourcesArray[frameIndex]->passCB->Resource()->GetGPUVirtualAddress();
	int passCbElementIndex = 0;
	passCB_Address += passCbElementIndex * passConstSize;
	int heapIndex = objectCount * frameResourcesCount + frameIndex;
	auto handle = CD3DX12_CPU_DESCRIPTOR_HANDLE(cbvHeap->GetCPUDescriptorHandleForHeapStart());
	handle.Offset(heapIndex, cbv_srv_uavDescriptorSize);
	//创建CBV描述符
	D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc;
	cbvDesc.BufferLocation = passCB_Address;
	cbvDesc.SizeInBytes = passConstSize;
	d3dDevice->CreateConstantBufferView(&cbvDesc, handle);
}

4.绘制

相应的,我们也要将帧资源因素在DrawRenderItems函数中考虑进去。这里设置跟描述符表objCbvIndex的地址和objCBV的地址是同一个,加上帧资源索引和每个帧资源的物体个数乘积即可。

void ShapesApp::DrawRenderItems()
{
	......//将智能指针数组转换成普通指针数组

	//遍历渲染项数组
	for (size_t i = 0; i < ritems.size(); i++)
	{
		......//设置顶点索引缓存

		//设置根描述符表
		UINT objCbvIndex = currFrameResourcesIndex * (UINT)allRitems.size() + ritem->objCBIndex;
		auto handle = CD3DX12_GPU_DESCRIPTOR_HANDLE(cbvHeap->GetGPUDescriptorHandleForHeapStart());
		handle.Offset(objCbvIndex, cbv_srv_uavDescriptorSize);

                ......//绘制
	}
}

最后,在Draw函数中,也要将第2个CBV描述符表的堆地址改了。

int passCbvIndex = (int)allRitems.size() * frameResourcesCount + currFrameResourcesIndex;
auto passCbvHandle = CD3DX12_GPU_DESCRIPTOR_HANDLE(cbvHeap->GetGPUDescriptorHandleForHeapStart());
passCbvHandle.Offset(passCbvIndex, cbv_srv_uavDescriptorSize);
cmdList->SetGraphicsRootDescriptorTable(1, //根参数的起始索引
	passCbvHandle);

差点忘记很重要的一点。那就是cmdAllocator,因为每个帧资源都有自己的命令分配器,所以在Draw函数开始,我们就要Reset当前帧资源中的cmdAllocator。

auto currCmdAllocator = currFrameResources->cmdAllocator;
ThrowIfFailed(currCmdAllocator->Reset());//重复使用记录命令的相关内存
ThrowIfFailed(cmdList->Reset(currCmdAllocator.Get(), PSO.Get()));//复用命令列表及其内存

编译运行,效果和之前是一样的,但现在的同步机制是和之前不同的。为了之后光照的计算,我们将模型实体显示。

5050030534feb6247cf7c4cedfff1f0e.png

到这儿,我们就完成了这个案例此章的所有代码。通过这个案例我们学到了模型静态合批处理、模型实例复制、同步性优化等知识。知识点看似不多,但是它们互相之间环环相扣,特别是在计算各种地址的时候需要非常小心。好了,今天就先到这里,下篇见。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值