Part 1 - 关于Vulkan
转载自最早发布在formu.ziyuesinicization.site的文章
2018年3月7日,科纳斯组织发布了Vulkan 1.1,随后,大量的游戏和游戏引擎几乎都开始支持Vulkan,它以高性能著称,与Direct3D 12类似。
Vulkan对GPU进行了更直接的控制,是一个极底层的图形API。由于它具有极高的自由度,这也导致了它的代码及其繁琐,光是渲染一个在着色器硬编码的三角形就要写600多行代码。
下面我们来看看怎么回事:
Vulkan居有和OpenGL一样的扩展机制,Vulkan与窗口的联系,需要用到表面扩展,例如:用于Windows系统窗口的扩展为:VK_KHR_surface_win32。
要想使用Vulkan API首先要创建一个实例(Instance),创建实例前,需要获取扩展,同时为了方便后续的调试,我们还需要开启验证层(validation)。
unsigned int extensionCount;//扩展数量
const char* const* extensionNames = SDL_Vulkan_GetInstanceExtensions(&extensionCount);
//获取扩展
//开启验证层
const char* validation = "VK_LAYER_KHRONOS_validation";
const char* const* validationNames = &validation;
在创建实例前,我需要填写两个结构体,一个是应用程序信息结构体(VkApplicationInfo),一个是实例创建信息结构体(VkInstanceCreateInfo)。
typedef struct VkApplicationInfo {
VkStructureType sType; // 结构体的类型,由于我们使用的是VkApplicationInfo,所以这里必须为 VK_STRUCTURE_TYPE_APPLICATION_INFO
const void* pNext; // 用于扩展链,不用管,设为 NULL或nullptr
const char* pApplicationName; // 应用程序名称,UTF-8字符
uint32_t applicationVersion; // 应用程序的版本号
const char* pEngineName; // 使用的引擎名称,可以设为NULL或nullptr
uint32_t engineVersion; // 引擎版本号
uint32_t apiVersion; // 请求使用的 Vulkan API 版本
} VkApplicationInfo;
typedef struct VkInstanceCreateInfo {
VkStructureType sType; // 同上,必须为 VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO
const void* pNext; // 同上
VkInstanceCreateFlags flags; // 保留标志位不用管设为0
const VkApplicationInfo* pApplicationInfo; // 指向 VkApplicationInfo 的指针
uint32_t enabledLayerCount; // 启用的验证层数量
const char* const* ppEnabledLayerNames; // 启用的验证层
uint32_t enabledExtensionCount; // 启用的扩展数量
const char* const* ppEnabledExtensionNames; // 启用的扩展
} VkInstanceCreateInfo;
示例:
VkApplicationInfo appInfo = {};
appInfo.apiVersion = VK_API_VERSION_1_3;
appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.pApplicationName = "";
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
VkInstanceCreateInfo instanceCreateInfo = {};
instanceCreateInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
instanceCreateInfo.pApplicationInfo = &appInfo;
instanceCreateInfo.enabledExtensionCount = extensionCount;
instanceCreateInfo.enabledLayerCount = 1;
instanceCreateInfo.ppEnabledExtensionNames = extensionNames;
instanceCreateInfo.ppEnabledLayerNames = validationNames;
采用了C++的结构体初始化时,设为NULL, nullptr, 0的地方会自动填上,不需要再访问填写。
之后就可以创建实例了:
VkInstance instance;
vkCreateInstance(&instanceCreateInfo, nullptr, &instance);
vkCreateInstance的第三项参数为VkAllocationCallbacks,他是用于内存分配的回调函数指针的结构,目前暂时用不到,可以设为nullptr。
创建好实例之后,我就可以从窗口创建表面了:
VkSurfaceKHR surface = VK_NULL_HANDLE;
SDL_Vulkan_CreateSurface(window, instance, nullptr, &surface);
接下来就是要选择一个GPU来创建逻辑设备(VkDevice)和队列族(VkQueue)。
逻辑设备是从GPU创建的,他是连接应用程序与GPU的重要工具,而GPU的功能有很多,除了常见的绘图,他还可以进行通用并行计算,数据传输等,为了能让GPU进行不同类型的工作,我们需要用队列族进行管理。
我们需要先获取GPU的一些信息,考虑到一些土豪老哥可能会给设备插多块显卡,所以我们需要找出你设备上所有的显卡:
unsigned int deviceCount;
vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr);
const unsigned int devicesNum = deviceCount;
VkPhysicalDevice devices[devicesNum];
vkEnumeratePhysicalDevices(instance, &deviceCount, devices);
这里有一个十分有趣的技巧,我们第一次调用vkEnumeratePhysicalDevices时,把最后一项参数VkPhysicalDevice*设为了nullptr,这一步实际上是为了获取显卡的数量,第二次再调用时才是真正获取了显卡,这个技巧后面还要用。
接下来,我们需要筛选出最适合的显卡,现在我们暂时只关心绘图的功能,我们需要再次获取每块显卡的队列族,并找到能绘图的那一块显卡,并获取他的索引,稍微接触过计算机图形学的都知道,一块能绘图的显卡,可以同时绘制图形和呈现图像,这两个功能通常应该是在一个队列族里的,但是不排除绘制图形和呈现图像这两个是分开的情况,所以我们需要获取绘制图形和呈现图像的队列索引。
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;//那块最佳的显卡
unsigned int graphicsIndex = -1; //绘制图形索引
unsigned int presentIndex = -1;//呈现图想索引
for (unsigned int i = 0; i < deviceCount; i++) {
//获取GPU的各种信息
VkPhysicalDeviceProperties deviceProperties;
vkGetPhysicalDeviceProperties(devices[i], &deviceProperties);
std::cout << "GPU " << i << ": " << deviceProperties.deviceName << std::endl;
//获取GPU的队列族,利用刚刚的技巧
unsigned int queueFamilyCount;
vkGetPhysicalDeviceQueueFamilyProperties(devices[i], &queueFamilyCount, nullptr);
const unsigned int queueFamiliesNum = queueFamilyCount;
VkQueueFamilyProperties queueFamilies[queueFamiliesNum];
vkGetPhysicalDeviceQueueFamilyProperties(devices[i], &queueFamilyCount, queueFamilies);
for (unsigned int j = 0; j < queueFamilyCount; j++) {
//检查GPU是否可以绘制图形
if (queueFamilies[j].queueFlags & VK_QUEUE_GRAPHICS_BIT) {
graphicsIndex = j;
}
//检查GPU是否可以呈现图像
VkBool32 supported = false;
vkGetPhysicalDeviceSurfaceSupportKHR(devices[i], j, surface, &supported);
if (supported) {
presentIndex = j;
}
//都可以时,选择它作为最佳显卡
if ((graphicsIndex != -1) && (presentIndex != -1)) {
physicalDevice = devices[i];
std::cout << "Selected GPU " << i << ": " << deviceProperties.deviceName << std::endl;
}
}
//如果选择到了最佳显卡就跳出循环
if (physicalDevice != VK_NULL_HANDLE) {
break;
}
}
//没有合适的显卡
if (physicalDevice == VK_NULL_HANDLE) {
std::cout << "Filed to find a suitable GPU" << std::endl;
}
}
选择到了最佳显卡之后,就可以开始创建逻辑设备了,但是创建逻辑设备我们还需要填写两个结构体:
typedef struct VkDeviceQueueCreateInfo {
VkStructureType sType; // 必须为 VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO
const void* pNext; // nullptr
VkDeviceQueueCreateFlags flags; // 0
uint32_t queueFamilyIndex; // 队列族索索引
uint32_t queueCount; // 要创建的队列数量
const float* pQueuePriorities; // 队列优先级数组(范围 0.0 ~ 1.0)
} VkDeviceQueueCreateInfo;
typedef struct VkDeviceCreateInfo {
VkStructureType sType; // 必须为 VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO
const void* pNext; // nullptr
VkDeviceCreateFlags flags; // 0
uint32_t queueCreateInfoCount; // 队列创建信息数量
const VkDeviceQueueCreateInfo* pQueueCreateInfos; // 队列创建信息数组
uint32_t enabledLayerCount; // 启用的验证层数量, 已弃用,建议设为 0
const char* const* ppEnabledLayerNames; // 验证层名称数组,已弃用, 不用管
uint32_t enabledExtensionCount; // 启用的设备扩展数量
const char* const* ppEnabledExtensionNames; // 设备扩展名称数组
const VkPhysicalDeviceFeatures* pEnabledFeatures; // 启用的设备特性
} VkDeviceCreateInfo;
刚刚我们考虑到绘制图形和呈现图像可能是两个队列族,所以我们需要创建两个设备队列创建信息(VkDeviceQueueCreateInfo),另外,我们需要启用交换链扩展(VK_KHR_SWAPCHAIN_EXTENSION_NAME), 至于设备特性我们暂时不使用设为NULL。实例如下:
VkDeviceQueueCreateInfo dqCreateInfo[2];//0: Graphics 1: Present
float queuePriority = 1.0f;//优先级默认为1
for (unsigned int i = 0; i < 2; i++) {
dqCreateInfo[i].sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
dqCreateInfo[i].pQueuePriorities = &queuePriority;
dqCreateInfo[i].queueCount = 1;
}
dqCreateInfo[0].queueFamilyIndex = graphicsIndex;
dqCreateInfo[1].queueFamilyIndex = presentIndex;
const char* deviceExtensions[] = {
VK_KHR_SWAPCHAIN_EXTENSION_NAME
};
VkDeviceCreateInfo dCreateInfo = {};
dCreateInfo.enabledExtensionCount = sizeof(deviceExtensions) / sizeof(deviceExtensions[0]);
dCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
dCreateInfo.enabledLayerCount = 0;
dCreateInfo.ppEnabledExtensionNames = deviceExtensions;
dCreateInfo.ppEnabledLayerNames = nullptr;
dCreateInfo.pEnabledFeatures = NULL;
//判断是否为同一队列族
if (graphicsIndex == presentIndex) {
dCreateInfo.pQueueCreateInfos = &dqCreateInfo[0];
dCreateInfo.queueCreateInfoCount = 1;
}
else {
dCreateInfo.pQueueCreateInfos = dqCreateInfo;
dCreateInfo.queueCreateInfoCount = 2;
}
现在开始创建逻辑设备和获取绘制图形队列和呈现图像队列:
vkCreateDevice(physicalDevice, &dCreateInfo, nullptr, &device);
VkQueue graphicsQueue;
VkQueue presentQueue;
vkGetDeviceQueue(device, graphicsIndex, 0, &graphicsQueue);
vkGetDeviceQueue(device, presentIndex, 0, &presentQueue);
接下来就是创建交换链(Swap Chain)以及从交换链获取图像视图。
交换链,先前用OpenGL或Direct3D的应该不陌生,不知道的可以去google一下,图像视图代表了最原始的内存数据,后面的绘图操作都是在图像视图上进行的。
我们先看一下交换链创建信息结构体:
typedef struct VkSwapchainCreateInfoKHR {
VkStructureType sType; // 必须为 VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR
const void* pNext; // nullptr
VkSwapchainCreateFlagsKHR flags; // 0
VkSurfaceKHR surface; // 窗口表面
uint32_t minImageCount; // 最小缓冲区数量, 2为二重缓冲,3为三重缓冲
VkFormat imageFormat; // 像素格式,通常为VK_FORMAT_B8G8R8A8_SRGB就够用了
VkColorSpaceKHR imageColorSpace; // 色彩空间,通常为VK_COLOR_SPACE_SRGB_NONLINEAR_KHR
VkExtent2D imageExtent; // 图像尺寸,通常为窗口大小
uint32_t imageArrayLayers; // 数组层数,通常为 1,如果是VR或3维图像则为2
VkImageUsageFlags imageUsage; // 图像用途,设为VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT即可
//这三个后面再提
VkSharingMode imageSharingMode;
uint32_t queueFamilyIndexCount;
const uint32_t* pQueueFamilyIndices;
VkSurfaceTransformFlagBitsKHR preTransform; // 图像变换(旋转/翻转),通常为VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR:不变换
VkCompositeAlphaFlagBitsKHR compositeAlpha; // 透明混合方式,通常为VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR:完全不透明
VkPresentModeKHR presentMode; // 呈现模式,共有四种模式:
//VK_PRESENT_MODE_FIFO_KHR 等待垂直同步,队列先进先出
//VK_PRESENT_MODE_MAILBOX_KHR 保留最新图像,替换队列中的旧图像
//VK_PRESENT_MODE_IMMEDIATE_KHR 立即显示,不等待 V-Sync
//VK_PRESENT_MODE_FIFO_RELAXED_KHR FIFO 变体,错过 V-Sync 则立即显示
//一般为VK_PRESENT_MODE_MAILBOX_KHR即可
VkBool32 clipped; // 是否裁剪被遮挡像素
VkSwapchainKHR oldSwapchain; // 旧的交换链句柄(重建时使用)
} VkSwapchainCreateInfoKHR;
一个简单示例:
VkSwapchainCreateInfoKHR spCreateInfo;
spCreateInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
spCreateInfo.minImageCount = 2;
spCreateInfo.clipped = VK_TRUE;
spCreateInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
spCreateInfo.imageArrayLayers = 1;
spCreateInfo.imageColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR;
spCreateInfo.imageExtent = {window_width, window_height};
spCreateInfo.imageFormat = VK_FORMAT_B8G8R8A8_SRGB;
//如果有多个队列则为,则开启共享
uint32_t queueFamilyIndices[2] = {graphicsIndex, presentIndex};
if (graphicsIndex != presentIndex) {
spCreateInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
spCreateInfo.queueFamilyIndexCount = 2; //队列数量
spCreateInfo.pQueueFamilyIndices = queueFamilyIndices;//队列索引
}
else {
spCreateInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
}
spCreateInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
spCreateInfo.oldSwapchain = VK_NULL_HANDLE;
spCreateInfo.presentMode = VK_PRESENT_MODE_MAILBOX_KHR;
spCreateInfo.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
spCreateInfo.surface = surface;
接下来,我们就可以创建交换链了:
VkSwapchainKHR swapChain;
vkCreateSwapchainKHR(device, &spCreateInfo, nullptr, &swapChain);
要注意的是,我们手动填写的参数,不一定所有显卡都能用,我可以通过vkGetPhysicalDeviceSurfaceCapabilitiesKHR,来检查那些参数是否有用:
VkSurfaceCapabilitiesKHR capabilities;
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice, surface, &capabilities);
//Format
unsigned int formatCount;
vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice, surface, &formatCount, nullptr);
const unsigned int formatsNum = formatCount;
VkSurfaceFormatKHR formats[formatsNum];
vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice, surface, &formatCount, formats);
VkSurfaceFormatKHR surfaceFormat = formats[0];
for (unsigned int i = 0; i < formatCount; i++) {
if (formats[i].format == VK_FORMAT_R8G8B8A8_SRGB &&
formats[i].colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
surfaceFormat = formats[i];
}
}
//Present Mode
unsigned int presentModeCount;
vkGetPhysicalDeviceSurfacePresentModesKHR(physicalDevice, surface, &presentModeCount, nullptr);
const unsigned int presentModesNum = presentModeCount;
VkPresentModeKHR presentModes[presentModesNum];
vkGetPhysicalDeviceSurfacePresentModesKHR(physicalDevice, surface, &presentModeCount, presentModes);
VkPresentModeKHR surfacePresentMode = VK_PRESENT_MODE_IMMEDIATE_KHR;
for (unsigned int i = 0;i < presentModeCount; i++) {
if (presentModes[i] == VK_PRESENT_MODE_MAILBOX_KHR) {
surfacePresentMode = presentModes[i];
}
}
//Extent
VkExtent2D spExtent = capabilities.currentExtent;
spExtent.width = window_width;
spExtent.height = window_height;
unsigned int imageCount = capabilities.minImageCount + 1;
if (capabilities.maxImageCount > 0 && imageCount > capabilities.maxImageCount) {
imageCount = capabilities.maxImageCount;
}
接下来,我们需要从交换链获取图像,以此来创建图像视图:
unsigned int imageCount;
vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr);
const unsigned int imagesNum = imageCount;
VkImage spImages[imagesNum];
vkGetSwapchainImagesKHR(device, swapChain, &imageCount, spImages);
在创建图像视图前,我们依旧需要填写信息结构体:
typedef struct VkImageViewCreateInfo {
VkStructureType sType; // 必须为 VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO
const void* pNext; // nullptr
VkImageViewCreateFlags flags; // 0
VkImage image; // 目标图像句柄
VkImageViewType viewType; // 视图类型,默认为2D视图,填VK_IMAGE_VIEW_TYPE_2D即可
VkFormat format; // 图像格式,与上面创建交换链的格式一样即可
VkComponentMapping components; // 分量映射,RGBA 通道重排
VkImageSubresourceRange subresourceRange; // 访问的图像子集,层/级别范围
} VkImageViewCreateInfo;
其中有几个需要单独拿出来:
typedef struct VkComponentMapping {
VkComponentSwizzle r; // 红色分量来源
VkComponentSwizzle g; // 绿色分量来源
VkComponentSwizzle b; // 蓝色分量来源
VkComponentSwizzle a; // Alpha 分量来源
//全填VK_COMPONENT_SWIZZLE_IDENTITY,即原始通道即可
} VkComponentMapping;
typedef struct VkImageSubresourceRange {
VkImageAspectFlags aspectMask; // 访问的方面(颜色/深度/模板)
uint32_t baseMipLevel; // 起始 Mipmap 级别
uint32_t levelCount; // Mipmap 级别数量
uint32_t baseArrayLayer; // 起始数组层
uint32_t layerCount; // 数组层数量
} VkImageSubresourceRange;
我们为交换链的每个图像都创建一个图像视图即可:
VkImageView spImagesView[imagesNum];
for (unsigned int i = 0;i < imageCount; i++) {
VkImageViewCreateInfo ivCreateInfo;
ivCreateInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
ivCreateInfo.format = surfaceFormat.format;
ivCreateInfo.image = spImages[i];
ivCreateInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
ivCreateInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
ivCreateInfo.subresourceRange.baseMipLevel = 0;
ivCreateInfo.subresourceRange.levelCount = 1;
ivCreateInfo.subresourceRange.baseArrayLayer = 0;
ivCreateInfo.subresourceRange.layerCount = 1;
ivCreateInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;
ivCreateInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;
ivCreateInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;
ivCreateInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;
vkCreateImageView(device, &ivCreateInfo, nullptr, &spImagesView[i]);
}
接着,我们还需要命令缓冲区(Command Buffer)和命令池(Command Pool)
我们进行清屏,绘制等操作时,需要先把对应的指令放在命令池,由命令池管理这些命令,随后通过命令缓冲区录制,提交,执行这些命令给GPU。
同样,创建命令池和命令缓冲区,需要先填写信息结构体:
typedef struct VkCommandPoolCreateInfo {
VkStructureType sType; // 必须为 VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO
const void* pNext; // nullptr
VkCommandPoolCreateFlags flags; // 这里有三个标志可以设置:
//VK_COMMAND_POOL_CREATE_TRANSIENT_BIT 池中的命令缓冲区生命周期短、频繁重置 每帧重新录制的缓冲区
//VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT允许单独重置池中的单个缓冲区 需要灵活管理单个缓冲区,而非重置整个池
//VK_COMMAND_POOL_CREATE_PROTECTED_BIT 用于受保护内存的命令缓冲区
一般填VK_COMMAND_POOL_CREATE_TRANSIENT_BIT即可
uint32_t queueFamilyIndex; // 绑定的队列族索引
} VkCommandPoolCreateInfo;
typedef struct VkCommandBufferAllocateInfo {
VkStructureType sType; // 必须为 VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO
const void* pNext; // nullptr
VkCommandPool commandPool; // 所属的命令池
VkCommandBufferLevel level; // 缓冲区级别(Primary/Secondary)
uint32_t commandBufferCount; // 分配数量
} VkCommandBufferAllocateInfo;
以下简单的示例:
VkCommandPool commandPool;
VkCommandPoolCreateInfo cpCreateInfo;
cpCreateInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
cpCreateInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
cpCreateInfo.queueFamilyIndex = graphicsIndex;
vkCreateCommandPool(device, &cpCreateInfo, nullptr, &commandPool);
VkCommandBuffer commandBuffer;
VkCommandBufferAllocateInfo cbaInfo;
cbaInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
cbaInfo.commandBufferCount = 1;
cbaInfo.commandPool = commandPool;
cbaInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
vkAllocateCommandBuffers(device, &cbaInfo, &commandBuffer);
接下来我们需要信号量(VkSemaphore)和栅栏(VkFence)
栅栏是 CPU 和 GPU 之间的同步机制。它像一个"门闩":
GPU 执行完提交的命令缓冲区后,会触发(Signal) Fence。
CPU 可以通过 vkWaitForFences 等待这个触发信号,实现 CPU 阻塞。
核心用途:让 CPU 知道 GPU 什么时候完成了特定任务,以便安全地回收资源或读取结果。
信号量是 GPU 内部队列之间和队列内部的同步机制。它像一个"接力棒":
一个队列完成工作后,触发(Signal) Semaphore。
另一个队列在开始工作前,等待(Wait) 这个 Semaphore。
核心用途:协调 GPU 内部不同工作之间的执行顺序,避免数据竞争。
我们需要两个信号量,一个是图像可用信号量,一个是渲染结束信号量,以及一个飞行栅栏。
在创建信号量和栅栏之前,又双叒叕需要填写信息结构:
typedef struct VkSemaphoreCreateInfo {
VkStructureType sType; // 必须为 VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO
const void* pNext; //nullptr
VkSemaphoreCreateFlags flags; // 0
} VkSemaphoreCreateInfo;
typedef struct VkFenceCreateInfo {
VkStructureType sType; // 必须为 VK_STRUCTURE_TYPE_FENCE_CREATE_INFO
const void* pNext; // nullptr
VkFenceCreateFlags flags; // 创建标志,有两种初始状态
//0:Fence 刚创建时是未触发的,需要提交工作并关联 Fence 才会触发。适合"先提交,后等待"的模式。
//VK_FENCE_CREATE_SIGNALED_BIT:Fence 创建时就已经触发,使得首次等待立即返回。适合"多帧并行"场景,避免第一帧时阻塞。
//我们选择VK_FENCE_CREATE_SIGNALED_BIT即可
} VkFenceCreateInfo;
创建即可:
VkSemaphore imageSem, renderSem;
VkFence flightFen;
VkSemaphoreCreateInfo semCreateInfo;
semCreateInfo.pNext = nullptr;
semCreateInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
semCreateInfo.flags = 0;
VkFenceCreateInfo fenCreateInfo;
fenCreateInfo.pNext = nullptr;
fenCreateInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
fenCreateInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
vkCreateSemaphore(device, &semCreateInfo, nullptr, &imageSem);
vkCreateSemaphore(device, &semCreateInfo, nullptr, &renderSem);
vkCreateFence(device, &fenCreateInfo, nullptr, &flightFen);
接着,我们需要创建渲染管线,Vulkan的渲染管线大致为:
Vertex/Index buffer 顶点/索引输入
                           |
