1. 程式人生 > >Vulkan程式設計指南翻譯 第六章 著色器和管線 第3節 管線

Vulkan程式設計指南翻譯 第六章 著色器和管線 第3節 管線

6.3  管線

如你在前面小節所讀到的,Vulkan使用著色器module來表示一系列的著色器程式通過把module 程式碼交給vkCreateShaderModule()可以建立著色器module,但是,在它們可以在裝置上被用來工作之前,你需要建立管線。在Vulkan中有兩種管線:計算和圖形。圖形管線相對複雜,且包含許多和著色器相關的狀態。然而,計算管線在概念上簡單多了,且出來著色器程式碼也不包含其他的什麼東西。

6.3.1 計算管線

在我們討論建立計算管線之前,我們應該講講計算管線的基礎知識。著色器和它的執行時Vulkan的核心。Vulkan也提供了對各種功能的固定塊大小的訪問,比如複製和處理畫素資料。然而,著色器將會是任何有意義程式的核心。

計算著色器提供了對Vulkan裝置計算能力的直接訪問。裝置可以被視為許多處理相關資料的寬向量處理單元的集合。一個計算著色器一般被認為會連續的,單軌道的執行。然而,有辦法可以讓多個軌道上一起指向。實際上,這也是大多數Vulkan 裝置如何被構造的。每一個執行軌道被稱為一個呼叫。

當計算著色器被執行時,許多呼叫馬上開始。這些呼叫被子和到一個固定大小的本地工作組,然後,一個或多個組被一起發射,它有時被稱為全域性工作組的。邏輯上,本地工作組和全域性工作組都是三維的。然而,設定三維的任一維度的大小減少了組的維度。

本地工作組的大小在計算著色器內部設定。在GLSL中,這是通過使用layout限定符做到的,限定符被翻譯為傳遞給

Vulkan的著色器的OpExecutionMod描述符上的 LocalSize修飾符。Listing 6.3 展示了在著色器中使用的大小修飾符,Listing 6.4 展示了生成的精簡的SPIR-V反彙編程式碼

Listing 6.3: Local Size Declaration in a Compute Shader (GLSL)

#version 450 core

layout (local_size_x = 4, local_size_y = 5, local_size_z 6) in;

void main(void)

{

// Do nothing.

}

Listing 6.4: Local Size Declaration in a Compute Shader (SPIR-V)

...

OpCapability Shader

%1 = OpExtInstImport "GLSL.std.450"

OpMemoryModel Logical GLSL450

OpEntryPoint GLCompute %4 "main"

OpExecutionMode %4 LocalSize 4 5 6

OpSource GLSL 450

...

你可以看到,Listing 6.4OpExecutionMode 指令設定著色器的本地大小為 {4, 5, 6},這在Listing 6.3中指定。

一個著色器程式的本地工作組的最大個數由呼叫vkGetPhysicalDeviceProperties()可以獲取VkPhysicalDeviceLimits的資料的maxComputeWorkGroupSize域指定,在第一章“Vulkan簡介”中解釋過。還有,本地工作組的最大呼叫數之和也是同一個型別資料的maxComputeWorkGroupInvocations域指定的。一個Vulkan實現也許會拒絕超過這些限制條件的SPIR-V著色器,即使只是會出現一些未知的執行結果而已。

6.3.2 建立管線

可呼叫vkCreateComputePipelines()來建立一個或多個管線,函式原型如下:

VkResult vkCreateComputePipelines (

VkDevice device,

VkPipelineCache pipelineCache,

uint32_t createInfoCount,

const VkComputePipelineCreateInfo* pCreateInfos,

const VkAllocationCallbacks* pAllocator,

VkPipeline* pPipelines);

vkCreateComputePipelines()的引數device,就是負責使用管線並扶著分配管線物件的裝置。pipelineCache是用來加速管線建立的一個物件的handle,在本章稍後涉及到。建立一個新管線的引數資訊通過一個VkComputePipelineCreateInfo型別的資料表示。該資料結構的個數(亦即需要建立的管線的個數)通過createInfoCount傳入,這寫資料組成的陣列的地址通過pCreateInfos傳入。VkComputePipelineCreateInfo的定義如下:

