Vulkan簡介

最近學習了一下Vulkan,通過這篇文章來對我所學的知識進行一個總結。

在這裏插入圖片描述

前言

Vulkan可以認爲是Opengl版本的重寫,它提供高性能和低CPU負擔,天然支持多線程,能較好發揮多核CPU的性能,是一個能和DX12相提並論的東西。同時Vulkan幾乎支持所有平臺,跨平臺API具有非常好的優勢。Vulkan把驅動層做的很薄,把很多權限交給開發者,使開發者能更精確地控制渲染流程和資源管理。把很多功能做成一種擴展的形式,當你需要的時候才把它加進來。有的擴展還可以在開發的時候加入來提高開發效率,發佈的時候將其去掉提高運行效率。也可以把傳統的Opengl當做C#、Vulkan當C++來理解吧。

Vulkan概念介紹

VkPhysicalDevice & VkDevice

這是Vulkan獨有的,VkPhysicalDevice是物理設備或者說顯卡,程序啓動前要找到一塊支持Vulkan的顯卡,比如是否支持GeometryShader,是否支持光追擴展等。VkDevice就是從PhysicalDevice中創建得到的一個虛擬Device,在代碼中都是通過這的虛擬Device來控制顯卡的。在這個虛擬的Device中也需要檢查是否支持需要的Queue、數據格式等。

Pipeline:

傳統API:
1、可以認爲只有一條全局管線,驅動會記錄狀態,比如開啓了模板測試,後面的所有DrawCall就會開啓模板測試。
Vulkan:
1、有一個叫VkPipeline的類型,每一個DC都可以對應一條管線,管線的設置項非常多,比如頂點數據輸入信息、數據格式、Viewport、Shader、MultiSample、屬於哪個RenderPass的第幾個Subpass等等。
2、管線創建後基本是不允許修改的,只有少量的信息可以修改,要修改這些信息也要在創建管線的時候提前聲明。
3、管線的創建可以用pipelineCache加速創建速度,緩存信息可以從文件讀取。
下面是創建管線的一些基本設置。
		VkGraphicsPipelineCreateInfo pipelineInfo = {};
		pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
		pipelineInfo.stageCount = 2;
		pipelineInfo.pStages = shaderStage;
		pipelineInfo.pVertexInputState = &vertexInputInfo;
		pipelineInfo.pInputAssemblyState = &inputAssembly;
		pipelineInfo.pViewportState = &viewportState;
		pipelineInfo.pRasterizationState = &rasterizer;
		pipelineInfo.pMultisampleState = &multisampling;
		pipelineInfo.pDepthStencilState = nullptr;
		pipelineInfo.pColorBlendState = &colorBlending;
		pipelineInfo.pDynamicState = nullptr;
		pipelineInfo.layout = pipelineLayout;
		pipelineInfo.renderPass = renderPass;
		pipelineInfo.subpass = 0;
		pipelineInfo.basePipelineHandle = VK_NULL_HANDLE;
		pipelineInfo.basePipelineIndex = -1;

		if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &graphicsPipeline) != VK_SUCCESS) {
			throw std::runtime_error("failed to create graphics pipeline!");
		}

總結:每個DC可以有一個獨立管線,減少DC之間的耦合,有利於並行。管線可以提前創建,運行時不能修改,如果要切換一些渲染狀態, 可以直接綁定另一個Pipeline,狀態切換消耗低,保證高效。不像傳統API,不同DC間要大量設置狀態,驅動要做檢查。

Buffer

Buffers in Vulkan are regions of memory used for storing arbitrary data that can be read by the graphics card。Vulkan中的Buffer是一塊用來存儲顯卡可以讀取的的任意數據存儲區。Buffer的類型有幾種,比如有Device Local 的,在創建這種Buffer要是使用一個叫VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT的參數。這種存儲區域是顯卡本地的,而且CPU不能訪問的。而CPU能訪問的存儲區域,顯卡訪問性能比上面的略差一點。這些存儲區域主要是顯存和主存的一小部分區域。當顯存用完可以把顯存的數據交換到主存。這些存儲區域是開放的,像一個數組,由開發者自己管理。

	//with cpu assessable memory
	void createVertexBuffers() {
		VkDeviceSize bufferSize = sizeof(vertices[0])*vertices.size();
		//VK_MEMORY_PROPERTY_HOST_COHERENT_BIT使用這個bit可以保證顯卡在使用數據之前,數據已經複製到vertexbfferMemory了
		//不會因爲cache導致數據還沒過去(也可以flush一下)
		createBuffer(bufferSize, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
			VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, vertexBuffer, vertexBufferMemory);
	
		void* data;
		vkMapMemory(device, vertexBufferMemory, 0, bufferSize, 0, &data);
		memcpy(data, vertices.data(), (size_t)bufferSize);
		vkUnmapMemory(device, vertexBufferMemory);
	}