Input assembler 对输入的顶点按照索引装配成多边形
                           |
Vertex Shader 顶点着色器
                           |
Tessellation 曲面细分
                           |
Geometry shader 几何着色器
                           |
Rasterization 光栅化,转为像素
                           |
Fragment shader 片段着色器
                           |
Color blending 颜色混合
                           |
Framebuffer 输出到帧缓冲
创建管线时,我们只关注Input assembler, Vertex Shader, Rasterization, Fragment Shader, Color blending以及Framebuffer。
不过在此之前,我们需要顶点着色器与片段着色器。与传统的OpenGL和Direct3D的着色器不同,Vulkan只支持SPIR-V(Standard Portable Intermediate Representation)的中间二进制格式的编译,所以,你需要先用GLSL或HLSL语言编写着色器,然后用glslc编译为SPIR-V,最后才能给Vulkan使用。这里我用HLSL语言编写着色器:
vertex.hlsl:
struct VSOutput {
float4 Pos : SV_Position;
float3 Color : COLOR0;
};
VSOutput main(uint vertexID : SV_VertexID)
{
VSOutput output;
float2 positions[3] = {
float2( 0.0, -0.5),
float2( 0.5, 0.5),
float2(-0.5, 0.5)
};
float3 colors[3] = {
float3(1.0, 0.0, 0.0),
float3(0.0, 1.0, 0.0),
float3(0.0, 0.0, 1.0)
};
output.Pos = float4(positions[vertexID], 0.0, 1.0);
output.Color = colors[vertexID];
return output;
}
pixel.hlsl:
struct VSOutput {
float4 Pos : SV_Position;
float3 Color : COLOR0;
};
float4 main(VSOutput input) : SV_Target
{
return float4(input.Color, 1.0);
}
接着进行编译:
glslc -x hlsl -fshader-stage=vertex -fentry-point=main vertex.hlsl -o vert.spv
glslc -x hlsl -fshader-stage=fragment -fentry-point=main pixel.hlsl -o frag.spv
注意,在用HLSL编写着色器时,尽管我们还是习惯称作为Pixel Shader(像素着色器),但是在Vulkan中是不存在的,你依然要用fragment的格式去编译它.
接下来,我们需要创建着色器模块(VkShaderModule),还是需要填写信息结构:
typedef struct VkShaderModuleCreateInfo {
VkStructureType sType; // 必须为 VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO
const void* pNext; // nullptr
VkShaderModuleCreateFlags flags; // 0
size_t codeSize; // SPIR-V 字节码大小(字节)
const uint32_t* pCode; // SPIR-V 字节码数据指针
} VkShaderModuleCreateInfo;
我写一个CreateShaderModule的函数:
VkShaderModule CreateShaderModule(VkDevice device, const char* code, long size) {
VkShaderModuleCreateInfo smCreateInfo = {};
smCreateInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
smCreateInfo.codeSize = size;
smCreateInfo.pCode = reinterpret_cast(code);
VkShaderModule shaderMod;
vkCreateShaderModule(device, &smCreateInfo, nullptr, &shaderMod);
return shaderMod;
}
然后创建顶点和片段着色器模块:
long vertexSize, fragmentSize;
const char* vertexCode = ReadFile("shaders/vert.spv", &vertexSize);
const char* fragmentCode = ReadFile("shaders/frag.spv", &fragmentSize);
VkShaderModule vertexMod = CreateShaderModule(device, vertexCode, vertexSize);
VkShaderModule fragmentMod = CreateShaderModule(device, fragmentCode, fragmentSize);
我们需要用VkPipelineShaderStageCreateInfo来创建着色器
typedef struct VkPipelineShaderStageCreateInfo {
VkStructureType sType; // 必须为 VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO
const void* pNext; // nullptr
VkPipelineShaderStageCreateFlags flags; // 0
VkShaderStageFlagBits stage; // 着色器阶段,主要有:
//VK_SHADER_STAGE_VERTEX_BIT 顶点着色器
//VK_SHADER_STAGE_FRAGMENT_BIT 片元着色器
//VK_SHADER_STAGE_GEOMETRY_BIT 几何着色器
//VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT 曲面细分控制着色器
//VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT 曲面细分评估着色器
//VK_SHADER_STAGE_COMPUTE_BIT 计算着色器
//VK_SHADER_STAGE_RAYGEN_BIT_KHR 光线生成着色器 (扩展)
//VK_SHADER_STAGE_ANY_HIT_BIT_KHR Any-Hit 着色器 (扩展)
//VK_SHADER_STAGE_CLOSEST_HIT_BIT_KHR Closest-Hit 着色器 (扩展)
//VK_SHADER_STAGE_MISS_BIT_KHR Miss 着色器 (扩展)
//VK_SHADER_STAGE_INTERSECTION_BIT_KHR 相交着色器 (扩展)
//VK_SHADER_STAGE_CALLABLE_BIT_KHR 可调用着色器 (扩展)
VkShaderModule module; // 着色器模块
const char* pName; // 入口点函数名称
const VkSpecializationInfo* pSpecializationInfo; // 特化常量信息(用于优化)
} VkPipelineShaderStageCreateInfo;
我们这样进行创建:
long vertexSize, fragmentSize;
const char* vertexCode = ReadFile("shaders/vert.spv", &vertexSize);
const char* fragmentCode = ReadFile("shaders/frag.spv", &fragmentSize);
VkShaderModule vertexMod = CreateShader(device, vertexCode, vertexSize);
VkShaderModule fragmentMod = CreateShader(device, fragmentCode, fragmentSize);
VkPipelineShaderStageCreateInfo vssInfo = {};
vssInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
vssInfo.stage = VK_SHADER_STAGE_VERTEX_BIT;
vssInfo.module = vertexMod;
vssInfo.pName = "main";
VkPipelineShaderStageCreateInfo fssInfo = {};
fssInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
fssInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
fssInfo.module = fragmentMod;
fssInfo.pName = "main";
VkPipelineShaderStageCreateInfo shaderStages[2] = {vssInfo, fssInfo};
现在,我们开始创建管线,创建管线需要填写大量结构体:
typedef struct VkPipelineVertexInputStateCreateInfo {
VkStructureType sType; // 必须为 VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO
const void* pNext; // nullptr
VkPipelineVertexInputStateCreateFlags flags; // 0
uint32_t vertexBindingDescriptionCount; // 顶点绑定描述数量
const VkVertexInputBindingDescription* pVertexBindingDescriptions; // 绑定描述数组
uint32_t vertexAttributeDescriptionCount; // 顶点属性描述数量
const VkVertexInputAttributeDescription* pVertexAttributeDescriptions; // 属性描述数组
} VkPipelineVertexInputStateCreateInfo;
typedef struct VkPipelineInputAssemblyStateCreateInfo {
VkStructureType sType; // 必须为 VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO
const void* pNext; // nullptr
VkPipelineInputAssemblyStateCreateFlags flags; // 0
VkPrimitiveTopology topology; // 图元拓扑,拓扑类型主要有:
//VK_PRIMITIVE_TOPOLOGY_POINT_LIST 点列表(每个顶点独立)
//VK_PRIMITIVE_TOPOLOGY_LINE_LIST 线条列表(每 2 个顶点一条线)
//VK_PRIMITIVE_TOPOLOGY_LINE_STRIP 线条带(连续线条) ≥2 连续路径、折线
//VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST 三角形列表(每 3 个顶点一个三角形)
//VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP 三角形带(连续三角形)
//VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN 三角形扇
//VK_PRIMITIVE_TOPOLOGY_LINE_LIST_WITH_ADJACENCY 线条列表 + 邻接信息 4 的倍数
//VK_PRIMITIVE_TOPOLOGY_LINE_STRIP_WITH_ADJACENCY 线条带 + 邻接信息
//VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY 三角形列表 + 邻接信息
//VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP_WITH_ADJACENCY 三角形带 + 邻接信息
//VK_PRIMITIVE_TOPOLOGY_PATCH_LIST 面片列表(用于细分着色器)
VkBool32 primitiveRestartEnable; // 是否启用图元重启
} VkPipelineInputAssemblyStateCreateInfo;
typedef struct VkPipelineDynamicStateCreateInfo {
VkStructureType sType; // 必须为 VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO
const void* pNext; // nullptr
VkPipelineDynamicStateCreateFlags flags; // 0
uint32_t dynamicStateCount; // 动态状态数量
const VkDynamicState* pDynamicStates; // 动态状态枚举数组
} VkPipelineDynamicStateCreateInfo;
Vulkan支持的动态状态枚举主要有:
动态状态枚举 对应的设置函数 描述
VK_DYNAMIC_STATE_VIEWPORT vkCmdSetViewport 视口(Viewport)
VK_DYNAMIC_STATE_SCISSOR vkCmdSetScissor 裁剪区域(Scissor)
VK_DYNAMIC_STATE_LINE_WIDTH vkCmdSetLineWidth 线宽
VK_DYNAMIC_STATE_DEPTH_BIAS vkCmdSetDepthBias 深度偏移
VK_DYNAMIC_STATE_BLEND_CONSTANTS vkCmdSetBlendConstants 混合常量
VK_DYNAMIC_STATE_DEPTH_BOUNDS vkCmdSetDepthBounds 深度边界
VK_DYNAMIC_STATE_STENCIL_COMPARE_MASK vkCmdSetStencilCompareMask 模板比较掩码
VK_DYNAMIC_STATE_STENCIL_WRITE_MASK vkCmdSetStencilWriteMask 模板写入掩码
VK_DYNAMIC_STATE_STENCIL_REFERENCE vkCmdSetStencilReference 模板参考值
VK_DYNAMIC_STATE_CULL_MODE vkCmdSetCullMode 剔除模式(Vulkan 1.3)
VK_DYNAMIC_STATE_FRONT_FACE vkCmdSetFrontFace 正面朝向(Vulkan 1.3)
VK_DYNAMIC_STATE_PRIMITIVE_TOPOLOGY vkCmdSetPrimitiveTopology 图元拓扑(Vulkan 1.3)
VK_DYNAMIC_STATE_VERTEX_INPUT_BINDING_STRIDE vkCmdSetVertexInputBindingStride 顶点绑定步长(Vulkan 1.3)
VK_DYNAMIC_STATE_DEPTH_TEST_ENABLE vkCmdSetDepthTestEnable 深度测试启用(Vulkan 1.3)
VK_DYNAMIC_STATE_DEPTH_WRITE_ENABLE vkCmdSetDepthWriteEnable 深度写入启用(Vulkan 1.3)
VK_DYNAMIC_STATE_DEPTH_COMPARE_OP vkCmdSetDepthCompareOp 深度比较操作(Vulkan 1.3)
VK_DYNAMIC_STATE_DEPTH_BOUNDS_TEST_ENABLE vkCmdSetDepthBoundsTestEnable 深度边界测试启用(Vulkan 1.3)
VK_DYNAMIC_STATE_STENCIL_TEST_ENABLE vkCmdSetStencilTestEnable 模板测试启用(Vulkan 1.3)
VK_DYNAMIC_STATE_STENCIL_OP vkCmdSetStencilOp 模板操作(Vulkan 1.3)
VK_DYNAMIC_STATE_RASTERIZER_DISCARD_ENABLE vkCmdSetRasterizerDiscardEnable 光栅化丢弃启用(Vulkan 1.3)
VK_DYNAMIC_STATE_DEPTH_BIAS_ENABLE vkCmdSetDepthBiasEnable 深度偏移启用(Vulkan 1.3)
VK_DYNAMIC_STATE_PRIMITIVE_RESTART_ENABLE vkCmdSetPrimitiveRestartEnable 图元重启启用(Vulkan 1.3)
typedef struct VkPipelineViewportStateCreateInfo {
VkStructureType sType; // 必须为 VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO
const void* pNext; // nullptr
VkPipelineViewportStateCreateFlags flags; // 0
uint32_t viewportCount; // 视口数量
const VkViewport* pViewports; // 视口数组
uint32_t scissorCount; // 裁剪区域数量
const VkRect2D* pScissors; // 裁剪区域数组
} VkPipelineViewportStateCreateInfo;
typedef struct VkPipelineRasterizationStateCreateInfo {
VkStructureType sType; // 必须为 VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO
const void* pNext; // nullptr
VkPipelineRasterizationStateCreateFlags flags; // 0
VkBool32 depthClampEnable; // 是否启用深度钳制
VkBool32 rasterizerDiscardEnable; // 是否丢弃所有片元(不渲染任何内容)
VkPolygonMode polygonMode; // 多边形模式,主要有:
//VK_POLYGON_MODE_FILL 填充模式(默认)
//VK_POLYGON_MODE_LINE 线框模式
//VK_POLYGON_MODE_POINT 点模式
VkCullModeFlags cullMode; // 面剔除模式,主要有:
//VK_CULL_MODE_NONE 不剔除任何面(所有面都渲染)
//VK_CULL_MODE_FRONT_BIT 剔除正面(朝向相机的面)
//VK_CULL_MODE_BACK_BIT 剔除背面(背向相机的面)【最常用】
//VK_CULL_MODE_FRONT_AND_BACK 剔除所有面(什么也不渲染)
VkFrontFace frontFace; // 正面朝向
//VK_FRONT_FACE_COUNTER_CLOCKWISE 逆时针环绕(CCW)为正 【Vulkan 默认】
//VK_FRONT_FACE_CLOCKWISE 顺时针环绕(CW)为正
VkBool32 depthBiasEnable; // 是否启用深度偏移
float depthBiasConstantFactor; // 深度偏移常量因子
float depthBiasClamp; // 深度偏移最大值
float depthBiasSlopeFactor; // 深度偏移斜率因子
//计算公式为:depthBias = constantFactor + max(slopeFactor * maxSlope, 0.0)
float lineWidth; // 线宽(仅在 Vulkan 1.0 中静态设置)
} VkPipelineRasterizationStateCreateInfo;
typedef struct VkPipelineMultisampleStateCreateInfo {
VkStructureType sType; // 必须为 VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO
const void* pNext; // nullptr
VkPipelineMultisampleStateCreateFlags flags; // 0
VkSampleCountFlagBits rasterizationSamples; // 采样点数
//VK_SAMPLE_COUNT_1_BIT 1(无 MSAA) 标准渲染(性能最优)
//VK_SAMPLE_COUNT_2_BIT 2 轻量级抗锯齿
//VK_SAMPLE_COUNT_4_BIT 4 最常用(质量和性能的平衡)
//VK_SAMPLE_COUNT_8_BIT 8 高质量抗锯齿(性能开销较大)
//VK_SAMPLE_COUNT_16_BIT 16 极高品质(仅高端硬件支持)
//VK_SAMPLE_COUNT_32_BIT 32 超采样(非常罕见)
//VK_SAMPLE_COUNT_64_BIT 64 几乎不可用
VkBool32 sampleShadingEnable; // 是否启用样本着色(Sample Shading)
float minSampleShading; // 最小样本着色率(0.0 ~ 1.0)
const VkSampleMask* pSampleMask; // 样本遮罩(控制哪些样本被使用)
VkBool32 alphaToCoverageEnable; // 是否启用 Alpha-to-Coverage(透明度抗锯齿)
VkBool32 alphaToOneEnable; // 是否启用 Alpha-to-One(将 Alpha 替换为 1.0)
} VkPipelineMultisampleStateCreateInfo;
typedef struct VkPipelineColorBlendAttachmentState {
VkBool32 blendEnable; // 是否启用混合
VkBlendFactor srcColorBlendFactor; // 源颜色的混合因子(RGB)
VkBlendFactor dstColorBlendFactor; // 目标颜色的混合因子(RGB)
VkBlendOp colorBlendOp; // 颜色混合操作(RGB)
VkBlendFactor srcAlphaBlendFactor; // 源颜色的混合因子(Alpha)
VkBlendFactor dstAlphaBlendFactor; // 目标颜色的混合因子(Alpha)
VkBlendOp alphaBlendOp; // Alpha 混合操作
VkColorComponentFlags colorWriteMask; // 颜色通道写入掩码(RGBA)
} VkPipelineColorBlendAttachmentState;
混合因子主要有:
混合因子 公式(RGB) 公式(Alpha)
VK_BLEND_FACTOR_ZERO (0, 0, 0) 0
VK_BLEND_FACTOR_ONE (1, 1, 1) 1
VK_BLEND_FACTOR_SRC_COLOR (Rs, Gs, Bs) As
VK_BLEND_FACTOR_ONE_MINUS_SRC_COLOR (1-Rs, 1-Gs, 1-Bs) 1-As
VK_BLEND_FACTOR_DST_COLOR (Rd, Gd, Bd) Ad
VK_BLEND_FACTOR_ONE_MINUS_DST_COLOR (1-Rd, 1-Gd, 1-Bd) 1-Ad
VK_BLEND_FACTOR_SRC_ALPHA (As, As, As) As
VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA (1-As, 1-As, 1-As) 1-As
VK_BLEND_FACTOR_DST_ALPHA (Ad, Ad, Ad) Ad
VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA (1-Ad, 1-Ad, 1-Ad) 1-Ad
VK_BLEND_FACTOR_CONSTANT_COLOR (Cc, Cc, Cc) Ac
VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_COLOR (1-Cc, 1-Cc, 1-Cc) 1-Ac
VK_BLEND_FACTOR_CONSTANT_ALPHA (Ac, Ac, Ac) Ac
VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_ALPHA (1-Ac, 1-Ac, 1-Ac) 1-Ac
VK_BLEND_FACTOR_SRC_ALPHA_SATURATE (f, f, f),其中 f = min(As, 1-Ad) 1
混合操作主要有:
混合操作 公式
VK_BLEND_OP_ADD Src * SrcFactor + Dst * DstFactor
VK_BLEND_OP_SUBTRACT Src * SrcFactor - Dst * DstFactor
VK_BLEND_OP_REVERSE_SUBTRACT Dst * DstFactor - Src * SrcFactor
VK_BLEND_OP_MIN min(Src, Dst)
VK_BLEND_OP_MAX max(Src, Dst)
VK_BLEND_OP_ZERO_EXT 扩展操作(较少使用)
颜色通道写入掩码主要有:
VK_COLOR_COMPONENT_R_BIT 写入红色通道
VK_COLOR_COMPONENT_G_BIT 写入绿色通道
VK_COLOR_COMPONENT_B_BIT 写入蓝色通道
VK_COLOR_COMPONENT_A_BIT 写入 Alpha 通道
typedef struct VkPipelineColorBlendStateCreateInfo {
VkStructureType sType; // 必须为 VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO
const void* pNext; // nullptr
VkPipelineColorBlendStateCreateFlags flags; //0
VkBool32 logicOpEnable; // 是否启用逻辑操作(取代混合)
VkLogicOp logicOp; // 逻辑操作类型,主要有
//逻辑操作 公式(按位)
//VK_LOGIC_OP_CLEAR 0
//VK_LOGIC_OP_AND Src & Dst
//VK_LOGIC_OP_OR Src | Dst
//VK_LOGIC_OP_XOR Src ^ Dst
//VK_LOGIC_OP_SET 1
//VK_LOGIC_OP_COPY
//VK_LOGIC_OP_COPY_INVERTED ~Src
//VK_LOGIC_OP_NAND ~(Src & Dst)
//VK_LOGIC_OP_NOR ~(Src | Dst)
//VK_LOGIC_OP_EQUIVALENT ~(Src ^ Dst)
uint32_t attachmentCount; // 颜色附件数量
const VkPipelineColorBlendAttachmentState* pAttachments; // 每个附件的混合配置数组
float blendConstants[4]; // 全局混合常量(RGBA)
} VkPipelineColorBlendStateCreateInfo;
typedef struct VkPipelineLayoutCreateInfo {
VkStructureType sType; // 必须为 VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO
const void* pNext; // nullptr
VkPipelineLayoutCreateFlags flags; // 0
uint32_t setLayoutCount; // 描述符集布局数量
const VkDescriptorSetLayout* pSetLayouts; // 描述符集布局数组
uint32_t pushConstantRangeCount; // 推式常量范围数量
const VkPushConstantRange* pPushConstantRanges; // 推式常量范围数组
} VkPipelineLayoutCreateInfo;
typedef struct VkGraphicsPipelineCreateInfo {
VkStructureType sType; // 必须为 VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO
const void* pNext; // nullptr
VkPipelineCreateFlags flags; // 创建标志,主要有
//VK_PIPELINE_CREATE_DISABLE_OPTIMIZATION_BIT 禁用优化(快速编译)
//VK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT 允许派生子管线
//VK_PIPELINE_CREATE_DERIVATIVE_BIT 标记为派生管线
//VK_PIPELINE_CREATE_VIEW_INDEX_FROM_DEVICE_INDEX_BIT 多设备渲染
//VK_PIPELINE_CREATE_DISPATCH_BASE_BIT 支持基础调度
uint32_t stageCount; // 着色器阶段数量
const VkPipelineShaderStageCreateInfo* pStages; // 着色器阶段数组
const VkPipelineVertexInputStateCreateInfo* pVertexInputState; // 顶点输入状态
const VkPipelineInputAssemblyStateCreateInfo* pInputAssemblyState; // 输入组装状态
const VkPipelineTessellationStateCreateInfo* pTessellationState; // 细分状态
const VkPipelineViewportStateCreateInfo* pViewportState; // 视口状态
const VkPipelineRasterizationStateCreateInfo* pRasterizationState; // 光栅化状态
const VkPipelineMultisampleStateCreateInfo* pMultisampleState; // 多重采样状态
const VkPipelineDepthStencilStateCreateInfo* pDepthStencilState; // 深度模板状态
const VkPipelineColorBlendStateCreateInfo* pColorBlendState; // 颜色混合状态
const VkPipelineDynamicStateCreateInfo* pDynamicState; // 动态状态
VkPipelineLayout layout; // 管线布局
VkRenderPass renderPass; // 渲染通道
uint32_t subpass; // 子通道索引
VkPipeline basePipelineHandle; // 基管线句柄
int32_t basePipelineIndex; // 基管线索引
} VkGraphicsPipelineCreateInfo;
typedef struct VkAttachmentDescription {
VkAttachmentDescriptionFlags flags; // 0
VkFormat format; // 像素格式与上面的图像视图一样即可
VkSampleCountFlagBits samples; // 采样数,和上面那个一样
VkAttachmentLoadOp loadOp; // 颜色/深度数据加载操作
VkAttachmentStoreOp storeOp; // 颜色/深度数据存储操作
VkAttachmentLoadOp stencilLoadOp; // 模板数据加载操作
VkAttachmentStoreOp stencilStoreOp; // 模板数据存储操作
VkImageLayout initialLayout; // 渲染前的图像布局
VkImageLayout finalLayout; // 渲染后的图像布局
} VkAttachmentDescription;
关于上面出现的后面带Op的,他们的操作有如下几种:
VK_ATTACHMENT_LOAD_OP_LOAD 保留附件已有内容
VK_ATTACHMENT_LOAD_OP_CLEAR 清除附件内容(指定颜色/深度值)
VK_ATTACHMENT_LOAD_OP_DONT_CARE 不关心旧内容(可丢弃)
VK_ATTACHMENT_STORE_OP_STORE 保存渲染结果
VK_ATTACHMENT_STORE_OP_DONT_CARE 丢弃渲染结果
还有Layout结尾的:
VK_IMAGE_LAYOUT_UNDEFINED 未定义(内容无效) 配合 LOAD_OP_CLEAR 或 DONT_CARE
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL 颜色附件的最佳布局 颜色渲染目标
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL 深度/模板附件的最佳布局 深度/模板渲染目标
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR 用于呈现到屏幕 交换链图像的最终布局
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL 着色器只读 纹理采样
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL / DEST 数据传输 拷贝/清除图像
typedef struct VkAttachmentReference {
uint32_t attachment; // 附件索引(指向 VkAttachmentDescription 数组)
VkImageLayout layout; // 该子通道中使用的图像布局,主要有:
//颜色附件:VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
//深度/模板附件:VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL
//只读输入:VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
//不关心:VK_IMAGE_LAYOUT_UNDEFINED
} VkAttachmentReference;
typedef struct VkSubpassDescription {
VkSubpassDescriptionFlags flags; // 0
VkPipelineBindPoint pipelineBindPoint; // 管线绑定点
//VK_PIPELINE_BIND_POINT_GRAPHICS:图形管线
//VK_PIPELINE_BIND_POINT_COMPUTE:计算管线
uint32_t inputAttachmentCount; // 输入附件数量
const VkAttachmentReference* pInputAttachments; // 输入附件引用数组
uint32_t colorAttachmentCount; // 颜色附件数量
const VkAttachmentReference* pColorAttachments; // 颜色附件引用数组
const VkAttachmentReference* pResolveAttachments; // 解析附件引用数组(用于 MSAA)
const VkAttachmentReference* pDepthStencilAttachment; // 深度/模板附件引用(单个)
uint32_t preserveAttachmentCount; // 保留附件数量
const uint32_t* pPreserveAttachments; // 保留附件索引数组
} VkSubpassDescription;
typedef struct VkSubpassDependency {
uint32_t srcSubpass; // 源子通道索引(依赖的起点)特殊值:VK_SUBPASS_EXTERNAL表示外部操作
uint32_t dstSubpass; // 目标子通道索引(依赖的终点)特殊值:VK_SUBPASS_EXTERNAL表示外部操作
VkPipelineStageFlags srcStageMask; // 源管线阶段掩码(在哪个阶段完成)常见值:VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT、VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT
VkPipelineStageFlags dstStageMask; // 目标管线阶段掩码(在哪个阶段等待)
VkAccessFlags srcAccessMask; // 源访问掩码(需要哪些访问完成)VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT(颜色写入完成)
VkAccessFlags dstAccessMask; // 目标访问掩码(需要哪些访问可用)VK_ACCESS_INPUT_ATTACHMENT_READ_BIT(输入附件读取可用)
VkDependencyFlags dependencyFlags; // 依赖标志
//0:默认依赖(所有前序工作完成才能开始后续工作)。
//VK_DEPENDENCY_BY_REGION_BIT:按区域依赖(只依赖重叠的像素区域,性能优化!)。
//VK_DEPENDENCY_VIEW_LOCAL_BIT:视图本地依赖(用于多视图渲染)。
} VkSubpassDependency;
typedef struct VkRenderPassCreateInfo {
VkStructureType sType; // 必须为 VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO
const void* pNext; // nullptr
VkRenderPassCreateFlags flags; // 0
uint32_t attachmentCount; // 附件数量
const VkAttachmentDescription* pAttachments; // 附件描述数组
uint32_t subpassCount; // 子通道数量
const VkSubpassDescription* pSubpasses; // 子通道描述数组
uint32_t dependencyCount; // 子通道依赖数量
const VkSubpassDependency* pDependencies; // 子通道依赖数组
} VkRenderPassCreateInfo;
填写好这些信息之后,我就可以开始创建渲染管线了,先创建渲染通道(VkRenderPass),然后创建管线布局(VkPipelineLayout),最后就是渲染管线(VkPipeline)了,创建完后,先前的两个着色器就可以销毁了,示例代码:
VkAttachmentDescription colorAttachment = {};
colorAttachment.format = surfaceFormat.format;
colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
VkAttachmentReference colorAttachRef = {};
colorAttachRef.attachment = 0;
colorAttachRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
VkSubpassDescription spDes = {};
spDes.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
spDes.colorAttachmentCount = 1;
spDes.pColorAttachments = &colorAttachRef;
VkSubpassDependency spDep = {};
spDep.srcSubpass = VK_SUBPASS_EXTERNAL;
spDep.dstSubpass = 0;
spDep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
spDep.srcAccessMask = 0;
spDep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
spDep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
VkRenderPassCreateInfo rpCreateInfo = {};
rpCreateInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
rpCreateInfo.attachmentCount = 1;
rpCreateInfo.pAttachments = &colorAttachment;
rpCreateInfo.subpassCount = 1;
rpCreateInfo.pSubpasses = &spDes;
rpCreateInfo.dependencyCount = 1;
rpCreateInfo.pDependencies = &spDep;
VkRenderPass renderPass;
vkCreateRenderPass(device, &rpCreateInfo, nullptr, &renderPass);
VkPipelineVertexInputStateCreateInfo vertexInputCreateInfo = {};
vertexInputCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
vertexInputCreateInfo.vertexAttributeDescriptionCount = 0;
vertexInputCreateInfo.vertexBindingDescriptionCount = 0;
VkPipelineInputAssemblyStateCreateInfo inputAssembly = {};
inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
inputAssembly.primitiveRestartEnable = VK_FALSE;
VkDynamicState dynamicStates[] = {
VK_DYNAMIC_STATE_VIEWPORT,
VK_DYNAMIC_STATE_SCISSOR
};
VkPipelineDynamicStateCreateInfo dynaStateCreateInfo = {};
dynaStateCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dynaStateCreateInfo.dynamicStateCount = sizeof(dynamicStates) / sizeof(dynamicStates[0]);
dynaStateCreateInfo.pDynamicStates = dynamicStates;
VkPipelineViewportStateCreateInfo viewportCreateInfo = {};
viewportCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewportCreateInfo.viewportCount = 1;
viewportCreateInfo.scissorCount = 1;
VkPipelineRasterizationStateCreateInfo rasterCreateInfo = {};
rasterCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
rasterCreateInfo.depthClampEnable = VK_FALSE;
rasterCreateInfo.rasterizerDiscardEnable = VK_FALSE;
rasterCreateInfo.polygonMode = VK_POLYGON_MODE_FILL;
rasterCreateInfo.lineWidth = 1.0f;
rasterCreateInfo.cullMode = VK_CULL_MODE_BACK_BIT;
rasterCreateInfo.frontFace = VK_FRONT_FACE_CLOCKWISE;
rasterCreateInfo.depthBiasEnable = VK_FALSE;
VkPipelineMultisampleStateCreateInfo multisampleCreateInfo = {};
multisampleCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
multisampleCreateInfo.sampleShadingEnable = VK_FALSE;
multisampleCreateInfo.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
VkPipelineColorBlendAttachmentState colorBlendAttachment{};
colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
colorBlendAttachment.blendEnable = VK_FALSE;
VkPipelineColorBlendStateCreateInfo colorBlending{};
colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
colorBlending.logicOpEnable = VK_FALSE;
colorBlending.attachmentCount = 1;
colorBlending.pAttachments = &colorBlendAttachment;
VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 0;
pipelineLayoutInfo.pushConstantRangeCount = 0;
VkPipelineLayout pipelineLayout;
vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &pipelineLayout);
VkGraphicsPipelineCreateInfo pipelineInfo{};
pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
pipelineInfo.stageCount = 2;
pipelineInfo.pStages = shaderStages;
pipelineInfo.pVertexInputState = &vertexInputCreateInfo;
pipelineInfo.pInputAssemblyState = &inputAssembly;
pipelineInfo.pViewportState = &viewportCreateInfo;
pipelineInfo.pRasterizationState = &rasterCreateInfo;
pipelineInfo.pMultisampleState = &multisampleCreateInfo;
pipelineInfo.pDepthStencilState = nullptr;
pipelineInfo.pColorBlendState = &colorBlending;
pipelineInfo.pDynamicState = &dynaStateCreateInfo;
pipelineInfo.layout = pipelineLayout;
pipelineInfo.renderPass = renderPass;
pipelineInfo.subpass = 0;
VkPipeline graphicsPipeline;
vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &graphicsPipeline);
vkDestroyShaderModule(device, vertexMod, nullptr);
vkDestroyShaderModule(device, fragmentMod, nullptr);
根据刚刚的Vulkan渲染管线图像最后会输出到帧缓冲(Framebuffer),所以我还要为交换链中的每个图像视图创建帧缓冲,信息格式如下:
typedef struct VkFramebufferCreateInfo {
VkStructureType sType; // 必须为 VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO
const void* pNext; // nullptr
VkFramebufferCreateFlags flags; // 0
VkRenderPass renderPass; // 关联的渲染通道
uint32_t attachmentCount; // 附件数量
const VkImageView* pAttachments; // 图像视图数组
uint32_t width; // 帧缓冲宽度(像素)
uint32_t height; // 帧缓冲高度(像素)
uint32_t layers; // 层数(通常为 1)
} VkFramebufferCreateInfo;
创建示例如下:
VkFramebuffer frameBuffers[imagesNum];
for (unsigned int i = 0; i < imagesNum; i++) {
VkImageView attachments[] = {
spImagesView[i]
};
VkFramebufferCreateInfo framebufferInfo{};
framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
framebufferInfo.renderPass = renderPass;
framebufferInfo.attachmentCount = 1;
framebufferInfo.pAttachments = attachments;
framebufferInfo.width = spExtent.width;
framebufferInfo.height = spExtent.height;
framebufferInfo.layers = 1;
vkCreateFramebuffer(device, &framebufferInfo, nullptr, &frameBuffers[i]);
}
做完这些,我们就可以进入循环,开始渲染了。
首先我们可以让CPU先准备下一帧的绘制,但是为了保证CPU 不会在 GPU 还在使用资源时覆盖或修改它们,我们需要等栅栏被触发完再进行下一步,设置栅栏为未触发状态后,进行下一步:
vkWaitForFences(device, 1, &flightFen, VK_TRUE, UINT64_MAX);
vkResetFences(device, 1, &flightFen);
接着我们需要从交换链中申请一张可以用来绘图的图像:
uint32_t imageIndex;
vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageSem, VK_NULL_HANDLE, &imageIndex);
我们重置命令缓冲区,重新绘制命令:
vkResetCommandBuffer(commandBuffer, 0);
我们重置命令缓冲区,重新绘制命令:
vkResetCommandBuffer(commandBuffer, 0);
用一个空白的缓冲区开始录制命令:
VkCommandBufferBeginInfo cbbInfo = {};
cbbInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
vkBeginCommandBuffer(commandBuffer, &cbbInfo);
同时我们绑定渲染通道和渲染管线
VkRenderPassBeginInfo renderPassInfo{};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
renderPassInfo.renderPass = renderPass;
renderPassInfo.framebuffer = frameBuffers[imageIndex];
renderPassInfo.renderArea.offset = { 0, 0 };
renderPassInfo.renderArea.extent = spExtent;
VkClearValue clearColor = { {{0.0f, 0.0f, 0.0f, 1.0f}} };
renderPassInfo.clearValueCount = 1;
renderPassInfo.pClearValues = &clearColor;
vkCmdBeginRenderPass(commandBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);
VkRenderPassBeginInfo的详细参数可以去Vulkan的官方文档查看
然后我们设置视口和裁剪器:
VkViewport viewport{};
viewport.x = 0.0f;
viewport.y = 0.0f;
viewport.width = static_cast(spExtent.width);
viewport.height = static_cast(spExtent.height);
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
vkCmdSetViewport(commandBuffer, 0, 1, &viewport);
VkRect2D scissor{};
scissor.offset = { 0, 0 };
scissor.extent = spExtent;
vkCmdSetScissor(commandBuffer, 0, 1, &scissor);
我们设置渲染命令,参数依次为:vertexCount(顶点数)、instanceCount(实例数)、firstVertex(起始顶点偏移)、firstInstance(起始实例偏移)。
vkCmdDraw(commandBuffer, 3, 1, 0, 0);
然后我们结束渲染通道和命令缓冲区的录制:
vkCmdEndRenderPass(commandBuffer);
vkEndCommandBuffer(commandBuffer);
然后我们将命令缓冲区的队列提交到GPU队列:
VkSemaphore waitSem[1] = {imageSem};
VkSemaphore signalSem[1] = { renderSem };
VkPipelineStageFlags waitStages[1] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
VkSubmitInfo submitInfo = {};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = waitSem;
submitInfo.pWaitDstStageMask = waitStages;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffer;
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = signalSem;
vkQueueSubmit(graphicsQueue, 1, &submitInfo, flightFen) ;
最后我们将图像呈现即可:
VkSwapchainKHR swapchains[1] = {swapChain};
VkPresentInfoKHR presentInfo = {};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores = signalSem;
presentInfo.swapchainCount = 1;
presentInfo.pSwapchains = swapchains;
presentInfo.pImageIndices = &imageIndex;
vkQueuePresentKHR(presentQueue, &presentInfo);
循环部分就结束了,不过一定要记得在循环结束后调用vkDeviceWaitIdle(device);,让GPU先把最后一帧的活干完再停下。
最后要退出程序时,别忘了把你之前创建的那一大堆东西都销毁掉!并且记住销毁顺序:先创建的后销毁,后创建的先销毁!
for (auto framebuffer : frameBuffers) {
vkDestroyFramebuffer(device, framebuffer, nullptr);
}
vkDestroyPipeline(device, graphicsPipeline, nullptr);
vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
vkDestroyRenderPass(device, renderPass, nullptr);
free((void*)vertexCode);
free((void*)fragmentCode);
vkDestroySemaphore(device, imageSem, nullptr);
vkDestroySemaphore(device, renderSem, nullptr);
vkDestroyFence(device, flightFen, nullptr);
vkDestroyCommandPool(device, commandPool, nullptr);
for (auto imageView : spImagesView) {
vkDestroyImageView(device, imageView, nullptr);
}
vkDestroySwapchainKHR(device, swapChain, nullptr);
vkDestroyDevice(device, nullptr);
SDL_Vulkan_DestroySurface(instance, surface, nullptr);
vkDestroyInstance(instance, nullptr);
可以看到这么一套下来已经快写了五六百行代码了,但这也只是搭建起了最基本的一个框架,里面的三角形甚至都是硬编码出来的。
参考资料:
https://tutorial.vulkan.net.cn/
https://docs.vulkan.org/refpages/latest/refpages/
https://zh.wikipedia.org/wiki/Vulkan