将计算着色器的执行结果复制到系统内存
一般来说,在用计算着色器对纹理进行处理之后,我们就会将结果在屏幕上显示出来,并根据呈现的效果来验证计算着色器的准确性。但是,如果使用结构化缓冲区参与运算, 或使用GPGPU进行通用计算,则运算结果可能根本就无法显示出来。所以当前的燃眉之急是如何将GPU端显存(您是否还记得,在通过UAV向结构化缓冲区写入数据时,缓冲区其实是位于 显存之中)里的运算结果回传至系统内存中。首先,应以堆属性D3D12_HEAP_TYPE_READBACK来创建系统内存,再通过ID3D12GraphicsCommandList: : CopyResource方法将GPU资源复制到系统内存资源之中。其次,系统内存资源必须与待复制的资源有着相同的类型与大小。最后,还需用映射 API函数对系统内存缓冲区进行映射,使CPU可以顺利地读取其中的数据。至此,我们就能将数据复制 到系统内存块中了,可令CPU端对其开展后续的处理,或存数据于文件,或执行所需的各种操作。
本章包含了一个名为“VecAdd”的结构化缓冲区演示程序,它的功能比较简单,就是将分别存于两个结构化缓冲区中向量的对应分量进行求和运算:
struct Data
{
float3 v1;
float2 v2;
};
StructuredBuffer<Data> glnputA : register(t0);
StructuredBuffer<Data> glnputB : register(t1);
RWStructuredBuffer<Data> gOutput : register(u0);
[numthreads(32, 1, 1)]
void CS(int3 dtid : SV_DispatchThreadID)
{
gOutput[dtid.x].vl = glnputA[dtid.x].vl + glnputB[dtid.x].vl;
gOutput[dtid.x].v2 = glnputA[dtid.x].v2 + glnputB[dtid.x].v2;
}
为了方便起见,我们使每个结构化缓冲区中仅含有32个元素。因此,只需分派一个线程组即可(因为一个线程组即可同时处理32个数据元素)。待程序中的所有线程都完成计算着色器的运算任务之后, 我们将结果复制到系统内存,再保存于文件当中。下面的代码演示了如何创建系统内存缓冲区,以及怎样将GPU中的计算结果复制到CPU的内存:
//创建一个系统内存缓冲区,以便读回处理结果
ThrowlfFailed(md3dDevice->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_READBACK),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(byteSize),
D3D12_RESOURCE_STATE_COPY_DEST,
nullptr,
IID_PPV_ARGS(&mReadBackBuffer)));
// ...
//
//计算着色器执行完毕
struct Data
{
XMFLOAT3 vl;
XMFLOAT2 v2;
};
//按计划将数据从默认缓冲区复制到回读缓冲区(即系统内存缓冲区)中
mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(
mOutputBuffer.Get(),
D3D12_RESOURCE_STATE_COMMON,
D3 Dl2_RES0URCE_STATE_C0PY_S0URCE));
mCommandList->CopyResource(mReadBackBuffer.Get(), mOutputBuffer.Get());
mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition( mOutputBuffer.Get(),D3D12_RESOURCE_STATE_COPY_SOURCE, D3D12_RESOURCE_STATE_COMMON));
//命令记录完成
ThrowlfFailed (mCommanciList->Close ());
//将命令列表添加到命令队列中用于执行
ID3D12CommandList* cmdsLists[] = ( mCommandList.Get() }; mCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);
//等待命令执行完毕
FlushCommandQueue();
//对数据进行映射,以便CPU读取
Data* mappedData = nullptr;
ThrowlfFailed(mReadBackBuffer->Map(0, nullptr, reinterpret_cast<void**>(&mappedData)));
std::ofstream fout("results.txt");
for(int i = 0; i < NumDataElements; ++i)
{
fout << "("<< mappedData[i].vl.x <<","<<
mappedData[i].vl.y<<","<<
mappedData[i].vl.z<<","<<
mappedData[i].v2.x<<","<<
mappedData[i].v2.y<<","<<std::endl;
}
mReadBackBuffer->Unmap(0, nullptr);
在这个演示程序中,我们用下列初始数据来填写两个输入缓冲区:
std::vector<Data> dataA(NumDataElements);
std::vector<Data> dataB(NumDataElements);
for(int i = 0; i < NumDataElements; ++i)
{
dataA[i].vl = XMFLOAT3(i, i, i);
dataA[i].v2 = XMFLOAT2(i, 0);
dataB[i].vl = XMFLOAT3(-i, i, 0.0f);
dataB[i].v2 = XMFLOAT2(0,-i);
}
存有计算结果的文本文件应含有下列数据,据此我们便能确定计算着色器是否按预期完成任务:
(0, 0, 0, 0, 0)
(0, 2, 1, 1, -1)
(0, 4, 2, 2, -2)
(0, 6, 3, 3, -3)
(0, 8, 4, 4, -4)
(0, 10, 5, 5, -5)
(0, 12, 6, 6, -6)
(0, 14, 7, 1, -7)
(0, 16, 8, 8, -8)
(0, 18, 9, 9, -9)
(0, 20, 10, 10, -10)
(0, 22, 11, 11, -11)
(0, 24, 12, 12, -12)
(0, 26, 13, 13, -13)
(0, 28, 14, 14, -14)
(0, 30, 15, 15, -15)
(0, 32, 16, 16, -16)
(0, 34, 17, 17, -17)
(0, 36, 18z 18, -18)
(0, 38, 19, 19, -19)
(0, 40, 20, 20, -20)
(0, 42, 21, 21, -21)
(0, 44, 22, 22, -22)
(0, 46, 23, 23, -23)
(0, 48, 24, 24, -24)
(0, 50, 25, 25, -25)
(0, 52, 26, 26, -26)
(0, 54, 27, 27, -27)
(0, 56, 28, 28, -28)
(0, 58, 29, 29, -29)
(0, 60, 30, 30, -30)
(0, 62, 31, 31, -31)
CPU与GPU之间的存储器复制操作最为缓慢。而对于图形处理这 一角度来说,我们更是永远都不想在每一帧都执行这种复制操作,因为这样频繁地搬运数据对程序的性能而言无疑是毁灭性的的打击。并不是什么大难题,因为GPU运算所节省的时间远超GPU向CPU复制所花费的时间 一再者说,针对GPGPU编程而言,并不是“每一帧”都要执行这种复制操作。举个例 子,假设某个应用程序要通过GPGPU编程来实现一个开销极大的图形处理计算。在运算 结束之后,再将其处理结果复制到CPU。在这种情况下,GPU并不会立即开始下一次处理,而是只有在用户发起另一次计算请求时,它才会为此重新开动起来。