Direct X 12 – Drawing II

本章节介绍了一些常用的渲染模式,而之后书中的demo都将运用该模式。本章节开篇将介绍一种渲染的优化手段,我们称其为“frame resource(每一帧的资源)”。有了frame resource,我们将修改之前的渲染逻辑与循环,之后就不需要每一帧都flush command queue(让CPU闲置以等待GPU处理完所有command list)。此外,我们将试验更多的root signature并学习另外几种root parameter类型:root descriptor和root constants。最后,我们将展示如何绘制一些更为复杂的物体;在本章的最后,我们将绘制一个场景包含山丘,峡谷,圆柱体,球体和一个波浪模拟动画。

目标:

1. 理解我们对于渲染过程的修改,为何不需要每一帧都flush command queue。

2. 学习另外两种root parameter类型:root descriptor和root constants。

3. 学习如何系统地生成并绘制其他几何体,如格子,圆柱体和球体。

4. 学习如何通过CPU生成顶点动画,使用动态的vertex buffer来改变顶点的坐标并上传至GPU。

7.1 Frame Resources(每一帧资源)

在之前的4.2章里面,我们讲到CPU和GPU是并行工作的。CPU构建并提交command list,而GPU处理在command queue中的command list。首要目标是让CPU和GPU保持工作。而在我们的demo中,我们每一帧都进行CPU与GPU同步。理由如下:

1. command allocator只有在GPU处理完command之后才能重置。假设我们并不同步CPU与GPU,那么在GPU处理完第n帧之前,CPU继续处理第n+1帧。如果CPU在第n+1帧时重置了command allocator,那么我们就等于清除了GPU正在处理的command。

2. 知道GPU处理完了引用了constant buffer的draw call之后,CPU才能更新该constant buffer。这个例子正如我们在之前4.2.2小节所描述的。假设我们并不同步CPU与GPU,那么在GPU处理完第n帧之前,CPU继续处理第n+1帧。如果在第n+1帧时CPU重写了constant buffer内的数据,那么GPU所得到的第n帧的constant buffer数据就是错误的。

所以,我们在每一帧结束时调用函数D3DApp::FlushCommandQueue以保证GPU处理完了当前帧所有的command。虽然这样解决了一部分问题,但是这个解决方法的效率并不高:

1. 在每一帧开始时,GPU没有任何command可以处理,因为我们需要清空command queue。之后需要继续等待,知道CPU构建并提交了可以执行的command list。

2. 在每一帧结束时,CPU需要等待GPU处理完所有command。

所以,每一帧中,CPU和GPU都会在某一刻闲置。

一个解决办法是创建一个循环的资源列表,其包含了每一帧CPU需要修改的资源。我们称这些资源为frame resource,一般来说,我们让资源列表含有三个frame resource元素。对于第n帧,CPU遍历该列表以获取可使用的frame resource(也就是并未被GPU使用的frame resource)。之后CPU进行资源更新,构建并提交command list,同时GPU正在处理第n-1帧。然后CPU继续处理第n+1帧,以此往复。如果frame resource列表拥有三个元素,那么CPU可以比GPU多处理两帧的资源,可以让GPU保持工作。以下是frame resource的类的代码,我们在本章中的“Shape”demo中使用。因为在此demo中,CPU只需要修改constant buffer,frame resource的类只包含了constant buffer。

// 其包含了每一帧中CPU构建command list所需要的资源。
// 每个游戏或者每个应用的frame resource都会不同。
struct FrameResource
{
public:
    FrameResource(ID3D12Device* device, UINT passCount, UINT objectCount);
    FrameResource(const FrameResource& rhs) = delete;
    FrameResource& operator=(const FrameResource& rhs) = delete;
    ~FrameResource();

    // 直到GPU处理完所有command,我们才能重置allocator,
    // 所以每个frame resource需要自己的allocator。
    Microsoft::WRL::ComPtr<ID3D12CommandAllocator> CmdListAlloc;

    // 直到GPU处理完当帧所引用constant buffer,我们才能更新该constant buffer。
    // 所以每个frame resource需要自己的constant buffer。
    std::unique_ptr<UploadBuffer<PassConstants>> PassCB = nullptr;
    std::unique_ptr<UploadBuffer<ObjectConstants>> ObjectCB = nullptr;

    // 标记GPU处理command的Fence point。
    // 该值告诉我们GPU此刻是否在处理该frame resource。
    UINT64 Fence = 0;
};

FrameResource::FrameResource(ID3D12Device* device, UINT passCount, UINT objectCount)
{
    ThrowIfFailed(device->CreateCommandAllocator(
        D3D12_COMMAND_LIST_TYPE_DIRECT,
        IID_PPV_ARGS(CmdListAlloc.GetAddressOf())));
    
    PassCB = std::make_unique<UploadBuffer<PassConstants>>(device, passCount, true);
    ObjectCB = std::make_unique<UploadBuffer<ObjectConstants>>(devicec, objectCount, true);
}

FrameResource::~FrameResource() {}

我们的demo将实例化一个拥有三个frame resource的vector并记录当前帧的frame resource:

static const int NumFrameResources = 3;
std::vector<std::unique_ptr<FrameResource>> mFrameResource;
FrameResource* mCurrFrameResource = nullptr;
int mCurrFrameResourceIndex = 0;

void ShapesApp::BuildFrameResources()
{
    for(int i = 0; i < NumFrameResources; ++i)
    {
        mFrameResource.push_back(std::make_unique<FrameResource>(
            md3dDevice.Get(), 1, (UINT)mAllRitems.size()));
    }
}

