Skip to content

Commit

Permalink
make some reviews.
Browse files Browse the repository at this point in the history
  • Loading branch information
lijipeng787 committed Mar 28, 2018
1 parent ea792ae commit 08fe469
Showing 1 changed file with 25 additions and 49 deletions.
74 changes: 25 additions & 49 deletions Chapter 4 Direct3D Initialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,60 +315,47 @@ struct DXGI_SAMPLE_DESC
## 4.2 CPU/GPU INTERACTION
我们知道图形编程中是有两个处理器同时工作的,一个`CPU`一个`GPU`。
他们同时工作,但有的时候他们需要进行同步。
为了达到理想的性能,我们需要尽可能的减少同步次数。
同步并不是很好的情况,这意味着会有一个处理器处于空闲状态去等待另外一个处理器完成他的任务。
换句话说,他破坏了他们互相间的运行的独立。
图形编程涉及两个处理器,一个是`CPU`,另一个是`GPU`。它们同时工作,但有时也需要进行同步。为了达到最佳的性能,我们需要尽可能的减少同步次数以使两个处理器长时间保持在工作状态。同步意味着会有一个处理器处于空闲并等待另外一个处理器完成任务。换句话说,同步破坏了并行性。
### 4.2.1 The Command Queue and Commmand Lists
`GPU`有一个指令队列(`Command Queue`)。`CPU`通过使用`Direct3D`中的指令表(`Command List`)将指令提交到指令队列中去(参见图片[4.6](#Image4.6))。
有一个重要的地方是,当一组指令被提交到指令队列后`GPU`并不会立马执行他。
这些指令将会放在指令队列里面,等待`GPU`处理完前面提交的指令后处理他。
`GPU`有一个指令队列(`Command Queue`)。`CPU`通过使用`Direct3D`中的指令列表(`Command List`)将指令提交到指令队列中去(参见图片[4.6](#Image4.6))。重点在于,当一组指令被提交到指令队列后,`GPU`并不会立马执行这些指令。这些指令将会放在指令队列里面,等待`GPU`处理完前面提交的指令后再处理后来的指令。
![Image4.6](Images/4.6.png)
> 图片4.6
如果指令队列是空的,那么由于没有事情可以处理,`GPU`就是空闲的了。
另一方面来说,如果指令队列太满了,那么`CPU`就可能需要等待`GPU`处理完他的指令。
这两种情况都不是很好的情况,这意味着有性能浪费。
如果指令队列是空的,那么由于没有工作可以处理,`GPU`空闲;另一方面,如果指令队列满了,那么`CPU`就需要等待`GPU`处理完队列中的指令来释放队列空间。这两种情况都不理想。对于游戏这样的高性能应用,充分使用`GPU`和`CPU`才能发挥最好的性能。
在`Direct3D 12`中,指令队列被声明为`ID3D12CommandQueue`。
它通过填充`D3D12_QUEUE_DESC`结构,调用`ID3D12Device::CreateCommandQueue`函数来创建。
在`Direct3D 12`中,`ID3D12CommandQueue`接口代表指令队列。通过填充`D3D12_QUEUE_DESC`结构并调用`ID3D12Device::CreateCommandQueue`函数即可创建指令队列。
下面是创建代码。
其中有一个成员函数`ExecuteCommandLists`就是可以将指令列表提交到指令队列中去
主要的成员函数`ExecuteCommandLists`用来将指令列表提交到指令队列中去
```C++
void ID3D12CommandQueue::ExecuteCommandLists(
UINT Count, //要提交的指令列表的数量
ID3D12CommandList *const *ppCommandLists); //指令列表的数组
```

我们可以从上面的代码看出,一个图形指令列表声明为`ID3D12GraphicsCommandList`他继承自`ID3D12CommandList`
图形指令列表有很多方法加入指令,例如设置`ViewPort`,清理`Render Target`。例如:
指令列表按数组中的顺序,从第一个开始。
从上面的代码看出,`ID3D12GraphicsCommandList`接口代表图形指令列表,该接口继承自`ID3D12CommandList`。图形指令列表有很多方法加入指令,例如设置`ViewPort`,清理`Render Target`,绘制指令。例如:

```C++
CommandList->RSSetViewports(1, &myViewport);
CommandList->ClearRenderTargetView(CurrentBackBuffer, Color, 0, nullptr);
CommandList->DrawIndexedInstanced(36, 1, 0, 0, 0);
```
虽然看起来这些函数是调用后就立马执行的,但是其实并不是。上面的代码只是将指令加入到指令列表中去而已。
`ExecuteCommandLists`才将指令加入到指令队列中去,然后`GPU`从指令队列中读取指令去处理指令。
通过这本书我们将会学习`ID3D12GraphicsCommandList`支持的各种不同的指令。
当我们将所有的指令加入到指令列表后,我们需要使用`ID3D12GraphicsCommandList::Close`去关闭指令队列。
虽然看起来这些函数是调用后就立马执行的,但是并不是。上面的代码只是将指令加入到指令列表中去而已。`ExecuteCommandLists`才将指令加入到指令队列中去,然后`GPU`从指令队列中读取指令去处理指令。
通过这本书我们将会学习`ID3D12GraphicsCommandList`支持的各种不同的指令。当我们将所有的指令加入到指令列表后,我们需要使用`ID3D12GraphicsCommandList::Close`去关闭指令队列。
```C++
CommandList->Close();
```

注意必须在指令列表被提交到指令队列前关闭他
注意指令列表被提交到指令队列前必须关闭指令列表

一个和指令列表有关联的就是指令分配器(`ID3D12CommandAllocator`),指令列表记录指令,而指令分配器则是存储具体的数据。
当一个指令列表被指令队列提交的时候,指令队列会使用指令分配器里面的数据。
指令分配器(`ID3D12CommandAllocator`)是一个和指令列表有关联的内存支持类,指令列表记录所有指令,而指令分配器则为存储的指令分配内存。当一个指令列表被指令队列提交的时候,指令队列会使用指令分配器里面的数据。

```C++
HRESULT ID3D12Device::CreateCommandAllocator(
Expand All @@ -377,9 +364,9 @@ void ID3D12CommandQueue::ExecuteCommandLists(
void **ppCommandAllocator);
```
- `type`: 允许什么类型的指令列表可以和指令分配器关联
- `type`: 指令列表可以和指令分配器相关联的类型
- `D3D12_COMMAND_LIST_TYPE_DIRECT`: 存储直接提交给`GPU`类型的指令列表
- `D3D12_COMMAND_LIST_TYPE_BUNDLE`: 由于`CPU`在构建指令列表的时候也是有开销的,因此`Direct3D 12`提供了一个优化,让我们能够记录一组指令到所谓的`Bundle`中去。在一个`Bundle`被记录后,驱动会预处理一些指令来在渲染的时候优化他的执行。因此,`Bundle`中的指令应当在开始的时候就记录下来。因为`Direct3D 12`的`API`效率已经足够高了,所以你并不需要经常使用`Bundle`,你最好只在你能够确保使用`Bundle`能够提高性能的情况下使用他。
- `D3D12_COMMAND_LIST_TYPE_BUNDLE`: 由于`CPU`在构建指令列表的时候也是有开销的,因此`Direct3D 12`提供了一个优化,让我们能够记录一组指令到所谓的`Bundle`中去。在一个`Bundle`被记录后,驱动会预处理一些指令来在渲染的时候优化其执行。因此,`Bundle`中的指令应当在开始的时候就记录下来。因为`Direct3D 12`的`API`效率已经足够高了,所以你并不需要经常使用`Bundle`,你最好只在你能够确保使用`Bundle`能够提高性能的情况下使用他。
- `riid`: `ID3D12CommandAllocator`的**COM ID**。
- `ppCommandAllocator`: 创建的指令分配器。
Expand All @@ -394,58 +381,49 @@ void ID3D12CommandQueue::ExecuteCommandLists(
);
```

- `nodeMask`: 通常设置为0。在这里他确定和这个指令队列关联的`GPU`是哪个
- `type`: 上面有解释了
- `nodeMask`: 通常设置为0。该参数确定和这个指令队列关联的`GPU`
- `type`: `D3D12_COMMAND_LIST_TYPE_DIRECT`或者`D3D12_COMMAND_LIST_TYPE_BUNDLE`,解释请参照上文
- `pCommandAllocator`: 关联的指令分配器。指令分配器支持的类型必须和这个指令列表类型一致。
- `pInitialState`: 使用的初始的渲染管道,没有的话就设置为`nullptr`
- `riid`: `ID3D12GraphicsCommandList`**COM ID**
- `ppCommandList`: 创建的指令列表。

你可以创建多个指令列表同时关联同一个指令分配器,但是你不能同时记录指令。
也就是说我们必须保证除了正在记录指令的那个指令列表外,其他的指令列表必须被关闭。
这样的话,所有的由这个指令列表发出的指令就在指令分配器那里是连续的了。
注意的是,只要一个指令列表是被创建或者重置(**Reset**)了,那么就代表他被打开了。
所以如果我们在一行代码里面创建两个关联同一个指令分配器的指令列表的话就会报错。
你可以使用同一个指令分配器关联多个指令列表,但是你不能同时记录指令。也就是说,我们必须保证除了正在记录指令的那个指令列表外,其他的指令列表必须被关闭。这样,所有的由这个指令列表发出的指令就可以在指令分配器中保持连续。需要注意的是,只要一个指令列表被创建或者被重置(**Reset**),那么就代表他被打开了。所以如果我们在一行代码里面创建两个关联同一个指令分配器的指令列表的话就会报错。

```Unknown Language
D3D12 ERROR: ID3D12CommandList:: {Create,Reset}CommandList: The command allocator is currently in-use by another command list.
```

在我们将一个指令列表提交给指令队列后,重新使用他的内存去记录新的指令是没问题的。
我们使用函数`ID3D12CommandList::Reset`来重置他。他们的参数和创建的时候差不多。
在我们将一个指令列表提交给指令队列后,可以通过`ID3D12CommandList::Reset`重用其内部存储来记录新的指令。该函数的参数和创建指令列表函数的参数类似。

```C++
HRESULT ID3D12CommandList::Reset(
ID3D12CommandAllocator *pAllocator,
ID3D12PipelineState *pInitialState);
```
这样的话我们就可以让指令列表变得和创建的时候一样了,并且可以重新使用他原本的内存空间,以至于可以不用释放内存重新创建一个新的指令列表了。
重置一个指令列表不会影响到指令队列里面的指令,因为那些指令存储仍然储存在指令分配器里面。
这样就可以让指令列表恢复到初始状态,并且可以重新使用其内存空间,避免释放内存并重新创建一个新的指令列表所带来的开销。注意,重置一个指令列表不会影响到指令队列里面的指令,因为那些指令存储仍然储存在指令分配器里面。
在我们已经提交完这一帧的渲染指令后,我们也可以重新使用指令分配器里面的内存。因此我们可以重置他
在我们已经提交完这一帧的渲染指令后,可以重新使用指令分配器里面的内存渲染下一帧
```C++
HRESULT ID3D12CommandAllocator::Reset();
```

这个方法类似于`std::vector::clear`。能让里面的值变为0,但是大小仍然不变。
当然由于指令队列要从指令分配器中获取指令数据,**一个指令分配器必须在`GPU`处理完这个分配器里面的所有指令后才可以重置**
函数`ID3D12CommandAllocator::Reset`背后的思想与`std::vector::clear`类似,当`vector`的内容清零时,并不会改变其容量。另外,由于指令队列中的指令使用指令分配器中的数据,**因此在`GPU`执行完所有指令分配器中存储的指令之前,一定不能对指令分配器做`reset`操作**。具体操作请见下文。

### 4.2.2 CPU/GPU Synchronization

由于由两个处理器同时在运行,一系列同步的问题就出现了。
假设我们有一些想要绘制资源**R**。然后再**p1**的时候,`CPU`更新了他的数据,并且发出了绘制他的指令**C**
由于加入指令到指令队列后,并不会阻碍`CPU`的继续运行,所以`CPU`继续运行。然后在**p2**的时候`CPU`重新更新了资源`R`的数据,然后将其提交到队列中去。
由于由两个处理器同时在运行,因此一系列同步的问题就出现了。
首先假设我们有一些想要绘制的资源(几何体位置信息)`R`。接下来,`CPU`更新`R`的数据,将其位置信息修改为`p1`,并且发出了绘制指令`C`,想要在`p1`这个位置绘制几何体。由于将指令到指令队列后并不会阻碍`CPU`,所以`CPU`继续运行。之后,`CPU`又更新资源`R`的信息为`p2`并将其提交到队列中去,然而此时GPU还没有执行在`p1`位置绘制几何体的指令,这种不加同步即对资源重写的操作会造成错误。
参见图片[4.7](#Image4.7)

![Image4.7](Images/4.7.png)
> 图片4.7
只是一个错误的例子。因为指令**C**绘制的图形可能是**p2**这个时候的数据信息,或者是**R**被更新过后的数据信息
这是一个错误的例子。因为指令`C`绘制的图形可能使用的是`p2`的数据,或者是`R`在更新过程中产生的数据

我们的解决方法就是让`CPU`等待`GPU`运行到某一时刻(**fence**)再运行。
我们称之为清理队列。我们可以使用`fence`来做到这点。
我们的解决方法就是让`CPU`等待`GPU`运行到某一时刻再运行,该操作称之为清空指令队列。我们可以使用`fence`来做到这点。`ID3D12Fence`接口用来同步`GPU``CPU``fence`可以由如下方法创建:

```C++
HRESULT ID3D12Device::CreateFence(
Expand All @@ -456,9 +434,7 @@ D3D12 ERROR: ID3D12CommandList:: {Create,Reset}CommandList: The command allocato
);
```
伴随着`fence`的还有一个`UINT64`的值。
我们初始化他为0,然后在每次我们需要在标志一个新的点作为同步的标志的时候,我们都需要将这个数字增加。
这里还是给个例子。
`fence`内部维护一个`UINT64`的值当作计数器。该计数器被初始化为0,每次我们需要标志一个新的点作为同步点的时候,将这个数字增加即可。下面给出使用`fence`清空指令队列的操作。
```C++
CommandQueue->Signal(Fence, ++CurrentFence);
Expand Down

0 comments on commit 08fe469

Please sign in to comment.