這是創建VertexBuffer的一點代碼,這裏使用的是HOST_VISIBLE的存儲類型,就是CPU能訪問的,這種的使用比較簡單,直接map然後memcpy複製就行。

Image

Image在Vulkan中也是對應一種類型(VkImage),Image的用途非常多,具體如下所示。創建的時候可以指定Iamge大小,數據類型、數據佈局、使用的顏色空間等。
在這裏插入圖片描述
其中值得一提的是數據佈局,數據的不同佈局會影響效率,比如你是一行一行讀取Image,但是存儲卻是一列一列的,會降低命中率。下面是一些ImageLayout的選項。

typedef enum VkImageLayout {
    VK_IMAGE_LAYOUT_UNDEFINED = 0,
    VK_IMAGE_LAYOUT_GENERAL = 1,
    VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL = 2,
    VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL = 3,
    VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL = 4,
    VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL = 5,
    VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL = 6,
    VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL = 7,
    VK_IMAGE_LAYOUT_PREINITIALIZED = 8,
    VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL = 1000117000,
    VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL = 1000117001,
    VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL = 1000241000,
    VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL = 1000241001,
    VK_IMAGE_LAYOUT_STENCIL_ATTACHMENT_OPTIMAL = 1000241002,
    VK_IMAGE_LAYOUT_STENCIL_READ_ONLY_OPTIMAL = 1000241003,
    VK_IMAGE_LAYOUT_PRESENT_SRC_KHR = 1000001002,
    VK_IMAGE_LAYOUT_SHARED_PRESENT_KHR = 1000111000,
    VK_IMAGE_LAYOUT_SHADING_RATE_OPTIMAL_NV = 1000164003,
    VK_IMAGE_LAYOUT_FRAGMENT_DENSITY_MAP_OPTIMAL_EXT = 1000218000,
    VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL_KHR = VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL,
    VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL_KHR = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL,
    VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL_KHR = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL,
    VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL_KHR = VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL,
    VK_IMAGE_LAYOUT_STENCIL_ATTACHMENT_OPTIMAL_KHR = VK_IMAGE_LAYOUT_STENCIL_ATTACHMENT_OPTIMAL,
    VK_IMAGE_LAYOUT_STENCIL_READ_ONLY_OPTIMAL_KHR = VK_IMAGE_LAYOUT_STENCIL_READ_ONLY_OPTIMAL,
    VK_IMAGE_LAYOUT_BEGIN_RANGE = VK_IMAGE_LAYOUT_UNDEFINED,
    VK_IMAGE_LAYOUT_END_RANGE = VK_IMAGE_LAYOUT_PREINITIALIZED,
    VK_IMAGE_LAYOUT_RANGE_SIZE = (VK_IMAGE_LAYOUT_PREINITIALIZED - VK_IMAGE_LAYOUT_UNDEFINED + 1),
    VK_IMAGE_LAYOUT_MAX_ENUM = 0x7FFFFFFF
} VkImageLayout;

Descriptor Sets & Layouts

Vulkan強調資源的複用,資源可以通過DescriptorSet綁定到Shader使用,一個Image綁定到上一個DC作爲ColorAttachment,再綁定到當前DC做普通紋理使用。綁定的方式可以通過Multi-Set或者Multi-Binding。下面是使用Multi-Binding來綁定數據的Shader。這些資源佈局和綁定和Pipeline類似都是預先創建好的,驅動不做驗證,運行時Shader通過descriptorSet的信息尋找資源。

layout (binding = 0, rgba32f) uniform readonly image2D samplerPositionDepth;
layout (binding = 1, rgba8) uniform readonly image2D samplerNormal;
layout (binding = 2, rgba32f) uniform readonly image2D ssaoNoise;
layout (binding = 3, rgba8) uniform image2D resultImage;

CommandBuffer

傳統API的渲染方式:
1、bind&draw的模式,綁定一些東西然後Draw,再綁定一些東西再Draw,是同步的,非常適合串行。
2、命令在驅動層記錄和自動提交,開發者不知道命令什麼時候提交到顯卡計算。 如下圖所示,驅動會根據它的策略判斷是否應該提交命令,就有點像TCP,你的數據比協議頭還小,是不會立刻發出去的。這樣就會出現圖中的問題,如果第一次Submit的任務在第二次Submit前就已經完成了,則有一段時間GPU不工作,就會產生氣泡,GPU利用率不夠高。

在這裏插入圖片描述

Vulkan:(multi-thread rendering, control submission)
1、命令在CommandBuffer中記錄。
2、命令的記錄和提交是分離的,在使用Vulkan的時候記錄指令可以在MainLoop之前就記錄好,在主循環中直接Submit即可。
3、可以自定義提交的時機。在合適的時機提交命令可以提高CPU和GPU的利用率,減少一幀的計算時間。
4、可以創建多個CommandBuffer,多線程記錄命令。如下所示,左邊表示開兩個線程來記錄命令,充分發揮多核CPU的性能,提交完後再處理Physics和AI,這時候GPU和CPU一起工作,在EndOfFrame的時候所有渲染工作已經完成了。

