admin管理员组

文章数量:1582371

工程GIT地址:https://gitee/yaksue/yaksue-graphics

目标

初始化各个图形API,并尝试调用Clear命令让其以指定颜色填充屏幕:

目前的抽象

目前结构大体上是:application层负责用GLFW创建窗口,之后会将窗口传递给renderer层,而它负责调用具体的图形API。也就是说:

Application(应用层) Renderer(渲染器) 图形API

不过,我明白,像Vulkan,D3D12这种比较“先进”的图形API,和OpenGL,D3D11这种“传统”的图形API之间是有较大【差异】的。
正如Introduction - Vulkan Tutorial所说:

Vulkan is a new API by the Khronos group (known for OpenGL) that provides a much better abstraction of modern graphics cards. This new interface allows you to better describe what your application intends to do, which can lead to better performance and less surprising driver behavior compared to existing APIs like OpenGL and Direct3D. The ideas behind Vulkan are similar to those of Direct3D 12 and Metal
_
However, the price you pay for these benefits is that you have to work with a significantly more verbose API. Every detail related to the graphics API needs to be set up from scratch by your application, including initial frame buffer creation and memory management for objects like buffers and texture images. The graphics driver will do a lot less hand holding, which means that you will have to do more work in your application to ensure correct behavior.
_
The takeaway message here is that Vulkan is not for everyone. It is targeted at programmers who are enthusiastic about high performance computer graphics, and are willing to put some work in. If you are more interested in game development, rather than computer graphics, then you may wish to stick to OpenGL or Direct3D

简单来说,这种【差异】源自于对显卡更底层的封装,而这也意味着操纵难度更高,因为需要关心更多的东西了。

因此,我想,我的Renderer可能也需要区分这种“传统”型的图形API和“先进”型图形API,因为他们的操控方式是不同的。
目前的结构是这样的:

派生 派生 操作 操作 派生 派生 派生 派生 Renderer StandardRenderer AdvancedRenderer StandardGraphicsInterface AdvancedGraphicsInterface OpenGLInterface D3D11Interface VulkanInterface D3D12Interface

Renderer最重要的一个方法就是Render(),会在主循环中调用。StandardRendererAdvancedRenderer被期望以不同方式操作。例如,目前对屏幕进行“Clear”:
StandardRenderer比较直白,直接调用Clear命令:

#define graphicsAPI_standard ((StandardGraphicsInterface*)graphicsAPI)

void StandardRenderer::Render()
{
	graphicsAPI_standard->Clear(0.4f, 0.5f, 0.6f, 1.0f);

	graphicsAPI->Present();
}

AdvancedRenderer则会先将Clear命令录制到命令表中,然后执行命令表,最后同样需要Present

#define graphicsAPI_Advanced ((AdvancedGraphicsInterface*)graphicsAPI)

void AdvancedRenderer::Render()
{
	for (int CommandListIndex = 0; CommandListIndex < graphicsAPI_Advanced->QueryCommandListCount(); CommandListIndex++)
	{
		graphicsAPI_Advanced->BeginRecordCommandList(CommandListIndex);//开始录制命令
		{
			//清理颜色
			graphicsAPI_Advanced->CmdClear(0.6f, 0.1f, 0.1f, 1.0f);
		}
		graphicsAPI_Advanced->EndRecordCommandList(CommandListIndex);//结束录制命令
	}

	//执行命令
	graphicsAPI_Advanced->ExecuteCommandLists();

	graphicsAPI->Present();
}

(需要强调的是,这一抽象结构只是【目前】的理解,随着理解的深入,结构很可能有调整)

初始化图形API

所谓【初始化】指在做一切“有意思”的活动之前必须做的事情,而且只做一次(对于“一次”这个特点我并不确定,可能未来发现有一些操作需要执行多次,那么到时候就需要从初始化的函数中拆出了)。

OpenGL初始化