typedef struct VkComputePipelineCreateInfo {

VkStructureType sType;

const void* pNext;

VkPipelineCreateFlags flags;

VkPipelineShaderStageCreateInfo stage;

VkPipelineLayout layout;

VkPipeline basePipelineHandle;

int32_t basePipelineIndex;

} VkComputePipelineCreateInfo;

VkComputePipelineCreateInfosType應置為VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFOpNext應置為nullptrflags域保留使用,在當前的Vulkan版本中應置為0stage 域是一個嵌入式的結構,包含著色器本身的資訊,它是VkPipelineShaderStageCreateInfo型別的一個例項,定義如下:

typedef struct VkPipelineShaderStageCreateInfo {

VkStructureType sType;

const void* pNext;

VkPipelineShaderStageCreateFlags flags;

VkShaderStageFlagBits stage;

VkShaderModule module;

const char* pName;

const VkSpecializationInfo* pSpecializationInfo;

} VkPipelineShaderStageCreateInfo;

VkPipelineShaderStageCreateInfosTypeVK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFOpNext應置為nullptrflags域保留使用,在當前的Vulkan版本中應置為0

VkPipelineShaderStageCreateInfo型別資料在所有階段的管線建立中都使用。儘管圖形管線擁有多個階段(在第七章“圖形管線”中學到),計算管線只有一個階段,所以,stage應置為VK_SHADER_STAGE_COMPUTE_BIT

module是之前建立的著色器modulehandle,它包含了你想要建立的管線所需的程式碼。因為單一一個著色器module可以包含多個入口點和多個著色器,表示這個特別的管線的入口點通過VkPipelineShaderStageCreateInfopName域指定。這也是少數的幾個被Vulkan使用的人可斷的字串。

6.3.3 特殊常量

VkPipelineShaderStageCreateInfo最後一個域是一個指向VkSpecializationInfo型別資料的指標。這個結構包含特化一個著色器所需的資訊,“特化”是指包含常量的著色器構造的過程。

一個典型的Vulkan實現會管線程式碼的最終生成,直到呼叫vkCreateComputePipelines()函式。

這允許特化常量的值在著色器優化的最後一個pass中才被求值。常見使用和特化的程式使用的常量包含:

Producing special cases through branching: Including a condition on a Boolean specialization

constant will result in the final shader taking only one branch of the if statement. The nontaken

branch will probably be optimized away. If you have two similar versions of a shader that differ

in only a couple of places, this is a good way to merge them into one.

Special cases through switch statements: Likewise, using an integer specialization constant as

the tested variable in a switch statement will result in only one of the cases ever being taken in

that particular pipeline. Again, most Vulkan implementations will optimize out all the nevertaken

cases.

Unrolling loops: Using an integer specialization constant as the iteration count in a for loop

may result in the Vulkan implementation making better decisions about how to unroll the loop

or whether to unroll it at all. For example, if the loop counter ends up with a value of 1, then the

loop goes away and its body becomes straight-line code. A small loop iteration count might

result in the compiler unrolling the loop exactly that number of times. A larger iteration count

may result in the compiler unrolling the loop by a factor of the count and then looping over that

unrolled section a smaller number of times.

Constant folding: Subexpressions involving specialization constants can be folded just as with

any other constant. In particular, expressions involving multiple specialization constants may

fold into a single constant.

Operator simplification: Trivial operations such as adding zero or multiplying by one disappear,

multiplying by negative one can be absorbed into additions turning them to subtractions,

multiplying by small integers such as two can be turned into additions or even absorbed into

other operations, and so on.

GLSL中,特化常量被宣告為一個普通的常量,在一個layout限定符中被給予一個ID。在GLSL中特化常量可以是Boolean,整型,浮點型別或者諸如陣列、結構、向量、矩陣等複合型別。當被翻譯為SPIR-V時,這些都變成了OpSpecConstant tokenListing 6.5 展示了一個GLSL聲明瞭一些特化常量的例子,Listing 6.6展示了GLSL編譯器生成的SPIR-V

Listing 6.5

layout (constant_id = 0) const int numThings = 42;

layout (constant_id = 1) const float thingScale = 4.2f;

layout (constant_id = 2) const bool doThat = false;