在這裏插入圖片描述

爲什麼多線程渲染那麼重要?實際上很多遊戲都不止一個線程,如果渲染指令的提交只能單線程就會很拉胯。當使用多線程高效去計算遊戲各種對象下一幀的方位後,卻只能用單線程渲染所有對象,這時候很自然地就會想到把渲染也多線程一下。

應用實例:
比如要爲六個燈渲染各一張ShadowMap,在CPU多線程中可以像圖中那樣安排任務,其中橙色的代表指令提交。
在這裏插入圖片描述

總的來說,使用CommandBuffer可以更精確控制提交時機、提高運行效率。以前DrawCall會很嚴重限制幀率,十分消耗CPU資源,而現在則有所改觀,使用多線程可以提高利用率,縮短計算時間,在高端電腦遊戲中可以選擇壓榨性能,渲染更多物體,在手機上可以降低CPU的負擔。

GPU Queue

傳統API:
沒有Queue,渲染方式如圖,按照時間順序渲染。這樣的壞處是每次只能使用顯卡的一小部分功能。一般顯卡會有Copy Engine(專門做數據的複製傳輸)、Graphic Engine(管線化用來做渲染)、Compute Engine(只是計算數據)等。圖中在Stream textures階段時只有Copy Engine在工作,而其他不工作。
在這裏插入圖片描述
Vulkan
有Transfer、Graphic、Compute功能的隊列,其中帶Compute和Graphic的隊列都有Transfer功能,有的隊列同時支持Compute和Graphic,而有的只支持Compute。比如我的1660同時支持Compute和Graphic有16條Queue,在編程中是可以開啓多條線程向多條隊列提交渲染指令的,這些Queue並行計算,Vulkan也提供了一些同步原語可以對並行計算進行同步。其中,提交到同一條Queue的指令是按順序執行的。使用Queue後,上面的流程可以如下設計,其中最上面的是純粹離線計算,在Compute Queue算,中間的使用Graphic Queue計算。

在這裏插入圖片描述
總的來說,使用Queue可以充分利用GPU,可以進行異步計算,未來Compute Shader的應用會越來越多,把一些Graphic Queue上的計算工作抽出來到Compute Queue上計算,獲得更好的性能表現。資源加載也類似。

同步原語

因爲Vulkan任務提交和執行是異步的,使用多條Queue的時候計算也是並行的,不同的DC之間難免有依賴關係,需要進行一些同步操作。Vulkan提供了幾種不同粒度的同步原語,包括event、fence、barrier、semaphore。Vulkan的一些API可以自動wait和signal指定的同步原語,在資源上可以使用barrier定義依賴關係。比如在DC1中一張Image是一個被寫入的,在DC2中是隻讀的,而DC1和DC2是並行的,就有可能DC2執行,讀到空數據,然後DC1再去寫入,或者DC1和DC2同時讀寫之類的。這時候可以在DC2的RenderPass中對這個image定義一個Barrier。

			VkImageMemoryBarrier imageMemoryBarrier = {};
			imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
			// We won't be changing the layout of the image
			imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL;
			imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_GENERAL;
			imageMemoryBarrier.image = textureComputeTarget.image;
			imageMemoryBarrier.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
			imageMemoryBarrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; //注意這裏
			imageMemoryBarrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;  //注意這裏
			vkCmdPipelineBarrier(
				drawCmdBuffers[i],
				VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
				VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
				VK_FLAGS_NONE,
				0, nullptr,
				0, nullptr,
				1, &imageMemoryBarrier);

注意中間兩行,大概就是說我要等到這個Image從可寫變成可讀再執行後面的操作。

總結

Vulkan把驅動層的一些任務交給開發者,使開發者能夠以較低的消耗更深入地控制硬件,按照自己的需求來使用硬件,可以自定義內存管理,較好支持多線程。使用Vulkan的遊戲應該儘量使用多線程,把一些複雜計算從Graphic queue轉到Compute queue計算。由於本人能力有限,有錯誤的地方可以在評論中幫我指出。

Reference:

https://vulkan-tutorial.com/Introduction
https://www.khronos.org/registry/vulkan/specs/1.1-extensions/html/vkspec.html#introduction
https://zhuanlan.zhihu.com/p/20712354
https://zhuanlan.zhihu.com/p/73016473
https://www.youtube.com/watch?v=H1L4iLIU9xU
http://on-demand.gputechconf.com/gtc/2016/video/S6817.html
https://github.com/SaschaWillems/Vulkan

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章