不出意外,OpenGL是其中最简单的,很大的原因在于我使用GLFW作为生成窗口的库,而这个库本来就是为了照顾OpenGL的。
因此,在GLFW下,初始化OpenGL只需要两个语句:

/* Make the window's context current */
glfwMakeContextCurrent(p_window);

//使用GLAD来加载OpenGL的函数地址
gladLoadGL();

D3D11初始化

在之前的博客《试用GLFW并创建OpenGL和DX的环境》中已经有了介绍了。

D3D12初始化

代码上我主要参考了microsoft/DirectX-Graphics-Samples中的“Hellow Window”工程。
至于概念上的学习,我推荐“DX12龙书”:《Introduction to 3D Game Programming with DirectX12 》


另外,代码中用到了d3dx12.h这个文件,它和d3dx11.h的角色类似,都是原始的API规范之外的一些辅助方法。
介绍它的官网是:
Helper Structures and Functions for D3D12 - Win32 apps | Microsoft Docs
下载地址:
DirectX-Graphics-Samples/Libraries/D3DX12 at master · microsoft/DirectX-Graphics-Samples · GitHub

Vulkan初始化

Vulkan的初始化相比上面的而言,就是一个较为“宏大”的工程了。概念上的学习以及代码,基本上是参考https://vulkan-tutorial/的。
(我在很久之前学习过这个教程,并将代码放在了GIT上)

简单观察初始化阶段的异同

所有的共同之处

一个在所有图形API之间都共有的特点是:初始化阶段都一定需要“窗口”的参与。D3D11和D3D12在创建【交换链】时用到了窗口的HWND。而Vulkan使用窗口创建了一个VkSurfaceKHR对象,而它对于之后创建【交换链】以及相关的对象是必须的。


当然,OpenGL这里的初始化太简单了,没有看到显式的【交换链】,但我想封装在内部的操作会有类似的行为。


严格意义上讲,如果不是做实时渲染而是只想使用显卡“并行计算”的功能,那“窗口”也不是必须的。
正如Window surface - Vulkan Tutorial所说:

It should also be noted that window surfaces are an entirely optional component in Vulkan, if you just need off-screen rendering. Vulkan allows you to do that without hacks like creating an invisible window (necessary for OpenGL).

D3D11和D3D12相似的地方

D3D11与D3D12同属于“DirectX”家族,目前看到的相似之处有:

Device

在开始都创建了一个“Device”,对于D3D11来说是ID3D11Device,而对于D3D12是ID3D12Device

交换链
  • 【交换链】都是DXGI(DirectX Graphics Infrastructure)中的概念。(尽管D3D12使用了更高的版本)
  • 在“呈现(Present)”,二者调用的接口是一样的,都是IDXGISwapChain::Present 。不过当前代码中的参数不一样:SyncInterval(An integer that specifies how to synchronize presentation of a frame with the vertical blank.)。这里有待后续研究。
RenderTargetView

在《DX12龙书》的【4.1.6 Resources and Descriptors】中,介绍了“显卡资源”和Descriptor。“RenderTarget”也是一个显卡资源,自然需要一个Descriptor来描述。
在D3D12中是ComPtr<ID3D12DescriptorHeap> RTVHeap
而D3D11里是ID3D11RenderTargetView* RenderTargetView

虽然用词不同,但是《DX12龙书》指出DescriptorView其实是同义词:

A view is a synonym for descriptor. The term “view” was used in previous versions of Direct3D, and it is still used in some parts of the Direct3D 12 API. We use both interchangeably in this book; for example, constant buffer view and constant buffer descriptor mean the same thing.

D3D12与D3D11不同的地方

在D3D11中,执行命令的是 “ImmediateContext”ID3D11DeviceContext* ImmediateContext,例如:

ImmediateContext->ClearRenderTargetView( ...

命令被“立即”执行。

但在D3D12中,没有 “ImmediateContext” 这个概念,所有的命令都被录制到ComPtr<ID3D12GraphicsCommandList> CommandList中:

CommandList->ClearRenderTargetView( ...

随后一个“命令队列”对其执行:

ID3D12CommandList* ppCommandLists[] = { CommandList.Get() };
CommandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);

准确来说,其实D3D11也有类似的机制,叫DeferredContext

There is also such a thing called a deferred context (ID3D11Device::CreateDeferredContext). This is part of the multithreading support in Direct3D 11

(不过,我猜测其功能应该达不到D3D12想要实现的效果,所以在我的学习工程中我不准备使用DeferredContext,而仅仅把D3D11当作一个“传统”的图形API,使用ImmediateContext做事情。)

Vulkan和D3D12相似的地方

粗略来看,二者在初始化阶段想要创建出的东西有很多相似的:

D3D12:

  1. 使用D3D12CreateDevice函数创建出 Device
  2. 创建CommandQueue
  3. 创建SwapChain
  4. 创建RenderTarget
  5. 创建CommandAllocator和CommandList
  6. 创建同步用的东西

Vulkan:

  1. 创建Instance
  2. 创建Surface
  3. 选择合适的显卡
  4. 创建VkDevice并获得GraphicsQueue和PresentQueue
  5. 创建SwapChain及相关对象
  6. 创建RenderPass
  7. 创建CommandPool
  8. 创建深度相关资源
  9. 创建FrameBuffers
  10. 创建CommandBuffer
  11. 创建Semaphores(同步用)

具体来说:

命令

“命令”方面二者有很大的相似性:
D3D12有ID3D12GraphicsCommandList负责录制命令,而Vulkan中命令被放在VkCommandBuffer中。
当执行命令时,D3D12的命令队列叫ID3D12CommandQueue,而Vulkan中叫VkQueue(尽管当前代码中有两个Queue:GraphicsQueue和PresentQueue)

同步的操作

在之前接触D3D11时,我没有印象需要手动做同步方面的操作。但在D3D12和Vulkan的范例代码中我都看到了需要做这些工作。
关于同步,我的理解还比较浅,待后续更深的研究。。。

Vulkan和D3D12不同的地方

虽然同为“最先进”的图形API,但是D3D12和Vulkan在目前看来还是有不少差异。对二者的比较学习要放在另一个篇章中了,因为内容量还是较多的。

这里只是说一些观察到的较为“肤浅”的不同

语言风格

在D3D12里,“队列”要执行命令是:

CommandQueue->ExecuteCommandLists( ...

这很直白。
而Vulkan是:

vkQueueSubmit(GraphicsQueue, ...

我觉得vkQueueSubmit理论上可以变成是一个“GraphicsQueue”的成员函数,这样更容易理解。但Vulkan中并没有这样,就好像“类”,“成员函数”这种语法在Vulkan中不存在一样。OpenGL的API似乎也是这样。我暂时不清楚这样的用意(和它想要成为“跨平台”的API有关吗?)

更复杂的Vulkan

相比目前工程里一百多行的初始化D3D12代码,工程里Vulkan的初始化代码大约六百行左右,使其看起来过于复杂了。。。

不过我不认为这代表着Vulkan提供了更多操作显卡的细节,我觉得同为“先进”的图形API,D3D12所提供的抽象层级应该和Vulkan是平级的。而之所以目前工程里的代码量这么多,我觉得原因可能有:

  • 所学习的Vulkan教程,有意地展示了更多的细节来让读者学习。
  • 可能当前在初始化D3D12时用了很多“缺省默认”的量,如果想指定细节,D3D12可能也支持。

未来学习目标

  • 当前工程中部分代码排列略乱,注释较少(尤其是Vulkan部分)。应该比照着教程再复习一遍,并做好注释。
  • CPU和GPU的同步,GPU命令队列之间的同步,同步技术(Fence,Semaphore)待后续学习。

本文标签: 图形异同初始化阶段工程