Listing 6.6

...

OpDecorate %7 SpecId 0

OpDecorate %9 SpecId 1

OpDecorate %11 SpecId 2

%6 = OpTypeInt 32 1

%7 = OpSpecConstant %6 42

%8 = OpTypeFloat 32

%9 = OpSpecConstant %8 4.2

%10 = OpTypeBool

%11 = OpSpecConstantFalse %10

...

Listing 6.6 被編輯過,刪除了和特化常量的無關的程式碼。然而,你可以看到,, %7 OpSpecConstant指令宣告為一個特化常量,型別為% 6(一個32位的整型), 初始值為42。下一行,% 9被宣告為一個特化常量,型別為% 832位浮點型別),初始值為4.2.最後,%11 被宣告為一個Boolean型別(在這個SPIR-V中型別為%10),初始值為false。注意,Boolean型被 OpSpecConstantTrue OpSpecConstantFalse宣告,取決於他們的初始值為true還是false

注意,在GLSL著色器和生成的SPIR-V著色器中,特化常量都被賦予了初值。實際上,它們必須要被賦初值。這些常量也許和著色器中其他常量一樣的被使用。特殊情況下,它們可以被用來指定陣列大小--只允許編譯時常量能被使用。如果新的值沒有被包含在傳遞給vkCreateComputePipelines()VkSpecializationInfo型別的資料中,那麼這些預設值就被使用。然而,這些常量可以被建立管線時傳遞的新值覆蓋。VkSpecializationInfo定義為:

typedef struct VkSpecializationInfo {

uint32_t mapEntryCount;

const VkSpecializationMapEntry* pMapEntries;

size_t dataSize;

const void* pData;

} VkSpecializationInfo;

VkSpecializationInfo內部,mapEntryCount包含了需要被設定新值的特化常量的個數,這也是pMapEntries所指向的VkSpecializationMapEntry型別陣列元素的個數。每一個都表示一個特化常量。VkSpecializationMapEntry定義為:

typedef struct VkSpecializationMapEntry {

uint32_t constantID;

uint32_t offset;

size_t size;

} VkSpecializationMapEntry;

constantID是特化常量的ID,被用來匹配著色器module中使用常量ID。在GLSL中通過使用constant_id佈局限定符來設定值,在SPIR-V中使用SpecID描述符來設定值。Offsetsize域是包含特化常量的值的原生資料的偏移量和大小。VkSpecializationInfo型別資料的pData域指向了原生資料,資料大小通過dataSize.給定。Vulkan使用此資料來初始化特化常量。當管線被構造時,如果著色器中一個或者多個特化常量沒有在該資料中指定,它就會使用預設值。

當你使用完了管線並不再需要它時,可以銷燬它來釋放它關聯的資源。可呼叫vkDestroyPipeline()來銷燬管線物件,函式原型如下:

void vkDestroyPipeline (

VkDevice device,

VkPipeline pipeline,

const VkAllocationCallbacks* pAllocator);

擁有管線的裝置通過device指定,需要被銷燬的管線通過pipeline傳遞。如果在建立管線是使用了主機記憶體分配器,那麼需要使用pAllocator來傳遞一個匹配的分配器;否則,pAllocator應置為nullptr

在管線被銷燬後,它不應該再被使用了。這包含可能還沒有完成執行的命令緩衝區中的任何對它的引用。應用程式有責任保證任何提交的引用到該管線的命令緩衝區已經完成執行,和繫結該管線的任何命令緩衝區在管線被銷燬後不被提交。

6.3.4 加速管線的建立

建立管線可能是你的應用程式最昂貴的操作。儘管SPIR-V程式碼被vkCreateShaderModule()消耗,但是直到你呼叫vkCreateGraphicsPipelines() vkCreateComputePipelines()Vulkan才能看到所有的著色器階段和其他的域管線相關能影響到最終在裝置上執行的程式碼的階段。因為此原因,一個Vulkan實現也許會延遲涉及建立一個準備執行的管線物件的工作,直到儘可能最後一刻。這包括著色器編譯和程式碼生成,這些是典型的相當密集型的操作。