现在对于CPU来说,第n帧的算法如下:

void ShapesApp::Update(const GameTimer& gt)
{
    // 循环frame resource列表
    mCurrFrameResourceIndex = (mCurrFrameResourceIndex + 1) % NumFrameResources;
    mCurrFrameResource = mFrameResource[mCurrFrameResourceIndex];

    // 检测GPU是否处理完当前帧的command。
    // 如果没有,那么等待GPU处理完。
    if(mCurrFrameResource->Fence != 0 &&
        mCommandQueue->GetLastCompletedFence() < mCurrFrameResource->Fence)
    {
        HANDLE eventHandle = CreateEventEx(nullptr, false, false, EVENT_ALL_ACCESS);
        ThrowIfFailed(mCommandQueue->SetEventOnFenceCompletion(
            mCurrFrameResource->Fence, eventHandle));
        
        WaitForSingleObject(eventHandle, INFINITE);
        CloseHandle(eventHandle);
    }

    // 更新mCurrFrameResource
    ...
}

void ShapesApp::Draw(const GameTimer& gt)
{
    // 为本帧构建并上传command list。
    // 更新fence point
    mCurrFrameResource->Fence = ++mCurrentFence;

    // 设置新的fence point
    mCommandQueue->Signal(mFence.Get(), mCurrentFence);

    // GPU继续处理之前帧的command,但这并不会造成任何问题,
    // 因为我们并不会改变之前帧的frame resource。
    ...
}

请注意,以上这种方法并不能完全避免CPU或者GPU闲置。如果一个处理器处理每一帧的速度远高于另一个处理器,那么快的那个处理器终究会等待慢的处理器,因为我们并不能让快的处理器提前处理太多的帧数。如果GPU处理command的速度快于CPU提交command的速度,那么GPU将会闲置。总的来说,如果想要让GPU满负荷工作,我们就要避免这种情况,因为GPU闲置意味着我们没有完全利用GPU的性能。另一方面来说,如果CPU处理command的速度一直快于GPU,那么CPU在某个时间点将要等待GPU。但这正是我们想要的,由于GPU满负荷工作;额外的CPU运算能性能将能用来处理游戏中的其他模块,如AI,物理和game play逻辑等等。

那么如果多个frame resource并不能避免CPU闲置,它的作用到底是什么呢?多个frame resource可以让GPU持续工作。因为当GPU正在处理第n帧的command时,CPU将继续构建并提交第n+1和第n+2帧的command。这也意味着command queue并不会空置,那么GPU就能一直有command可以处理。

《Direct X 12 – Drawing II》有3条留言

  1. 哥们,7.7.4小节后的代码漏掉了
    void LandAndWavesApp::Draw(const GameTimer& gt)
    {
    […]

    // Bind per-pass constant buffer. We only need to do this once per-
    // pass.
    auto passCB = mCurrFrameResource->PassCB->Resource();
    mCommandList->SetGraphicsRootConstantBufferView(1, passCB –
    > GetGPUVirtualAddress());

    DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Opaque]);

    […]
    }

    void LandAndWavesApp::DrawRenderItems(
    ID3D12GraphicsCommandList* cmdList,
    const std::vector& ritems)
    {
    UINT objCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof
    (ObjectConstants));

    auto objectCB = mCurrFrameResource->ObjectCB->Resource();

    // For each render item…
    for (size_t i = 0; i IASetVertexBuffers(0, 1, &ri->Geo->VertexBufferView());
    cmdList->IASetIndexBuffer(&ri->Geo->IndexBufferView());
    cmdList->IASetPrimitiveTopology(ri->PrimitiveType);

    D3D12_GPU_VIRTUAL_ADDRESS objCBAddress = objectCB->GetGPUVirtualAddress();
    objCBAddress += ri->ObjCBIndex*objCBByteSize;

    cmdList->SetGraphicsRootConstantBufferView(0, objCBAddress);

    cmdList->DrawIndexedInstanced(ri->IndexCount, 1,
    ri->StartIndexLocation, ri->BaseVertexLocation, 0);
    }
    }

    回复
  2. 7.7.5的两段代码也漏掉了
    std::unique_ptr<UploadBuffer> WavesVB = nullptr;
    WavesVB = std::make_unique<UploadBuffer>(
    device, waveVertCount, false);

    void LandAndWavesApp::UpdateWaves(const GameTimer& gt)
    {
    // Every quarter second, generate a random wave.
    static float t_base = 0.0f;
    if((mTimer.TotalTime() – t_base) >= 0.25f)
    {
    t_base += 0.25f;

    int i = MathHelper::Rand(4, mWaves->RowCount() – 5);
    int j = MathHelper::Rand(4, mWaves->ColumnCount() – 5);

    float r = MathHelper::RandF(0.2f, 0.5f);

    mWaves->Disturb(i, j, r);
    }

    // Update the wave simulation.
    mWaves->Update(gt.DeltaTime());

    // Update the wave vertex buffer with the new solution.
    auto currWavesVB = mCurrFrameResource->WavesVB.get();
    for(int i = 0; i VertexCount(); ++i)
    { Vertex v;

    v.Pos = mWaves->Position(i);
    v.Color = XMFLOAT4(DirectX::Colors::Blue);

    currWavesVB->CopyData(i, v);
    }

    // Set the dynamic VB of the wave renderitem to the current frame VB.
    mWavesRitem->Geo->VertexBufferGPU = currWavesVB->Resource();
    }

    回复

留下评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据