Vulkan 示例解析:triangle.cpp 如何把一个三角形渲染到窗口上
本文分析的是Vulkan的 examples/triangle/triangle.cpp。这是 Sascha Willems Vulkan 示例中的基础案例,标题是 Basic indexed triangle。它的目标很直接:不依赖过多封装,手动展示 Vulkan 从初始化、创建资源、录制命令到提交并显示画面的完整路径。
/*
* Vulkan 示例 - 基础索引三角形渲染
*
* 说明:
* 这是一个“贴近底层”的示例,用于展示如何让 Vulkan 启动并显示内容
* 与其他示例不同,本示例不会使用辅助函数或初始化器
* 少数情况除外(例如交换链设置)
*
* 版权所有 (C) 2016-2025 Sascha Willems - www.saschawillems.de
*
* 本代码基于 MIT 许可证授权 (MIT) (http://opensource.org/licenses/MIT)
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <fstream>
#include <vector>
#include <exception>
#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <vulkan/vulkan.h>
#include "vulkanexamplebase.h"
// 我们希望让 GPU 和 CPU 都保持忙碌;为此,可以在上一帧命令缓冲仍在执行时开始构建新的命令缓冲
// 该数值定义了同一时间最多允许多少帧同时处于处理中
// 增大该值可能提升性能,但也会引入额外延迟
constexpr auto MAX_CONCURRENT_FRAMES = 2;
class VulkanExample : public VulkanExampleBase
{
public:
// 本示例使用的顶点布局
struct Vertex {
float position[3];
float color[3];
};
// 顶点缓冲及其属性
struct {
VkDeviceMemory memory{ VK_NULL_HANDLE }; // 该缓冲区对应的设备内存句柄
VkBuffer buffer{ VK_NULL_HANDLE }; // 与这块内存绑定的 Vulkan 缓冲对象句柄
} vertices;
// 索引缓冲
struct {
VkDeviceMemory memory{ VK_NULL_HANDLE };
VkBuffer buffer{ VK_NULL_HANDLE };
uint32_t count{ 0 };
} indices;
// 统一缓冲块对象
struct UniformBuffer {
VkDeviceMemory memory{ VK_NULL_HANDLE };
VkBuffer buffer{ VK_NULL_HANDLE };
// 描述符集用于存储绑定到着色器绑定点的资源
// 它将不同着色器的绑定点与这些绑定所使用的缓冲区和图像连接起来
VkDescriptorSet descriptorSet{ VK_NULL_HANDLE };
// 保留一个指向已映射缓冲区的指针,便于通过 memcpy 更新其内容
uint8_t* mapped{ nullptr };
};
// 每帧使用一个 UBO,这样可以实现帧重叠,并确保仍在使用的 uniform 不会被更新
std::array<UniformBuffer, MAX_CONCURRENT_FRAMES> uniformBuffers;
// 为了简化处理,这里使用与着色器中相同的 uniform 块布局:
//
// layout(set = 0, binding = 0) uniform UBO
// {
// mat4 projectionMatrix;
// mat4 modelMatrix;
// mat4 viewMatrix;
// } ubo;
//
// 这样可以直接将 ubo 数据 memcpy 到 uniform buffer 中
// 注意:应使用与 GPU 对齐规则匹配的数据类型,以避免手动填充(例如 vec4、mat4)
struct ShaderData {
glm::mat4 projectionMatrix;
glm::mat4 modelMatrix;
glm::mat4 viewMatrix;
};
// 管线布局用于让管线访问描述符集
// 它定义了管线所用着色器阶段与着色器资源之间的接口,但不绑定任何实际数据
// 只要接口匹配,一个管线布局就可以被多个管线共享
VkPipelineLayout pipelineLayout{ VK_NULL_HANDLE };
// 管线(通常称为“管线状态对象”)用于固化所有会影响管线的状态
// 在 OpenGL 中,几乎所有状态都可以在任意时刻修改;而 Vulkan 要求预先布局图形(以及计算)管线状态
// 因此,对于每一种非动态管线状态组合,都需要创建一个新的管线(这里不讨论少数例外情况)
// 虽然这增加了预先规划的维度,但也为驱动进行性能优化提供了很好的机会
VkPipeline pipeline{ VK_NULL_HANDLE };
// 描述符集布局描述着色器绑定布局,但并不真正引用描述符
// 与管线布局类似,它本质上是一个蓝图;只要布局匹配,就可以与不同的描述符集配合使用
VkDescriptorSetLayout descriptorSetLayout{ VK_NULL_HANDLE };
// 同步原语
// 同步是 Vulkan 中的重要概念,而 OpenGL 通常将其隐藏起来;正确处理同步是正确使用 Vulkan 的关键
// 信号量用于协调图形队列中的操作,并保证命令顺序正确
std::vector<VkSemaphore> presentCompleteSemaphores{};
std::vector<VkSemaphore> renderCompleteSemaphores{};
VkCommandPool commandPool{ VK_NULL_HANDLE };
std::array<VkCommandBuffer, MAX_CONCURRENT_FRAMES> commandBuffers{};
std::array<VkFence, MAX_CONCURRENT_FRAMES> waitFences{};
// 为选择正确的同步对象和命令对象,需要跟踪当前帧
uint32_t currentFrame{ 0 };
VulkanExample() : VulkanExampleBase()
{
title = "Basic indexed triangle";
// 为了保持简单,这里不使用框架提供的 UI 覆盖层
settings.overlay = false;
// 设置一个默认的 look-at 摄像机
camera.type = Camera::CameraType::lookat;
camera.setPosition(glm::vec3(0.0f, 0.0f, -2.5f));
camera.setRotation(glm::vec3(0.0f));
camera.setPerspective(60.0f, (float)width / (float)height, 1.0f, 256.0f);
// 此处未设置的值会在基类构造函数中初始化
}
~VulkanExample() override
{
// 清理使用过的 Vulkan 资源
// 注意:继承的析构函数会清理存储在基类中的资源
if (device) {
vkDestroyPipeline(device, pipeline, nullptr);
vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr);
vkDestroyBuffer(device, vertices.buffer, nullptr);
vkFreeMemory(device, vertices.memory, nullptr);
vkDestroyBuffer(device, indices.buffer, nullptr);
vkFreeMemory(device, indices.memory, nullptr);
vkDestroyCommandPool(device, commandPool, nullptr);
for (size_t i = 0; i < presentCompleteSemaphores.size(); i++) {
vkDestroySemaphore(device, presentCompleteSemaphores[i], nullptr);
}
for (size_t i = 0; i < renderCompleteSemaphores.size(); i++) {
vkDestroySemaphore(device, renderCompleteSemaphores[i], nullptr);
}
for (uint32_t i = 0; i < MAX_CONCURRENT_FRAMES; i++) {
vkDestroyFence(device, waitFences[i], nullptr);
vkDestroyBuffer(device, uniformBuffers[i].buffer, nullptr);
vkFreeMemory(device, uniformBuffers[i].memory, nullptr);
}
}
}
// 该函数用于请求一种支持所有指定属性标志的设备内存类型(例如设备本地、主机可见)
// 成功时返回符合所需内存属性的内存类型索引
// 这是必要的,因为不同实现可以提供任意数量、属性各异的内存类型
// 内存属性。
// 可在 https://vulkan.gpuinfo.org/ 查看不同内存配置的详细信息
uint32_t getMemoryTypeIndex(uint32_t typeBits, VkMemoryPropertyFlags properties)
{
// 遍历本示例所用设备可用的全部内存类型
for (uint32_t i = 0; i < deviceMemoryProperties.memoryTypeCount; i++)
{
if ((typeBits & 1) == 1)
{
if ((deviceMemoryProperties.memoryTypes[i].propertyFlags & properties) == properties)
{
return i;
}
}
typeBits >>= 1;
}
throw "Could not find a suitable memory type!";
}
// 创建本示例中逐帧(in-flight)使用的 Vulkan 同步原语
void createSynchronizationPrimitives()
{
// 栅栏用于在主机端检查绘制命令缓冲是否执行完成
for (uint32_t i = 0; i < MAX_CONCURRENT_FRAMES; i++) {
VkFenceCreateInfo fenceCI{};
fenceCI.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
// 以已发信号状态创建栅栏,这样首次渲染每个命令缓冲时不会等待
fenceCI.flags = VK_FENCE_CREATE_SIGNALED_BIT;
// 该栅栏用于确保命令缓冲执行完成后再被复用
VK_CHECK_RESULT(vkCreateFence(device, &fenceCI, nullptr, &waitFences[i]));
}
// 信号量用于保证队列内部的命令顺序正确
// 用于确保图像呈现完成后再开始下一次提交
presentCompleteSemaphores.resize(MAX_CONCURRENT_FRAMES);
for (auto& semaphore : presentCompleteSemaphores) {
VkSemaphoreCreateInfo semaphoreCI{ VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO };
VK_CHECK_RESULT(vkCreateSemaphore(device, &semaphoreCI, nullptr, &semaphore));
}
// 渲染完成同步
// 该信号量用于确保所有已提交命令执行完成后,再将图像提交到队列进行呈现
renderCompleteSemaphores.resize(swapChain.images.size());
for (auto& semaphore : renderCompleteSemaphores) {
VkSemaphoreCreateInfo semaphoreCI{ VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO };
VK_CHECK_RESULT(vkCreateSemaphore(device, &semaphoreCI, nullptr, &semaphore));
}
}
void createCommandBuffers()
{
// 所有命令缓冲都从命令池中分配
VkCommandPoolCreateInfo commandPoolCI{};
commandPoolCI.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
commandPoolCI.queueFamilyIndex = swapChain.queueNodeIndex;
commandPoolCI.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
VK_CHECK_RESULT(vkCreateCommandPool(device, &commandPoolCI, nullptr, &commandPool));
// 从上述命令池中按最大并发帧数分配命令缓冲
VkCommandBufferAllocateInfo cmdBufAllocateInfo = vks::initializers::commandBufferAllocateInfo(commandPool, VK_COMMAND_BUFFER_LEVEL_PRIMARY, MAX_CONCURRENT_FRAMES);
VK_CHECK_RESULT(vkAllocateCommandBuffers(device, &cmdBufAllocateInfo, commandBuffers.data()));
}
// 为一个索引三角形准备顶点缓冲和索引缓冲
// 同时通过暂存缓冲将其上传到设备本地内存,并初始化与顶点着色器匹配的顶点输入和属性绑定
void createVertexBuffer()
{
// 关于 Vulkan 中内存管理的一点说明:
// 这是一个非常复杂的话题;示例程序进行少量独立内存分配尚可,
// 但真实应用不应这样做,而应一次性分配较大的内存块。
// 设置顶点数据
std::vector<Vertex> vertexBuffer{
{ { 1.0f, 1.0f, 0.0f }, { 1.0f, 0.0f, 0.0f } },
{ { -1.0f, 1.0f, 0.0f }, { 0.0f, 1.0f, 0.0f } },
{ { 0.0f, -1.0f, 0.0f }, { 0.0f, 0.0f, 1.0f } }
};
uint32_t vertexBufferSize = static_cast<uint32_t>(vertexBuffer.size()) * sizeof(Vertex);
// 设置索引数据
std::vector<uint32_t> indexBuffer{ 0, 1, 2 };
indices.count = static_cast<uint32_t>(indexBuffer.size());
uint32_t indexBufferSize = indices.count * sizeof(uint32_t);
VkMemoryAllocateInfo memAlloc{};
memAlloc.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
VkMemoryRequirements memReqs;
// 顶点缓冲和索引缓冲这类静态数据应存放在设备内存中,以便 GPU 获得最佳、最快访问
//
// 为实现这一点,这里使用所谓的“暂存缓冲”:
// - 创建一个主机可见且可映射的缓冲区
// - 将数据复制到该缓冲区
// - 创建另一个大小相同、位于设备本地(VRAM)的缓冲区
// - 使用命令缓冲将数据从主机端复制到设备端
// - 删除主机可见的暂存缓冲
// - 使用设备本地缓冲进行渲染
//
// 注意:在 CPU 与 GPU 共享同一内存的统一内存架构上,不一定需要暂存缓冲
// 为了保持示例易于理解,这里没有对此进行检测
struct StagingBuffer {
VkDeviceMemory memory;
VkBuffer buffer;
};
struct {
StagingBuffer vertices;
StagingBuffer indices;
} stagingBuffers{};
void* data;
// 顶点缓冲
VkBufferCreateInfo vertexBufferInfoCI{};
vertexBufferInfoCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
vertexBufferInfoCI.size = vertexBufferSize;
// 该缓冲区作为复制源使用
vertexBufferInfoCI.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
// 创建一个主机可见缓冲区,用于复制顶点数据(暂存缓冲)
VK_CHECK_RESULT(vkCreateBuffer(device, &vertexBufferInfoCI, nullptr, &stagingBuffers.vertices.buffer));
vkGetBufferMemoryRequirements(device, stagingBuffers.vertices.buffer, &memReqs);
memAlloc.allocationSize = memReqs.size;
// 请求一种主机可见的内存类型,以便将数据复制进去
// 同时要求其具备一致性,这样取消映射后写入即可立即对 GPU 可见
memAlloc.memoryTypeIndex = getMemoryTypeIndex(memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
VK_CHECK_RESULT(vkAllocateMemory(device, &memAlloc, nullptr, &stagingBuffers.vertices.memory));
// 映射并复制数据
VK_CHECK_RESULT(vkMapMemory(device, stagingBuffers.vertices.memory, 0, memAlloc.allocationSize, 0, &data));
memcpy(data, vertexBuffer.data(), vertexBufferSize);
vkUnmapMemory(device, stagingBuffers.vertices.memory);
VK_CHECK_RESULT(vkBindBufferMemory(device, stagingBuffers.vertices.buffer, stagingBuffers.vertices.memory, 0));
// 创建设备本地缓冲区,随后将主机本地顶点数据复制到其中,并用于渲染
vertexBufferInfoCI.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
VK_CHECK_RESULT(vkCreateBuffer(device, &vertexBufferInfoCI, nullptr, &vertices.buffer));
vkGetBufferMemoryRequirements(device, vertices.buffer, &memReqs);
memAlloc.allocationSize = memReqs.size;
memAlloc.memoryTypeIndex = getMemoryTypeIndex(memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
VK_CHECK_RESULT(vkAllocateMemory(device, &memAlloc, nullptr, &vertices.memory));
VK_CHECK_RESULT(vkBindBufferMemory(device, vertices.buffer, vertices.memory, 0));
// 索引缓冲
VkBufferCreateInfo indexbufferCI{};
indexbufferCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
indexbufferCI.size = indexBufferSize;
indexbufferCI.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
// 将索引数据复制到一个主机可见缓冲区(暂存缓冲)
VK_CHECK_RESULT(vkCreateBuffer(device, &indexbufferCI, nullptr, &stagingBuffers.indices.buffer));
vkGetBufferMemoryRequirements(device, stagingBuffers.indices.buffer, &memReqs);
memAlloc.allocationSize = memReqs.size;
memAlloc.memoryTypeIndex = getMemoryTypeIndex(memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
VK_CHECK_RESULT(vkAllocateMemory(device, &memAlloc, nullptr, &stagingBuffers.indices.memory));
VK_CHECK_RESULT(vkMapMemory(device, stagingBuffers.indices.memory, 0, indexBufferSize, 0, &data));
memcpy(data, indexBuffer.data(), indexBufferSize);
vkUnmapMemory(device, stagingBuffers.indices.memory);
VK_CHECK_RESULT(vkBindBufferMemory(device, stagingBuffers.indices.buffer, stagingBuffers.indices.memory, 0));
// 创建仅设备可见的目标缓冲区
indexbufferCI.usage = VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
VK_CHECK_RESULT(vkCreateBuffer(device, &indexbufferCI, nullptr, &indices.buffer));
vkGetBufferMemoryRequirements(device, indices.buffer, &memReqs);
memAlloc.allocationSize = memReqs.size;
memAlloc.memoryTypeIndex = getMemoryTypeIndex(memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
VK_CHECK_RESULT(vkAllocateMemory(device, &memAlloc, nullptr, &indices.memory));
VK_CHECK_RESULT(vkBindBufferMemory(device, indices.buffer, indices.memory, 0));
// 缓冲区复制必须提交到队列,因此需要为其准备一个命令缓冲
// 注意:某些设备提供专用传输队列(只设置 transfer 位),在大量复制时可能更快
VkCommandBuffer copyCmd;
VkCommandBufferAllocateInfo cmdBufAllocateInfo{};
cmdBufAllocateInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
cmdBufAllocateInfo.commandPool = commandPool;
cmdBufAllocateInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
cmdBufAllocateInfo.commandBufferCount = 1;
VK_CHECK_RESULT(vkAllocateCommandBuffers(device, &cmdBufAllocateInfo, ©Cmd));
VkCommandBufferBeginInfo cmdBufInfo = vks::initializers::commandBufferBeginInfo();
VK_CHECK_RESULT(vkBeginCommandBuffer(copyCmd, &cmdBufInfo));
// 将缓冲区区域复制命令写入命令缓冲
VkBufferCopy copyRegion{};
// 顶点缓冲
copyRegion.size = vertexBufferSize;
vkCmdCopyBuffer(copyCmd, stagingBuffers.vertices.buffer, vertices.buffer, 1, ©Region);
// 索引缓冲
copyRegion.size = indexBufferSize;
vkCmdCopyBuffer(copyCmd, stagingBuffers.indices.buffer, indices.buffer, 1, ©Region);
VK_CHECK_RESULT(vkEndCommandBuffer(copyCmd));
// 将命令缓冲提交到队列以完成复制
VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = ©Cmd;
// 创建栅栏,确保命令缓冲执行完成
VkFenceCreateInfo fenceCI{};
fenceCI.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
fenceCI.flags = 0;
VkFence fence;
VK_CHECK_RESULT(vkCreateFence(device, &fenceCI, nullptr, &fence));
// 提交到队列
VK_CHECK_RESULT(vkQueueSubmit(queue, 1, &submitInfo, fence));
// 等待栅栏发出信号,表示命令缓冲已执行完成
VK_CHECK_RESULT(vkWaitForFences(device, 1, &fence, VK_TRUE, DEFAULT_FENCE_TIMEOUT));
vkDestroyFence(device, fence, nullptr);
vkFreeCommandBuffers(device, commandPool, 1, ©Cmd);
// 销毁暂存缓冲
// 注意:在复制命令已经提交并执行完成之前,不能删除暂存缓冲
vkDestroyBuffer(device, stagingBuffers.vertices.buffer, nullptr);
vkFreeMemory(device, stagingBuffers.vertices.memory, nullptr);
vkDestroyBuffer(device, stagingBuffers.indices.buffer, nullptr);
vkFreeMemory(device, stagingBuffers.indices.memory, nullptr);
}
// 描述符从描述符池中分配;描述符池会告知实现最多会使用多少描述符以及使用哪些类型
void createDescriptorPool()
{
// 需要告知 API 每种类型最多请求多少个描述符
VkDescriptorPoolSize descriptorTypeCounts[1]{};
// 本示例只使用一种描述符类型(uniform buffer)
descriptorTypeCounts[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
// 每帧对应一个缓冲区,因此也对应一个描述符
descriptorTypeCounts[0].descriptorCount = MAX_CONCURRENT_FRAMES;
// 若使用其他类型,需要在类型计数列表中添加新条目
// 例如,两个组合图像采样器可以这样设置:
// typeCounts[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
// typeCounts[1].descriptorCount = 2;
// 创建全局描述符池
// 本示例使用的所有描述符都从该池中分配
VkDescriptorPoolCreateInfo descriptorPoolCI{};
descriptorPoolCI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
descriptorPoolCI.pNext = nullptr;
descriptorPoolCI.poolSizeCount = 1;
descriptorPoolCI.pPoolSizes = descriptorTypeCounts;
// 设置可从该池请求的描述符集最大数量(超出该限制会导致错误)
// 本示例会为每帧的每个 uniform buffer 创建一个描述符集
descriptorPoolCI.maxSets = MAX_CONCURRENT_FRAMES;
VK_CHECK_RESULT(vkCreateDescriptorPool(device, &descriptorPoolCI, nullptr, &descriptorPool));
}
// 描述符集布局定义应用程序与着色器之间的接口
// 它基本上将不同着色器阶段连接到用于绑定 uniform buffer、图像采样器等资源的描述符
// 因此,每个着色器绑定都应映射到一个描述符集布局绑定
void createDescriptorSetLayout()
{
// 绑定 0:Uniform buffer(顶点着色器)
VkDescriptorSetLayoutBinding layoutBinding{};
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
layoutBinding.descriptorCount = 1;
layoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
layoutBinding.pImmutableSamplers = nullptr;
VkDescriptorSetLayoutCreateInfo descriptorLayoutCI{};
descriptorLayoutCI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
descriptorLayoutCI.pNext = nullptr;
descriptorLayoutCI.bindingCount = 1;
descriptorLayoutCI.pBindings = &layoutBinding;
VK_CHECK_RESULT(vkCreateDescriptorSetLayout(device, &descriptorLayoutCI, nullptr, &descriptorSetLayout));
}
// 着色器通过“指向” uniform buffer 的描述符集访问数据
// 描述符集使用上面创建的描述符集布局
void createDescriptorSets()
{
// 从全局描述符池中为每帧分配一个描述符集
for (uint32_t i = 0; i < MAX_CONCURRENT_FRAMES; i++) {
VkDescriptorSetAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
allocInfo.descriptorPool = descriptorPool;
allocInfo.descriptorSetCount = 1;
allocInfo.pSetLayouts = &descriptorSetLayout;
VK_CHECK_RESULT(vkAllocateDescriptorSets(device, &allocInfo, &uniformBuffers[i].descriptorSet));
// 更新描述符集,以确定着色器绑定点
// 着色器中使用的每个绑定点都需要一个
// 与该绑定点匹配的描述符集
VkWriteDescriptorSet writeDescriptorSet{};
// 通过描述符信息结构传递缓冲区信息
VkDescriptorBufferInfo bufferInfo{};
bufferInfo.buffer = uniformBuffers[i].buffer;
bufferInfo.range = sizeof(ShaderData);
// 绑定 0:Uniform buffer
writeDescriptorSet.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writeDescriptorSet.dstSet = uniformBuffers[i].descriptorSet;
writeDescriptorSet.descriptorCount = 1;
writeDescriptorSet.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
writeDescriptorSet.pBufferInfo = &bufferInfo;
writeDescriptorSet.dstBinding = 0;
vkUpdateDescriptorSets(device, 1, &writeDescriptorSet, 0, nullptr);
}
}
// 创建帧缓冲所用的深度(以及模板)缓冲附件
// 注意:这是对基类虚函数的重写,并会从 VulkanExampleBase::prepare 内部调用
void setupDepthStencil() override
{
// 创建一个作为深度模板附件使用的最优布局图像
VkImageCreateInfo imageCI{};
imageCI.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageCI.imageType = VK_IMAGE_TYPE_2D;
imageCI.format = depthFormat;
// 使用示例的高度和宽度
imageCI.extent = { width, height, 1 };
imageCI.mipLevels = 1;
imageCI.arrayLayers = 1;
imageCI.samples = VK_SAMPLE_COUNT_1_BIT;
imageCI.tiling = VK_IMAGE_TILING_OPTIMAL;
imageCI.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
imageCI.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
VK_CHECK_RESULT(vkCreateImage(device, &imageCI, nullptr, &depthStencil.image));
// 为图像分配设备本地内存,并将其绑定到图像
VkMemoryAllocateInfo memAlloc{};
memAlloc.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
VkMemoryRequirements memReqs;
vkGetImageMemoryRequirements(device, depthStencil.image, &memReqs);
memAlloc.allocationSize = memReqs.size;
memAlloc.memoryTypeIndex = getMemoryTypeIndex(memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
VK_CHECK_RESULT(vkAllocateMemory(device, &memAlloc, nullptr, &depthStencil.memory));
VK_CHECK_RESULT(vkBindImageMemory(device, depthStencil.image, depthStencil.memory, 0));
// 为深度模板图像创建视图
// 在 Vulkan 中不能直接访问图像,而是通过由子资源范围描述的视图进行访问
// 这允许同一图像拥有多个范围不同的视图(例如用于不同图层)
VkImageViewCreateInfo depthStencilViewCI{};
depthStencilViewCI.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
depthStencilViewCI.viewType = VK_IMAGE_VIEW_TYPE_2D;
depthStencilViewCI.format = depthFormat;
depthStencilViewCI.subresourceRange = {};
depthStencilViewCI.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
// 只有深度 + 模板格式才应设置模板方面(VK_FORMAT_D16_UNORM_S8_UINT 到 VK_FORMAT_D32_SFLOAT_S8_UINT)
if (depthFormat >= VK_FORMAT_D16_UNORM_S8_UINT) {
depthStencilViewCI.subresourceRange.aspectMask |= VK_IMAGE_ASPECT_STENCIL_BIT;
}
depthStencilViewCI.subresourceRange.baseMipLevel = 0;
depthStencilViewCI.subresourceRange.levelCount = 1;
depthStencilViewCI.subresourceRange.baseArrayLayer = 0;
depthStencilViewCI.subresourceRange.layerCount = 1;
depthStencilViewCI.image = depthStencil.image;
VK_CHECK_RESULT(vkCreateImageView(device, &depthStencilViewCI, nullptr, &depthStencil.view));
}
// 为每个交换链图像创建帧缓冲
// 注意:这是对基类虚函数的重写,并会从 VulkanExampleBase::prepare 内部调用
void setupFrameBuffer() override
{
// 为交换链中的每张图像创建一个帧缓冲
frameBuffers.resize(swapChain.images.size());
for (size_t i = 0; i < frameBuffers.size(); i++)
{
std::array<VkImageView, 2> attachments{};
// 颜色附件是交换链图像的视图
attachments[0] = swapChain.imageViews[i];
// 由于当前 GPU 处理深度的方式,深度/模板附件在所有帧缓冲之间共享
attachments[1] = depthStencil.view;
VkFramebufferCreateInfo frameBufferCI{};
frameBufferCI.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
// 所有帧缓冲使用相同的 render pass 配置
frameBufferCI.renderPass = renderPass;
frameBufferCI.attachmentCount = static_cast<uint32_t>(attachments.size());
frameBufferCI.pAttachments = attachments.data();
frameBufferCI.width = width;
frameBufferCI.height = height;
frameBufferCI.layers = 1;
// 创建帧缓冲
VK_CHECK_RESULT(vkCreateFramebuffer(device, &frameBufferCI, nullptr, &frameBuffers[i]));
}
}
// Render pass 设置
// Render pass 是 Vulkan 中的新概念;它描述渲染期间使用的附件,并可包含多个带附件依赖的子通道
// 这使驱动可以预先知道渲染过程的形态,尤其有利于在基于 tile 的渲染器上进行优化(包括多个子通道)
// 使用子通道依赖还会为所用附件添加隐式布局转换,因此无需显式添加图像内存屏障来转换它们
// 注意:这是对基类虚函数的重写,并会从 VulkanExampleBase::prepare 内部调用
void setupRenderPass() override
{
// 本示例使用一个只包含单个子通道的 render pass
// 本 render pass 使用的附件描述
std::array<VkAttachmentDescription, 2> attachments{};
// 颜色附件
attachments[0].format = swapChain.colorFormat; // 使用交换链选择的颜色格式
attachments[0].samples = VK_SAMPLE_COUNT_1_BIT; // 本示例不使用多重采样
attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; // 在 render pass 开始时清除该附件
attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; // render pass 结束后保留其内容,以便显示
attachments[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; // 不使用模板,因此不关心加载操作
attachments[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; // 存储操作同理
attachments[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; // render pass 开始时的布局;初始内容无关紧要,因此使用 undefined
attachments[0].finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; // render pass 结束时该附件要转换到的布局
// 因为需要将颜色缓冲呈现到交换链,所以转换为 PRESENT_KHR
// 深度附件
attachments[1].format = depthFormat; // 合适的深度格式在示例基类中选择
attachments[1].samples = VK_SAMPLE_COUNT_1_BIT;
attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; // 在第一个子通道开始时清除深度
attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; // render pass 结束后不需要深度内容(DONT_CARE 可能带来更好性能)
attachments[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; // 不使用模板
attachments[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; // 不使用模板
attachments[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; // render pass 开始时的布局;初始内容无关紧要,因此使用 undefined
attachments[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; // 转换到深度/模板附件布局
// 设置附件引用
VkAttachmentReference colorReference{};
colorReference.attachment = 0; // 附件 0 为颜色附件
colorReference.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; // 子通道中作为颜色附件使用的布局
VkAttachmentReference depthReference{};
depthReference.attachment = 1; // 附件 1 为深度附件
depthReference.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; // 子通道中作为深度/模板附件使用的布局
// 设置单个子通道引用
VkSubpassDescription subpassDescription{};
subpassDescription.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpassDescription.colorAttachmentCount = 1; // 子通道使用一个颜色附件
subpassDescription.pColorAttachments = &colorReference; // 指向槽位 0 中颜色附件的引用
subpassDescription.pDepthStencilAttachment = &depthReference; // 指向槽位 1 中深度附件的引用
subpassDescription.inputAttachmentCount = 0; // 输入附件可用于从前一个子通道的内容中采样
subpassDescription.pInputAttachments = nullptr; // 本示例不使用输入附件
subpassDescription.preserveAttachmentCount = 0; // 保留附件可用于在子通道之间传递并保留附件内容
subpassDescription.pPreserveAttachments = nullptr; // 本示例不使用保留附件
subpassDescription.pResolveAttachments = nullptr; // 解析附件会在子通道结束时被解析,可用于多重采样等场景
// 设置子通道依赖
// 这些依赖会添加附件描述中指定的隐式附件布局转换
// 实际使用布局会通过附件引用中指定的布局来保持
// 每个子通道依赖都会在源子通道与目标子通道之间引入内存和执行依赖,
// 这些依赖由 srcStageMask、dstStageMask、srcAccessMask、dstAccessMask 描述(并设置 dependencyFlags)
// 注意:VK_SUBPASS_EXTERNAL 是一个特殊常量,表示所有在实际 render pass 之外执行的命令
std::array<VkSubpassDependency, 2> dependencies{};
// 执行深度附件和颜色附件从最终布局到初始布局的转换
// 深度附件
dependencies[0].srcSubpass = VK_SUBPASS_EXTERNAL;
dependencies[0].dstSubpass = 0;
dependencies[0].srcStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
dependencies[0].dstStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
dependencies[0].srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
dependencies[0].dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT;
dependencies[0].dependencyFlags = 0;
// 颜色附件
dependencies[1].srcSubpass = VK_SUBPASS_EXTERNAL;
dependencies[1].dstSubpass = 0;
dependencies[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependencies[1].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependencies[1].srcAccessMask = 0;
dependencies[1].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT;
dependencies[1].dependencyFlags = 0;
// 创建实际的 render pass
VkRenderPassCreateInfo renderPassCI{};
renderPassCI.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
renderPassCI.attachmentCount = static_cast<uint32_t>(attachments.size()); // 本 render pass 使用的附件数量
renderPassCI.pAttachments = attachments.data(); // render pass 使用的附件描述
renderPassCI.subpassCount = 1; // 本示例只使用一个子通道
renderPassCI.pSubpasses = &subpassDescription; // 该子通道的描述
renderPassCI.dependencyCount = static_cast<uint32_t>(dependencies.size()); // 子通道依赖数量
renderPassCI.pDependencies = dependencies.data(); // render pass 使用的子通道依赖
VK_CHECK_RESULT(vkCreateRenderPass(device, &renderPassCI, nullptr, &renderPass));
}
// Vulkan 从一种名为 SPIR-V 的即时二进制表示中加载着色器
// 着色器通常由 GLSL 等语言通过参考 glslang 编译器离线编译得到
// 该函数从二进制文件加载此类着色器,并返回一个着色器模块结构
VkShaderModule loadSPIRVShader(const std::string& filename)
{
size_t shaderSize;
char* shaderCode{ nullptr };
#if defined(__ANDROID__)
// 从压缩资源中加载着色器
AAsset* asset = AAssetManager_open(androidApp->activity->assetManager, filename.c_str(), AASSET_MODE_STREAMING);
assert(asset);
shaderSize = AAsset_getLength(asset);
assert(shaderSize > 0);
shaderCode = new char[shaderSize];
AAsset_read(asset, shaderCode, shaderSize);
AAsset_close(asset);
#else
std::ifstream is(filename, std::ios::binary | std::ios::in | std::ios::ate);
if (is.is_open())
{
shaderSize = is.tellg();
is.seekg(0, std::ios::beg);
// 将文件内容复制到缓冲区
shaderCode = new char[shaderSize];
is.read(shaderCode, shaderSize);
is.close();
assert(shaderSize > 0);
}
#endif
if (shaderCode)
{
// 创建一个新的着色器模块,用于管线创建
VkShaderModuleCreateInfo shaderModuleCI{};
shaderModuleCI.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
shaderModuleCI.codeSize = shaderSize;
shaderModuleCI.pCode = (uint32_t*)shaderCode;
VkShaderModule shaderModule;
VK_CHECK_RESULT(vkCreateShaderModule(device, &shaderModuleCI, nullptr, &shaderModule));
delete[] shaderCode;
return shaderModule;
}
else
{
std::cerr << "Error: Could not open shader file \"" << filename << "\"" << std::endl;
return VK_NULL_HANDLE;
}
}
void createPipelines()
{
// 创建管线布局,该布局用于生成基于此描述符集布局的渲染管线
// 在更复杂的场景中,可以为不同描述符集布局创建不同的管线布局,并进行复用
VkPipelineLayoutCreateInfo pipelineLayoutCI{};
pipelineLayoutCI.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutCI.pNext = nullptr;
pipelineLayoutCI.setLayoutCount = 1;
pipelineLayoutCI.pSetLayouts = &descriptorSetLayout;
VK_CHECK_RESULT(vkCreatePipelineLayout(device, &pipelineLayoutCI, nullptr, &pipelineLayout));
// 创建本示例使用的图形管线
// Vulkan 使用渲染管线概念封装固定状态,用以替代 OpenGL 复杂的状态机
// 管线随后会存储并在 GPU 上进行哈希处理,使管线切换非常快速
// 注意:仍有少量动态状态并不直接属于管线的一部分(但会包含其被使用的信息)
VkGraphicsPipelineCreateInfo pipelineCI{};
pipelineCI.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
// 此管线使用的布局(可以在使用相同布局的多个管线之间共享)
pipelineCI.layout = pipelineLayout;
// 此管线所附着的 render pass
pipelineCI.renderPass = renderPass;
// 构造组成管线的不同状态
// 输入装配状态描述图元如何被装配
// 该管线会将顶点数据装配为三角形列表(尽管这里只有一个三角形)
VkPipelineInputAssemblyStateCreateInfo inputAssemblyStateCI{};
inputAssemblyStateCI.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
inputAssemblyStateCI.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
// 光栅化状态
VkPipelineRasterizationStateCreateInfo rasterizationStateCI{};
rasterizationStateCI.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
rasterizationStateCI.polygonMode = VK_POLYGON_MODE_FILL;
rasterizationStateCI.cullMode = VK_CULL_MODE_NONE;
rasterizationStateCI.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
rasterizationStateCI.depthClampEnable = VK_FALSE;
rasterizationStateCI.rasterizerDiscardEnable = VK_FALSE;
rasterizationStateCI.depthBiasEnable = VK_FALSE;
rasterizationStateCI.lineWidth = 1.0f;
// 颜色混合状态描述混合因子如何计算(如果启用混合)
// 每个颜色附件都需要一个混合附件状态(即使不使用混合)
VkPipelineColorBlendAttachmentState blendAttachmentState{};
blendAttachmentState.colorWriteMask = 0xf;
blendAttachmentState.blendEnable = VK_FALSE;
VkPipelineColorBlendStateCreateInfo colorBlendStateCI{};
colorBlendStateCI.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
colorBlendStateCI.attachmentCount = 1;
colorBlendStateCI.pAttachments = &blendAttachmentState;
// 视口状态设置此管线中使用的视口和裁剪矩形数量
// 注意:该状态实际上会被动态状态覆盖(见下文)
VkPipelineViewportStateCreateInfo viewportStateCI{};
viewportStateCI.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewportStateCI.viewportCount = 1;
viewportStateCI.scissorCount = 1;
// 启用动态状态
// 大多数状态会固化到管线中,但仍有少量动态状态可以在命令缓冲中修改
// 为了修改这些状态,需要指定此管线将使用哪些动态状态;其实际值稍后会在命令缓冲中设置
// 本示例会通过动态状态设置视口和裁剪矩形
std::vector<VkDynamicState> dynamicStateEnables;
dynamicStateEnables.push_back(VK_DYNAMIC_STATE_VIEWPORT);
dynamicStateEnables.push_back(VK_DYNAMIC_STATE_SCISSOR);
VkPipelineDynamicStateCreateInfo dynamicStateCI{};
dynamicStateCI.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dynamicStateCI.pDynamicStates = dynamicStateEnables.data();
dynamicStateCI.dynamicStateCount = static_cast<uint32_t>(dynamicStateEnables.size());
// 深度与模板状态,包含深度/模板比较和测试操作
// 本示例只使用深度测试,并希望启用深度测试与深度写入,比较方式为小于等于
VkPipelineDepthStencilStateCreateInfo depthStencilStateCI{};
depthStencilStateCI.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
depthStencilStateCI.depthTestEnable = VK_TRUE;
depthStencilStateCI.depthWriteEnable = VK_TRUE;
depthStencilStateCI.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL;
depthStencilStateCI.depthBoundsTestEnable = VK_FALSE;
depthStencilStateCI.back.failOp = VK_STENCIL_OP_KEEP;
depthStencilStateCI.back.passOp = VK_STENCIL_OP_KEEP;
depthStencilStateCI.back.compareOp = VK_COMPARE_OP_ALWAYS;
depthStencilStateCI.stencilTestEnable = VK_FALSE;
depthStencilStateCI.front = depthStencilStateCI.back;
// 多重采样状态
// 本示例不使用多重采样(抗锯齿),但该状态仍必须设置并传递给管线
VkPipelineMultisampleStateCreateInfo multisampleStateCI{};
multisampleStateCI.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
multisampleStateCI.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
multisampleStateCI.pSampleMask = nullptr;
// 顶点输入描述
// 指定管线的顶点输入参数
// 顶点输入绑定
// 本示例在绑定点 0 使用单个顶点输入绑定(参见 vkCmdBindVertexBuffers)
VkVertexInputBindingDescription vertexInputBinding{};
vertexInputBinding.binding = 0;
vertexInputBinding.stride = sizeof(Vertex);
vertexInputBinding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
// 输入属性绑定描述着色器属性位置和内存布局
std::array<VkVertexInputAttributeDescription, 2> vertexInputAttributs{};
// 这些设置与下面的着色器布局匹配(见 triangle.vert):
// layout (location = 0) in vec3 inPos;
// layout (location = 1) in vec3 inColor;
// 属性位置 0:位置
vertexInputAttributs[0].binding = 0;
vertexInputAttributs[0].location = 0;
// 位置属性由三个 32 位有符号浮点数(SFLOAT)组成(R32 G32 B32)
vertexInputAttributs[0].format = VK_FORMAT_R32G32B32_SFLOAT;
vertexInputAttributs[0].offset = offsetof(Vertex, position);
// 属性位置 1:颜色
vertexInputAttributs[1].binding = 0;
vertexInputAttributs[1].location = 1;
// 颜色属性由三个 32 位有符号浮点数(SFLOAT)组成(R32 G32 B32)
vertexInputAttributs[1].format = VK_FORMAT_R32G32B32_SFLOAT;
vertexInputAttributs[1].offset = offsetof(Vertex, color);
// 用于创建管线的顶点输入状态
VkPipelineVertexInputStateCreateInfo vertexInputStateCI{};
vertexInputStateCI.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
vertexInputStateCI.vertexBindingDescriptionCount = 1;
vertexInputStateCI.pVertexBindingDescriptions = &vertexInputBinding;
vertexInputStateCI.vertexAttributeDescriptionCount = 2;
vertexInputStateCI.pVertexAttributeDescriptions = vertexInputAttributs.data();
// 着色器
std::array<VkPipelineShaderStageCreateInfo, 2> shaderStages{};
// 顶点着色器
shaderStages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
// 设置该着色器的管线阶段
shaderStages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
// 加载二进制 SPIR-V 着色器
shaderStages[0].module = loadSPIRVShader(getShadersPath() + "triangle/triangle.vert.spv");
// 着色器的主入口点
shaderStages[0].pName = "main";
assert(shaderStages[0].module != VK_NULL_HANDLE);
// 片元着色器
shaderStages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
// 设置该着色器的管线阶段
shaderStages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
// 加载二进制 SPIR-V 着色器
shaderStages[1].module = loadSPIRVShader(getShadersPath() + "triangle/triangle.frag.spv");
// 着色器的主入口点
shaderStages[1].pName = "main";
assert(shaderStages[1].module != VK_NULL_HANDLE);
// 设置管线着色器阶段信息
pipelineCI.stageCount = static_cast<uint32_t>(shaderStages.size());
pipelineCI.pStages = shaderStages.data();
// 将各管线状态赋值到管线创建信息结构中
pipelineCI.pVertexInputState = &vertexInputStateCI;
pipelineCI.pInputAssemblyState = &inputAssemblyStateCI;
pipelineCI.pRasterizationState = &rasterizationStateCI;
pipelineCI.pColorBlendState = &colorBlendStateCI;
pipelineCI.pMultisampleState = &multisampleStateCI;
pipelineCI.pViewportState = &viewportStateCI;
pipelineCI.pDepthStencilState = &depthStencilStateCI;
pipelineCI.pDynamicState = &dynamicStateCI;
// 使用指定状态创建渲染管线
VK_CHECK_RESULT(vkCreateGraphicsPipelines(device, pipelineCache, 1, &pipelineCI, nullptr, &pipeline));
// 图形管线创建完成后,不再需要着色器模块
vkDestroyShaderModule(device, shaderStages[0].module, nullptr);
vkDestroyShaderModule(device, shaderStages[1].module, nullptr);
}
void createUniformBuffers()
{
// 准备并初始化逐帧 uniform buffer 块,其中包含着色器 uniform 数据
// Vulkan 中不再存在 OpenGL 那样的单独 uniform;所有着色器 uniform 都通过 uniform buffer 块传递
VkMemoryRequirements memReqs;
// 顶点着色器 uniform buffer 块
VkBufferCreateInfo bufferInfo{};
VkMemoryAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.pNext = nullptr;
allocInfo.allocationSize = 0;
allocInfo.memoryTypeIndex = 0;
bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufferInfo.size = sizeof(ShaderData);
// 该缓冲区将作为 uniform buffer 使用
bufferInfo.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
// 创建缓冲区
for (uint32_t i = 0; i < MAX_CONCURRENT_FRAMES; i++) {
VK_CHECK_RESULT(vkCreateBuffer(device, &bufferInfo, nullptr, &uniformBuffers[i].buffer));
// 获取内存需求,包括大小、对齐和内存类型
vkGetBufferMemoryRequirements(device, uniformBuffers[i].buffer, &memReqs);
allocInfo.allocationSize = memReqs.size;
// 获取支持主机可见内存访问的内存类型索引
// 大多数实现提供多种内存类型,选择正确的内存类型进行分配非常关键
// 同时希望缓冲区具备主机一致性,这样每次更新后就不必刷新(或同步)
// 注意:这可能影响性能,因此在真实应用中频繁更新缓冲时未必希望这样做
allocInfo.memoryTypeIndex = getMemoryTypeIndex(memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
// 为 uniform buffer 分配内存
VK_CHECK_RESULT(vkAllocateMemory(device, &allocInfo, nullptr, &(uniformBuffers[i].memory)));
// 将内存绑定到缓冲区
VK_CHECK_RESULT(vkBindBufferMemory(device, uniformBuffers[i].buffer, uniformBuffers[i].memory, 0));
// 只映射一次缓冲区,之后即可无需再次映射而更新它
VK_CHECK_RESULT(vkMapMemory(device, uniformBuffers[i].memory, 0, sizeof(ShaderData), 0, (void**)&uniformBuffers[i].mapped));
}
}
void prepare() override
{
VulkanExampleBase::prepare();
createSynchronizationPrimitives();
createCommandBuffers();
createVertexBuffer();
createUniformBuffers();
createDescriptorSetLayout();
createDescriptorPool();
createDescriptorSets();
createPipelines();
prepared = true;
}
void render() override
{
if (!prepared)
return;
// 使用栅栏等待命令缓冲执行完成,然后再复用它
vkWaitForFences(device, 1, &waitFences[currentFrame], VK_TRUE, UINT64_MAX);
VK_CHECK_RESULT(vkResetFences(device, 1, &waitFences[currentFrame]));
// 从实现中获取下一张交换链图像
// 注意:实现可以按任意顺序返回图像,因此必须使用 acquire 函数,而不能自行循环使用 images/imageIndex
uint32_t imageIndex;
VkResult result = vkAcquireNextImageKHR(device, swapChain.swapChain, UINT64_MAX, presentCompleteSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex);
if (result == VK_ERROR_OUT_OF_DATE_KHR) {
windowResize();
return;
}
else if ((result != VK_SUCCESS) && (result != VK_SUBOPTIMAL_KHR)) {
throw "Could not acquire the next swap chain image!";
}
// 更新下一帧使用的 uniform buffer
ShaderData shaderData{};
shaderData.projectionMatrix = camera.matrices.perspective;
shaderData.viewMatrix = camera.matrices.view;
shaderData.modelMatrix = glm::mat4(1.0f);
// 将当前矩阵复制到当前帧的 uniform buffer
// 注意:由于 uniform buffer 请求的是主机一致性内存类型,因此写入会立即对 GPU 可见
memcpy(uniformBuffers[currentFrame].mapped, &shaderData, sizeof(ShaderData));
// 构建命令缓冲
// 与 OpenGL 不同,所有渲染命令都会记录到命令缓冲,然后提交到队列
// 这样可以提前在单独线程中生成工作负载
// 对于本示例这种基础命令缓冲,记录速度很快,因此无需将其转移到其他线程
vkResetCommandBuffer(commandBuffers[currentFrame], 0);
VkCommandBufferBeginInfo cmdBufInfo{};
cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
// 为所有 loadOp 设置为 clear 的帧缓冲附件设置清除值
// 这里使用两个附件(颜色和深度),它们会在子通道开始时被清除,因此需要分别设置清除值
VkClearValue clearValues[2]{};
clearValues[0].color = { { 0.0f, 0.0f, 0.2f, 1.0f } };
clearValues[1].depthStencil = { 1.0f, 0 };
VkRenderPassBeginInfo renderPassBeginInfo{};
renderPassBeginInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
renderPassBeginInfo.pNext = nullptr;
renderPassBeginInfo.renderPass = renderPass;
renderPassBeginInfo.renderArea.offset.x = 0;
renderPassBeginInfo.renderArea.offset.y = 0;
renderPassBeginInfo.renderArea.extent.width = width;
renderPassBeginInfo.renderArea.extent.height = height;
renderPassBeginInfo.clearValueCount = 2;
renderPassBeginInfo.pClearValues = clearValues;
renderPassBeginInfo.framebuffer = frameBuffers[imageIndex];
const VkCommandBuffer commandBuffer = commandBuffers[currentFrame];
VK_CHECK_RESULT(vkBeginCommandBuffer(commandBuffer, &cmdBufInfo));
// 开始基类默认 render pass 设置中指定的第一个子通道
// 这会清除颜色附件和深度附件
vkCmdBeginRenderPass(commandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
// 更新动态视口状态
VkViewport viewport{};
viewport.height = (float)height;
viewport.width = (float)width;
viewport.minDepth = (float)0.0f;
viewport.maxDepth = (float)1.0f;
vkCmdSetViewport(commandBuffer, 0, 1, &viewport);
// 更新动态裁剪矩形状态
VkRect2D scissor{};
scissor.extent.width = width;
scissor.extent.height = height;
scissor.offset.x = 0;
scissor.offset.y = 0;
vkCmdSetScissor(commandBuffer, 0, 1, &scissor);
// 绑定当前帧 uniform buffer 的描述符集,使着色器在本次绘制中使用该缓冲中的数据
vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &uniformBuffers[currentFrame].descriptorSet, 0, nullptr);
// 绑定渲染管线
// 管线(状态对象)包含渲染管线的全部状态;绑定它会设置管线创建时指定的所有状态
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
// 绑定三角形顶点缓冲(包含位置和颜色)
VkDeviceSize offsets[1]{ 0 };
vkCmdBindVertexBuffers(commandBuffer, 0, 1, &vertices.buffer, offsets);
// 绑定三角形索引缓冲
vkCmdBindIndexBuffer(commandBuffer, indices.buffer, 0, VK_INDEX_TYPE_UINT32);
// 绘制索引三角形
vkCmdDrawIndexed(commandBuffer, indices.count, 1, 0, 0, 0);
vkCmdEndRenderPass(commandBuffer);
// 结束 render pass 会添加一个隐式屏障,将帧缓冲颜色附件转换为
// VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,以便将其呈现到窗口系统
VK_CHECK_RESULT(vkEndCommandBuffer(commandBuffer));
// 将命令缓冲提交到图形队列
// 队列提交将等待的管线阶段(通过 pWaitSemaphores 指定)
VkPipelineStageFlags waitStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
// submitInfo 结构指定一次命令缓冲队列提交批次
VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.pWaitDstStageMask = &waitStageMask; // 指向发生信号量等待的管线阶段列表
submitInfo.pCommandBuffers = &commandBuffer; // 本批次(提交)中要执行的命令缓冲
submitInfo.commandBufferCount = 1; // 这里提交单个命令缓冲
// 提交的命令缓冲开始执行前需要等待的信号量
submitInfo.pWaitSemaphores = &presentCompleteSemaphores[currentFrame];
submitInfo.waitSemaphoreCount = 1;
// 命令缓冲完成后需要发出信号的信号量
submitInfo.pSignalSemaphores = &renderCompleteSemaphores[imageIndex];
submitInfo.signalSemaphoreCount = 1;
// 带等待栅栏提交到图形队列
VK_CHECK_RESULT(vkQueueSubmit(queue, 1, &submitInfo, waitFences[currentFrame]));
// 将当前帧缓冲呈现到交换链
// 将命令缓冲提交产生信号的信号量作为交换链呈现的等待信号量
// 这确保所有命令提交完成前,图像不会呈现到窗口系统
VkPresentInfoKHR presentInfo{};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores = &renderCompleteSemaphores[imageIndex];
presentInfo.swapchainCount = 1;
presentInfo.pSwapchains = &swapChain.swapChain;
presentInfo.pImageIndices = &imageIndex;
result = vkQueuePresentKHR(queue, &presentInfo);
if ((result == VK_ERROR_OUT_OF_DATE_KHR) || (result == VK_SUBOPTIMAL_KHR)) {
windowResize();
}
else if (result != VK_SUCCESS) {
throw "Could not present the image to the swap chain!";
}
// 根据最大并发帧数选择下一帧进行渲染
currentFrame = (currentFrame + 1) % MAX_CONCURRENT_FRAMES;
}
};
// 操作系统相关的主入口点
// 大部分代码在不同受支持操作系统之间共享,但消息处理等部分存在差异
#if defined(_WIN32)
// Windows 入口点
VulkanExample *vulkanExample;
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
if (vulkanExample != NULL)
{
vulkanExample->handleMessages(hWnd, uMsg, wParam, lParam);
}
return (DefWindowProc(hWnd, uMsg, wParam, lParam));
}
int APIENTRY WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR, _In_ int)
{
for (size_t i = 0; i < __argc; i++) { VulkanExample::args.push_back(__argv[i]); };
vulkanExample = new VulkanExample();
vulkanExample->initVulkan();
vulkanExample->setupWindow(hInstance, WndProc);
vulkanExample->prepare();
vulkanExample->renderLoop();
delete(vulkanExample);
return 0;
}
#elif defined(__ANDROID__)
// Android 入口点
VulkanExample *vulkanExample;
void android_main(android_app* state)
{
vulkanExample = new VulkanExample();
state->userData = vulkanExample;
state->onAppCmd = VulkanExample::handleAppCommand;
state->onInputEvent = VulkanExample::handleAppInput;
androidApp = state;
vulkanExample->renderLoop();
delete(vulkanExample);
}
#elif defined(_DIRECT2DISPLAY)
// Linux direct-to-display WSI 入口点
// Direct-to-Display(D2D)用于嵌入式平台
VulkanExample *vulkanExample;
static void handleEvent()
{
}
int main(const int argc, const char *argv[])
{
for (size_t i = 0; i < argc; i++) { VulkanExample::args.push_back(argv[i]); };
vulkanExample = new VulkanExample();
vulkanExample->initVulkan();
vulkanExample->prepare();
vulkanExample->renderLoop();
delete(vulkanExample);
return 0;
}
#elif defined(VK_USE_PLATFORM_DIRECTFB_EXT)
VulkanExample *vulkanExample;
static void handleEvent(const DFBWindowEvent *event)
{
if (vulkanExample != NULL)
{
vulkanExample->handleEvent(event);
}
}
int main(const int argc, const char *argv[])
{
for (size_t i = 0; i < argc; i++) { VulkanExample::args.push_back(argv[i]); };
vulkanExample = new VulkanExample();
vulkanExample->initVulkan();
vulkanExample->setupWindow();
vulkanExample->prepare();
vulkanExample->renderLoop();
delete(vulkanExample);
return 0;
}
#elif defined(VK_USE_PLATFORM_WAYLAND_KHR)
VulkanExample *vulkanExample;
int main(const int argc, const char *argv[])
{
for (size_t i = 0; i < argc; i++) { VulkanExample::args.push_back(argv[i]); };
vulkanExample = new VulkanExample();
vulkanExample->initVulkan();
vulkanExample->setupWindow();
vulkanExample->prepare();
vulkanExample->renderLoop();
delete(vulkanExample);
return 0;
}
#elif defined(__linux__) || defined(__FreeBSD__)
// Linux 入口点
VulkanExample *vulkanExample;
#if defined(VK_USE_PLATFORM_XCB_KHR)
static void handleEvent(const xcb_generic_event_t *event)
{
if (vulkanExample != NULL)
{
vulkanExample->handleEvent(event);
}
}
#else
static void handleEvent()
{
}
#endif
int main(const int argc, const char *argv[])
{
for (size_t i = 0; i < argc; i++) { VulkanExample::args.push_back(argv[i]); };
vulkanExample = new VulkanExample();
vulkanExample->initVulkan();
vulkanExample->setupWindow();
vulkanExample->prepare();
vulkanExample->renderLoop();
delete(vulkanExample);
return 0;
}
#elif (defined(VK_USE_PLATFORM_MACOS_MVK) || defined(VK_USE_PLATFORM_METAL_EXT)) && defined(VK_EXAMPLE_XCODE_GENERATED)
VulkanExample *vulkanExample;
int main(const int argc, const char *argv[])
{
@autoreleasepool
{
for (size_t i = 0; i < argc; i++) { VulkanExample::args.push_back(argv[i]); };
vulkanExample = new VulkanExample();
vulkanExample->initVulkan();
vulkanExample->setupWindow(nullptr);
vulkanExample->prepare();
vulkanExample->renderLoop();
delete(vulkanExample);
}
return 0;
}
#elif defined(VK_USE_PLATFORM_SCREEN_QNX)
VULKAN_EXAMPLE_MAIN()
#endif
这份代码虽然只画了一个三角形,但它已经覆盖了 Vulkan 图形渲染中最关键的概念:
- swapchain:窗口系统可显示的图像队列。
- framebuffer:把 swapchain image 和 depth image 组合成一次 render pass 的目标。
- render pass:描述本次渲染会写哪些 attachment,以及它们的布局转换。
- graphics pipeline:把 shader、顶点布局、光栅化、深度测试、混合等状态固定下来。
- vertex/index buffer:提供三角形几何数据。
- uniform buffer:给 vertex shader 提供投影、视图、模型矩阵。
- command buffer:记录 Vulkan 绘制命令。
- semaphore/fence:协调 CPU、GPU、swapchain image acquire、render、present 的时序。
1. 程序入口:从窗口到 render loop
Windows 入口在文件末尾:
int APIENTRY WinMain(...)
{
vulkanExample = new VulkanExample();
vulkanExample->initVulkan();
vulkanExample->setupWindow(hInstance, WndProc);
vulkanExample->prepare();
vulkanExample->renderLoop();
delete(vulkanExample);
return 0;
}
这几步可以理解为:
new VulkanExample():创建示例对象,设置标题、相机和一些示例配置。initVulkan():在基类中创建 Vulkan instance、选择 physical device、创建 logical device、队列等基础 Vulkan 对象。setupWindow(...):创建平台窗口。prepare():创建三角形渲染需要的所有资源。renderLoop():进入消息循环,每帧调用render()。
基类 VulkanExampleBase::renderLoop() 会处理窗口消息,然后调用 nextFrame();nextFrame() 内部调用虚函数 render()。由于 triangle.cpp 里的 VulkanExample 重写了 render(),所以每帧真正执行的是本示例自己的渲染逻辑。
2. VulkanExample 类维护了哪些资源
示例类定义在:
class VulkanExample : public VulkanExampleBase
它继承了基类中的 Vulkan 基础对象,比如 device、queue、swapChain、renderPass、frameBuffers、width/height 等。同时它自己维护了三角形示例专用的资源。
2.1 顶点格式
struct Vertex {
float position[3];
float color[3];
};
每个顶点包含两个属性:
position:三维位置。color:顶点颜色。
后面创建 graphics pipeline 时,会把这个 C++ 结构映射到 shader 的输入:
layout (location = 0) in vec3 inPos;
layout (location = 1) in vec3 inColor;
也就是说,position 对应 shader 的 location = 0,color 对应 shader 的 location = 1。
2.2 顶点缓冲和索引缓冲
struct {
VkDeviceMemory memory;
VkBuffer buffer;
} vertices;
struct {
VkDeviceMemory memory;
VkBuffer buffer;
uint32_t count;
} indices;
vertices.buffer 存三角形顶点数据,indices.buffer 存索引数据。后续绘制时通过:
vkCmdBindVertexBuffers(...);
vkCmdBindIndexBuffer(...);
vkCmdDrawIndexed(...);
告诉 GPU 用这些数据组装三角形。
2.3 Uniform Buffer
struct ShaderData {
glm::mat4 projectionMatrix;
glm::mat4 modelMatrix;
glm::mat4 viewMatrix;
};
这个结构和 vertex shader 中的 uniform block 对应:
layout (binding = 0) uniform UBO
{
mat4 projectionMatrix;
mat4 modelMatrix;
mat4 viewMatrix;
} ubo;
它负责把相机矩阵传给 shader。vertex shader 会用这些矩阵把模型坐标变换到裁剪空间:
gl_Position = ubo.projectionMatrix * ubo.viewMatrix * ubo.modelMatrix * vec4(inPos.xyz, 1.0);
示例为每个并发帧准备一个 uniform buffer:
std::array<UniformBuffer, MAX_CONCURRENT_FRAMES> uniformBuffers;
这样 CPU 更新当前帧 uniform buffer 时,不会覆盖 GPU 仍在使用的上一帧数据。
2.4 Pipeline、Descriptor 和同步对象
示例还维护:
VkPipelineLayout pipelineLayout;
VkPipeline pipeline;
VkDescriptorSetLayout descriptorSetLayout;
其中:
descriptorSetLayout描述 shader 需要哪些资源。这里就是binding = 0的 uniform buffer。pipelineLayout把 descriptor set layout 放进 pipeline 接口。pipeline是最终用于绘制三角形的 graphics pipeline。
同步对象包括:
std::vector<VkSemaphore> presentCompleteSemaphores;
std::vector<VkSemaphore> renderCompleteSemaphores;
std::array<VkFence, MAX_CONCURRENT_FRAMES> waitFences;
它们分别用于:
- 等 swapchain image acquire 完成。
- 等渲染命令执行完成后再 present。
- 等某一帧的 command buffer 执行完成后,CPU 才复用它。
3. prepare:渲染前的资源准备
本示例的 prepare() 如下:
void prepare() override
{
VulkanExampleBase::prepare();
createSynchronizationPrimitives();
createCommandBuffers();
createVertexBuffer();
createUniformBuffers();
createDescriptorSetLayout();
createDescriptorPool();
createDescriptorSets();
createPipelines();
prepared = true;
}
这里有一个容易忽略的点:VulkanExampleBase::prepare() 会创建 surface、swapchain、基类默认 command buffer、基类默认同步对象,并调用虚函数创建 depth stencil、render pass、framebuffer。
在本示例中:
setupDepthStencil()是重写的,所以基类prepare()调用的是本文件的版本。setupRenderPass()是重写的,所以 render pass 也是本文件定义的。setupFrameBuffer()是重写的,所以 framebuffer 也是本文件定义的。createCommandBuffers()和createSynchronizationPrimitives()在基类中不是虚函数,所以本示例在自己的prepare()中又创建了一套自己使用的 command buffer 和同步对象。
这样做的原因是这个例子想尽量展示完整的 Vulkan acquire、record、submit、present 流程,而不是依赖基类封装好的绘制路径。
4. 创建三角形数据:createVertexBuffer
三角形的三个顶点定义在 createVertexBuffer() 中:
std::vector<Vertex> vertexBuffer{
{ { 1.0f, 1.0f, 0.0f }, { 1.0f, 0.0f, 0.0f } },
{ { -1.0f, 1.0f, 0.0f }, { 0.0f, 1.0f, 0.0f } },
{ { 0.0f, -1.0f, 0.0f }, { 0.0f, 0.0f, 1.0f } }
};
这三个点大致形成一个倒三角:
- 右上角:红色。
- 左上角:绿色。
- 下方中间:蓝色。
索引数据是:
std::vector<uint32_t> indexBuffer{ 0, 1, 2 };
indices.count = static_cast<uint32_t>(indexBuffer.size());
这表示按照顶点 0 -> 1 -> 2 画一个三角形。
4.1 为什么要用 staging buffer
代码没有直接把数据放进最终的 GPU buffer,而是用了 staging buffer:
- 创建 CPU 可见的 staging buffer。
vkMapMemory()把 staging buffer 映射到 CPU 地址空间。memcpy()把顶点和索引数据拷进去。- 创建 GPU device local buffer。
- 用
vkCmdCopyBuffer()从 staging buffer 拷到 GPU buffer。 - 等 copy 命令完成。
- 销毁 staging buffer。
这么做是为了让最终绘制用的 buffer 位于 VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT 内存中。它通常对 GPU 访问更快,但 CPU 不能直接写入,所以先借助 staging buffer 中转。
5. Render Pass 和 Framebuffer
本示例重写了 setupRenderPass()。它定义了两个 attachment:
std::array<VkAttachmentDescription, 2> attachments{};
第一个 attachment 是 color attachment:
attachments[0].format = swapChain.colorFormat;
attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachments[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
attachments[0].finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
关键点:
loadOp = CLEAR:render pass 开始时清屏。storeOp = STORE:render pass 结束后保留颜色结果,因为要显示到窗口。finalLayout = PRESENT_SRC_KHR:最终转成可 present 的布局。
第二个 attachment 是 depth attachment:
attachments[1].format = depthFormat;
attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachments[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
深度图只在本次渲染中使用,渲染结束后不需要显示,所以 storeOp 是 DONT_CARE。
setupFrameBuffer() 为 swapchain 的每张 image 创建一个 framebuffer:
attachments[0] = swapChain.imageViews[i];
attachments[1] = depthStencil.view;
也就是说,每个 framebuffer 包含:
- 当前 swapchain image 的 image view,作为颜色输出。
- 共用的 depth stencil image view,作为深度输出。
当某一帧 acquire 到 imageIndex 后,渲染时就使用:
renderPassBeginInfo.framebuffer = frameBuffers[imageIndex];
这样 GPU 写入的就是当前要显示的 swapchain image。
6. Descriptor:把 Uniform Buffer 连接到 Shader
shader 中有:
layout (binding = 0) uniform UBO { ... } ubo;
所以 C++ 侧需要创建 descriptor set layout:
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
layoutBinding.descriptorCount = 1;
layoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
这说明:
- binding 0 是一个 uniform buffer。
- 这个 uniform buffer 被 vertex shader 使用。
然后 createDescriptorSets() 为每个并发帧分配一个 descriptor set,并把对应帧的 uniform buffer 写进去:
bufferInfo.buffer = uniformBuffers[i].buffer;
bufferInfo.range = sizeof(ShaderData);
writeDescriptorSet.dstSet = uniformBuffers[i].descriptorSet;
writeDescriptorSet.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
writeDescriptorSet.pBufferInfo = &bufferInfo;
writeDescriptorSet.dstBinding = 0;
vkUpdateDescriptorSets(device, 1, &writeDescriptorSet, 0, nullptr);
每帧绘制时,代码绑定当前帧的 descriptor set:
vkCmdBindDescriptorSets(
commandBuffer,
VK_PIPELINE_BIND_POINT_GRAPHICS,
pipelineLayout,
0,
1,
&uniformBuffers[currentFrame].descriptorSet,
0,
nullptr);
这样 shader 使用的就是当前帧对应的矩阵数据。
7. Graphics Pipeline:固定一次绘制所需的大部分状态
Vulkan 的 pipeline 可以理解为一次绘制的完整状态包。createPipelines() 里创建了本示例唯一的 graphics pipeline。
7.1 Pipeline Layout
pipelineLayoutCI.setLayoutCount = 1;
pipelineLayoutCI.pSetLayouts = &descriptorSetLayout;
vkCreatePipelineLayout(device, &pipelineLayoutCI, nullptr, &pipelineLayout);
pipeline layout 定义了 shader 能访问哪些 descriptor set。这里就是前面创建的 uniform buffer layout。
7.2 Input Assembly
inputAssemblyStateCI.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
这表示 GPU 按三角形列表解释输入顶点。配合索引 {0, 1, 2},最终只生成一个三角形。
7.3 Rasterization
rasterizationStateCI.polygonMode = VK_POLYGON_MODE_FILL;
rasterizationStateCI.cullMode = VK_CULL_MODE_NONE;
rasterizationStateCI.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
这里的设置表示:
- 用填充模式画面,而不是线框。
- 不做背面剔除。
- 逆时针为正面,但由于不剔除,所以这个三角形无论正反都能画出来。
7.4 Depth 和 Blend
depthStencilStateCI.depthTestEnable = VK_TRUE;
depthStencilStateCI.depthWriteEnable = VK_TRUE;
depthStencilStateCI.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL;
虽然只有一个三角形,但示例仍启用了深度测试和深度写入。这是为了展示完整的常规 3D pipeline 配置。
颜色混合关闭:
blendAttachmentState.blendEnable = VK_FALSE;
fragment shader 输出什么颜色,color attachment 就写什么颜色。
7.5 Viewport 和 Scissor 是动态状态
代码把 viewport 和 scissor 加入 dynamic state:
dynamicStateEnables.push_back(VK_DYNAMIC_STATE_VIEWPORT);
dynamicStateEnables.push_back(VK_DYNAMIC_STATE_SCISSOR);
这表示 pipeline 创建时不用固定窗口大小。每帧录制 command buffer 时再设置:
vkCmdSetViewport(commandBuffer, 0, 1, &viewport);
vkCmdSetScissor(commandBuffer, 0, 1, &scissor);
窗口大小变化时更方便。
7.6 顶点输入布局
VkVertexInputBindingDescription vertexInputBinding{};
vertexInputBinding.binding = 0;
vertexInputBinding.stride = sizeof(Vertex);
vertexInputBinding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
这说明 binding 0 是按顶点读取,每个顶点大小是 sizeof(Vertex)。
两个 attribute:
vertexInputAttributs[0].location = 0;
vertexInputAttributs[0].format = VK_FORMAT_R32G32B32_SFLOAT;
vertexInputAttributs[0].offset = offsetof(Vertex, position);
vertexInputAttributs[1].location = 1;
vertexInputAttributs[1].format = VK_FORMAT_R32G32B32_SFLOAT;
vertexInputAttributs[1].offset = offsetof(Vertex, color);
这把 C++ 里的 Vertex 布局和 shader 输入绑定起来:
layout (location = 0) in vec3 inPos;
layout (location = 1) in vec3 inColor;
7.7 Shader Stage
shaderStages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
shaderStages[0].module = loadSPIRVShader(getShadersPath() + "triangle/triangle.vert.spv");
shaderStages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
shaderStages[1].module = loadSPIRVShader(getShadersPath() + "triangle/triangle.frag.spv");
Vulkan pipeline 使用的是 SPIR-V 二进制 shader。loadSPIRVShader() 会读取 .spv 文件,然后调用 vkCreateShaderModule() 创建 shader module。
最后:
vkCreateGraphicsPipelines(device, pipelineCache, 1, &pipelineCI, nullptr, &pipeline);
到这里,一个能画三角形的 graphics pipeline 就创建好了。
8. Shader:顶点如何变成颜色
顶点 shader:
layout (location = 0) in vec3 inPos;
layout (location = 1) in vec3 inColor;
layout (binding = 0) uniform UBO
{
mat4 projectionMatrix;
mat4 modelMatrix;
mat4 viewMatrix;
} ubo;
layout (location = 0) out vec3 outColor;
void main()
{
outColor = inColor;
gl_Position = ubo.projectionMatrix * ubo.viewMatrix * ubo.modelMatrix * vec4(inPos.xyz, 1.0);
}
它做了两件事:
- 把顶点颜色传给 fragment shader。
- 把顶点位置乘以
projection * view * model,输出到gl_Position。
fragment shader:
layout (location = 0) in vec3 inColor;
layout (location = 0) out vec4 outFragColor;
void main()
{
outFragColor = vec4(inColor, 1.0);
}
fragment shader 直接输出插值后的顶点颜色。由于三个顶点分别是红、绿、蓝,三角形内部会出现平滑渐变。
9. 每帧渲染:render 函数的完整流程
render() 是这份代码最核心的函数。它每帧完成:
- 等上一轮使用的 command buffer 执行完。
- 从 swapchain 获取下一张 image。
- 更新当前帧 uniform buffer。
- 录制 command buffer。
- 提交 command buffer 到 graphics queue。
- 把渲染完成的 image present 到窗口。
9.1 等待当前帧资源可复用
vkWaitForFences(device, 1, &waitFences[currentFrame], VK_TRUE, UINT64_MAX);
vkResetFences(device, 1, &waitFences[currentFrame]);
currentFrame 在 0 和 1 之间循环,因为:
constexpr auto MAX_CONCURRENT_FRAMES = 2;
这允许 CPU 最多提前准备两个 frame。复用某个 frame 的 command buffer 和 uniform buffer 前,需要确认 GPU 已经不用它了。
9.2 获取 swapchain image
uint32_t imageIndex;
VkResult result = vkAcquireNextImageKHR(
device,
swapChain.swapChain,
UINT64_MAX,
presentCompleteSemaphores[currentFrame],
VK_NULL_HANDLE,
&imageIndex);
imageIndex 是本帧要写入的 swapchain image 下标。
presentCompleteSemaphores[currentFrame] 会在 image 可用时 signal。后面提交绘制命令时会等待这个 semaphore,确保 GPU 不会在 image 还不可用时就开始写它。
9.3 更新矩阵 Uniform Buffer
ShaderData shaderData{};
shaderData.projectionMatrix = camera.matrices.perspective;
shaderData.viewMatrix = camera.matrices.view;
shaderData.modelMatrix = glm::mat4(1.0f);
memcpy(uniformBuffers[currentFrame].mapped, &shaderData, sizeof(ShaderData));
这里每帧把相机矩阵写入当前帧的 uniform buffer。因为创建 uniform buffer 时选择了:
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
所以 CPU 写入后对 GPU 可见,不需要额外 flush。
9.4 开始录制 command buffer
vkResetCommandBuffer(commandBuffers[currentFrame], 0);
vkBeginCommandBuffer(commandBuffer, &cmdBufInfo);
Vulkan 不是立即执行 draw,而是先把命令录进 command buffer。录完后再提交给 queue。
9.5 开始 Render Pass
VkClearValue clearValues[2]{};
clearValues[0].color = { { 0.0f, 0.0f, 0.2f, 1.0f } };
clearValues[1].depthStencil = { 1.0f, 0 };
颜色 attachment 会被清成深蓝色,深度 attachment 会被清成 1.0。
renderPassBeginInfo.renderPass = renderPass;
renderPassBeginInfo.framebuffer = frameBuffers[imageIndex];
renderPassBeginInfo.renderArea.extent.width = width;
renderPassBeginInfo.renderArea.extent.height = height;
renderPassBeginInfo.pClearValues = clearValues;
vkCmdBeginRenderPass(commandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
这里的 frameBuffers[imageIndex] 很关键:它让本帧渲染命令写入刚刚从 swapchain acquire 到的 image。
9.6 设置 viewport 和 scissor
VkViewport viewport{};
viewport.width = (float)width;
viewport.height = (float)height;
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
vkCmdSetViewport(commandBuffer, 0, 1, &viewport);
VkRect2D scissor{};
scissor.extent.width = width;
scissor.extent.height = height;
vkCmdSetScissor(commandBuffer, 0, 1, &scissor);
这决定了渲染结果映射到窗口的哪个区域。本例覆盖整个窗口。
9.7 绑定资源并发出 draw call
vkCmdBindDescriptorSets(...);
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
vkCmdBindVertexBuffers(commandBuffer, 0, 1, &vertices.buffer, offsets);
vkCmdBindIndexBuffer(commandBuffer, indices.buffer, 0, VK_INDEX_TYPE_UINT32);
vkCmdDrawIndexed(commandBuffer, indices.count, 1, 0, 0, 0);
这一段就是三角形真正被绘制出来的地方。
绑定顺序可以理解为:
vkCmdBindDescriptorSets:告诉 shader 用哪个 uniform buffer。vkCmdBindPipeline:告诉 GPU 使用哪套图形管线和 shader。vkCmdBindVertexBuffers:告诉 GPU 从哪里读顶点位置和颜色。vkCmdBindIndexBuffer:告诉 GPU 从哪里读索引。vkCmdDrawIndexed:真正发出绘制命令。
因为 indices.count == 3,所以这次 draw call 只画一个三角形。
9.8 结束 Render Pass 和 Command Buffer
vkCmdEndRenderPass(commandBuffer);
vkEndCommandBuffer(commandBuffer);
render pass 结束时,color attachment 会根据 render pass 中的配置转换到:
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
也就是可用于 present 的布局。
9.9 提交到 Graphics Queue
VkPipelineStageFlags waitStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
submitInfo.pWaitSemaphores = &presentCompleteSemaphores[currentFrame];
submitInfo.waitSemaphoreCount = 1;
submitInfo.pSignalSemaphores = &renderCompleteSemaphores[imageIndex];
submitInfo.signalSemaphoreCount = 1;
submitInfo.pCommandBuffers = &commandBuffer;
vkQueueSubmit(queue, 1, &submitInfo, waitFences[currentFrame]);
这里有两个重要同步点:
- 等
presentCompleteSemaphores[currentFrame]:确保 swapchain image 已经可写。 - signal
renderCompleteSemaphores[imageIndex]:告诉后面的 present,本帧渲染已经完成。
waitFences[currentFrame] 会在 GPU 执行完这次提交后 signal。下一次复用这个 currentFrame 时,CPU 会等待这个 fence。
9.10 Present 到窗口
presentInfo.pWaitSemaphores = &renderCompleteSemaphores[imageIndex];
presentInfo.pSwapchains = &swapChain.swapChain;
presentInfo.pImageIndices = &imageIndex;
vkQueuePresentKHR(queue, &presentInfo);
present 阶段等待 renderCompleteSemaphores[imageIndex],确保 image 已经渲染完成,然后把这张 swapchain image 交给窗口系统显示。
最后:
currentFrame = (currentFrame + 1) % MAX_CONCURRENT_FRAMES;
切换到下一套 per-frame 资源。
10. 一帧渲染的数据流总结
可以把整个过程简化成下面这条链路:
CPU 创建顶点/索引数据
|
v
staging buffer
|
v
GPU vertex buffer / index buffer
|
v
command buffer 绑定 pipeline + descriptor + vertex/index buffer
|
v
vkCmdDrawIndexed
|
v
vertex shader:位置乘 MVP,颜色传递
|
v
rasterizer:三角形变成片元,颜色插值
|
v
fragment shader:输出颜色
|
v
color attachment,也就是当前 swapchain image
|
v
vkQueuePresentKHR 显示到窗口
11. 从代码角度看,真正让三角形出现的最小闭环
如果只看“为什么窗口上出现了三角形”,最关键的是这几个条件同时成立:
createVertexBuffer()创建了 3 个顶点和 3 个索引。createPipelines()设置了三角形列表拓扑、顶点输入布局和 vertex/fragment shader。createDescriptorSets()把 uniform buffer 绑定给 vertex shader。render()acquire 到 swapchain image,并选择对应 framebuffer。- command buffer 中绑定 pipeline、descriptor、vertex buffer、index buffer。
vkCmdDrawIndexed(..., indices.count, ...)发出绘制命令。vkQueueSubmit()执行命令。vkQueuePresentKHR()把渲染结果显示到窗口。
三角形本身来自顶点缓冲,颜色来自顶点颜色经过 shader 插值,窗口显示来自 swapchain present。
12. 这个例子适合学习什么
这份 triangle.cpp 的价值不在于图形复杂,而在于它把 Vulkan 的基础图形渲染路径摊开了:
- 如何把 CPU 数据上传到 GPU buffer。
- 如何描述 shader 需要的 uniform buffer。
- 如何配置 graphics pipeline。
- 如何定义 render pass 和 framebuffer。
- 如何每帧 acquire swapchain image。
- 如何录制 command buffer。
- 如何同步 acquire、render、present。
如果后续要扩展这个例子,可以按下面顺序继续学习:
- 修改顶点数据,画更多三角形。
- 修改 fragment shader,输出固定颜色、渐变或简单光照。
- 添加纹理,学习 sampled image 和 sampler descriptor。
- 添加多个 uniform 或 push constants。
- 添加模型加载,把 vertex/index buffer 换成真实 mesh。
理解这个例子后,再看 Vulkan 中的模型渲染、纹理采样、深度测试、多 pipeline、多 descriptor set,会容易很多。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)