因為應用程式執行很多次也只是使用一遍又一遍的使用相同的管線,Vulkan提供了一個多次執行時快取管線建立結果的機制。這允許應用程式在啟動時就構建所有的管線來迅速啟動。管線快取通過下面函式建立的一個物件表示,

VkResult vkCreatePipelineCache (

VkDevice device,

const VkPipelineCacheCreateInfo* pCreateInfo,

const VkAllocationCallbacks * pAllocator,

VkPipelineCache* pPipelineCache);

用來建立管線快取的裝置由device指定。建立管線快取所需的剩下的引數通過一個VkPipelineCacheCreateInfo型別資料的指標傳遞。該型別定義如下:

typedef struct VkPipelineCacheCreateInfo {

VkStructureType sType;

const void * pNext;

VkPipelineCacheCreateFlags flags;

size_t initialDataSize;

const void * pInitialData;

} VkPipelineCacheCreateInfo;

VkPipelineCacheCreateInfosType域應置為VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFOpNext應置為nullptrflags域保留使用,應置為0。如果存在程式上一次執行產生的資料,資料的地址可以通過pInitialData傳遞。資料的大小通過initialDataSize傳遞。如果沒有資料,initialDataSize應置為0pInitialData應置為nullptr

當建立了快取之後,初始出具用來填充快取。如果有必要,Vulkan會複製一份資料。pInitialData指向的資料沒有被修改。當更多的管線建立時,描述它們的資料被新增到快取,隨著時間增長。可以呼叫vkGetPipelineCacheData()從快取中取出資料。其原型如下:

VkResult vkGetPipelineCacheData (

VkDevice device,

VkPipelineCache pipelineCache,

size_t* pDataSize,

void* pData);

擁有該管線快取的裝置通過device指定,需被獲取資料的管線快取應通過pipelineCache傳遞。如果pData不是nullptr,那麼他指向將接受快取資料的記憶體區域。這種情況下,pDataSize指向的變數的初始值是記憶體區域以位元組為單位的大小。這個變數就會被資料的真實大小所覆蓋。

如果pDatanullptr,那麼pDataSize所指向的變數初始值就被忽略,變數被覆蓋為用來儲存快取資料的記憶體的大小。可以呼叫vkGetPipelineCacheData()兩次來儲存所有的快取資料;第一次,設定pDatanullptrpDataSize指向一個接受快取資料大小的變數。然後,建立一個緩衝區來儲存生成的快取資料並再次呼叫vkGetPipelineCacheData(),這一次向pData傳遞一個指向這個地址區域的指標。Listing 6.7 舉例說明如何儲存管線資料到檔案。

Listing 6.7: Saving Pipeline Cache Data to a File

VkResult SaveCacheToFile(VkDevice device, VkPipelineCache cache,

const char* fileName)

{

size_t cacheDataSize;

VkResult result = VK_SUCCESS;

// Determine the size of the cache data.

result = vkGetPipelineCacheData(device,

cache,

&cacheDataSize,

nullptr);

if (result == VK_SUCCESS && cacheDataSize != 0)

{

FILE* pOutputFile;

void* pData;

// Allocate a temporary store for the cache data.

result = VK_ERROR_OUT_OF_HOST_MEMORY;

pData = malloc(cacheDataSize);

if (pData != nullptr)

{

// Retrieve the actual data from the cache.

result = vkGetPipelineCacheData(device,

cache,

&cacheDataSize,

pData);

if (result == VK_SUCCESS)

{

// Open the file and write the data to it.

pOutputFile = fopen(fileName, "wb");

if (pOutputFile != nullptr)

{

fwrite(pData, 1, cacheDataSize, pOutputFile);

fclose(pOutputFile);

}

free(pData);

}

}

}

return result;

}

一旦你接收了管線資料,你可以把它儲存到磁碟或者打包以供再次執行程式時使用。快取的內容沒有預定的結構,這是有實現決定的。然而,快取資料的前幾個word形成的頭部可用來驗證大塊的資料是否為有效的快取和哪個裝置建立了它。

快取頭部的佈局可以使用下面的C結構體來表示:

// This structure does not exist in official headers but is included here

// for illustration.

typedef struct VkPipelineCacheHeader {

uint32_t length;

uint32_t version;

uint32_t vendorID;

uint32_t deviceID;

uint8_t uuid[16];

} VkPipelineCacheHeader;

儘管結構體的成員都是uint32_t 型別變數,快取的資料並不必要是uint32_t 型別的。快取總是以小端排序的,不管主機的位元組排序是那種。這意味著如果你想在大端排序的主機上解釋這個結構,你需要翻轉uint32_t 型別域的排序。

lenth域是頭部結構的大小,以位元組為單位。在當前的技術規範版本中,這個長度應該為32version域是結構的版本。已定義的版本只有1vendorIDdeviceID域應該和通過呼叫vkGetPhysicalDeviceProperties()返回的VkPhysicalDeviceProperties結構的vendorID

deviceID域匹配。uuid域是一個不透明的字串型別,用來唯一標識這個GPU裝置。如果vendorID, deviceID, uuid域有一個不匹配Vulkan 驅動期望的值,那麼它會拒絕快取資料並把它置為空。一個驅動也許會內建摘要加密或其他資料到快取裡,來保證無效的快取資料不會載入到裝置。

如果你有兩個快取物件並希望那個融合它們,可呼叫vkMergePipelineCaches()來完成,其原型如下:

VkResult vkMergePipelineCaches (

VkDevice device,

VkPipelineCache dstCache,

uint32_t srcCacheCount,

const VkPipelineCache* pSrcCaches);

device引數是擁有被融合快取資料的裝置的handledstCache是目標快取的handle,它最終會變成源快取陣列中每一條的合體。將被融合的快取的個數通過srcCacheCount指定,pSrcCaches是一個指向VkPipelineCache型別陣列的指標,陣列中每一個handle是需要被融合的快取。

vkMergePipelineCaches()執行後,dstCache將包含pSrcCaches所指定的源快取陣列中每一條快取。然後才能呼叫vkGetPipelineCacheData()來獲取一個單一的、大型的快取資料結構,它可表示其他所有的快取。

這一點特別有用,例如,在多執行緒建立管線時。儘管對管線快取的訪問是執行緒安全 的,Vulkan實現也許在內部採用鎖來防止對多個快取的同時寫入。如果你建立多個管線快取--每執行緒一個--並在管線的建立初始時使用它們。稍後,當管線都被建立完,你可以合併多個管線,以把它們的資料儲存在一個大型的資源裡。

當你使用完管線並不再需要很長,就需要銷燬它,因為它可能會很大。可呼叫vkDestroyPipelineCache()來銷燬管線快取物件,其原型是:

void vkDestroyPipelineCache (

VkDevice device,

VkPipelineCache pipelineCache,

const VkAllocationCallbacks* pAllocator);

device是擁有管線快取的裝置handlepipelineCache是需要被銷燬的管線快取物件。在管線快取被銷燬後,它不應該再把使用了,儘管用快取建立的管線依然有效。通過呼叫vkGetPipelineCacheData()從快取中獲取到的任何資料也還是有效的,可以用來構建新的快取。

6.3.5  繫結管線

在你可使用管線之前,它必須保額繫結到執行互指或分發命令的命令緩衝區。當一個命令被執行是,當前的管線(即其中所有的著色器程式)被用來處理這個命令。可呼叫vkCmdBindPipeline()來把管線繫結到一個命令緩衝區,其原型如下:

void vkCmdBindPipeline (

VkCommandBuffer commandBuffer,

VkPipelineBindPoint pipelineBindPoint,

VkPipeline pipeline);

正在繫結的命令緩衝區通過commandBuffer指定,被繫結的管線通過pipeline指定。在每一個命令緩衝區上有兩個管線繫結點:圖形和計算繫結點。計算繫結點是計算管線應該繫結的點。圖形管線在下一章講解,它應被繫結到圖形管線繫結點。

把管線繫結到計算繫結點,需設定pipelineBindPointVK_PIPELINE_BIND_POINT_COMPUTE;把管線繫結到圖形管線繫結點,應設定pipelineBindPointVK_PIPELINE_BIND_POINT_GRAPHICS

當前與計算和圖形管線的繫結是命令緩衝區的一個狀態。當新的命令緩衝區開始時,這個狀態是未定義的。因此,你必須在管線被用來工作前把管線繫結到一個繫結點。