Vulkan学习笔记6—渲染呈现

一、渲染循环核心

    while (!glfwWindowShouldClose(window)) {
        glfwPollEvents();
        helloTriangleApp.drawFrame(); // 绘制帧
    }

在 Vulkan 中渲染帧包含一组常见的步骤

  • 等待前一帧完成(vkWaitForFences)

  • 从交换链获取图像(vkAcquireNextImageKHR)

  • 录制一个命令缓冲区将场景绘制到图像上(vkBeginCommandBuffer、vkEndCommandBuffer)

  • 提交已记录的命令缓冲区(vkQueueSubmit)

  • 呈现交换链图像(vkQueuePresentKHR)

这就是一个drawFrame函数要做的主要工作。

二、同步

GPU执行需显式同步。

例如下面这些事件:

  • 从交换链获取图像(vkAcquireNextImageKHR)

  • 执行在获取的图像上绘制的命令

  • 将该图像呈现到屏幕上进行呈现,将其返回到交换链(vkQueuePresentKHR)

信号量

用于控制GPU上的同步操作。

VkCommandBuffer A, B = ... // 录制命令缓冲
VkSemaphore S = ... // 创建一个信号

// 当操作 A 完成时,将发出信号量 S 的信号,而操作 B 将不会启动,直到 S 发出信号
vkQueueSubmit(work: A, signal: S, wait: None)

// 在操作 B 开始执行后,信号量 S 将自动重置回未发出信号的状态,从而允许再次使用它。
vkQueueSubmit(work: B, signal: None, wait: S)

栅栏

它是用于对 CPU(也称为主机)上的执行进行排序的。如果主机需要知道 GPU 何时完成某件事,我们会使用栅栏。

例如:截屏操作

VkCommandBuffer A = ... // 记录包含传输操作的命令缓冲区
VkFence F = ... // 创建围栏对象

// 将命令缓冲区A提交到队列,立即开始执行,并在完成时发出围栏F的信号
vkQueueSubmit(work: A, fence: F)

vkWaitForFence(F) // 阻塞当前执行线程,直到命令缓冲区A完成执行

save_screenshot_to_disk() // 必须等待传输操作完成后才能执行

三、绘制过程

创建同步对象

void HelloTriangle::createSyncObjects() {
    // 为每一帧预分配同步对象的存储
    imageAvailableSemaphores.resize(MAX_CONCURRENT_FRAMES);
    renderFinishedSemaphores.resize(MAX_CONCURRENT_FRAMES);
    inFlightFences.resize(MAX_CONCURRENT_FRAMES);

    // 设置信号量创建信息结构体
    VkSemaphoreCreateInfo semaphoreInfo{};
    semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
    
    // 设置围栏创建信息结构体,初始化为已信号化状态
    VkFenceCreateInfo fenceInfo{};
    fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
    fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;  // 初始状态设为已信号化,允许第一帧立即执行
    
    // 为每一个并发帧创建一组同步对象
    for (size_t i = 0; i < MAX_CONCURRENT_FRAMES; i++) {
        if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAvailableSemaphores[i]) != VK_SUCCESS
            || vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderFinishedSemaphores[i]) != VK_SUCCESS
            || vkCreateFence(device, &fenceInfo, nullptr, &inFlightFences[i]) != VK_SUCCESS) {
            throw std::runtime_error("Failed to create synchronization objects for a frame!");
        }
    }
}

录制命令

void HelloTriangle::recordCommandBuffer(VkCommandBuffer commandBuffer, uint32_t imageIndex) {
    // 1. 开始记录命令缓冲区
    VkCommandBufferBeginInfo beginInfo{};
    beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;

    if (vkBeginCommandBuffer(commandBuffer, &beginInfo) != VK_SUCCESS) {
        throw std::runtime_error("Failed to begin recording command buffer!");
    }

    // 2. 设置渲染通道信息
    VkRenderPassBeginInfo renderPassInfo{};
    renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
    renderPassInfo.renderPass = renderPass;  // 指定使用的渲染通道
    renderPassInfo.framebuffer = swapChainFramebuffers[imageIndex];  // 指定目标帧缓冲
    renderPassInfo.renderArea.offset = {0, 0};  // 渲染区域起点
    renderPassInfo.renderArea.extent = swapChainExtent;  // 渲染区域大小

    // 设置清除颜色 (黑色)
    VkClearValue clearColor = {{{0.0f, 0.0f, 0.0f, 1.0f}}};
    renderPassInfo.clearValueCount = 1;
    renderPassInfo.pClearValues = &clearColor;

    // 3. 开始渲染通道,使用内联子通道内容
    vkCmdBeginRenderPass(commandBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);
    
    // 4. 绑定图形管线
    vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);

    // 5. 设置视口 (Viewport) - 定义裁剪空间坐标到帧缓冲坐标的映射
    VkViewport viewport{};
    viewport.x = 0.0f;
    viewport.y = 0.0f;
    viewport.width = static_cast<float>(swapChainExtent.width);
    viewport.height = static_cast<float>(swapChainExtent.height);
    viewport.minDepth = 0.0f;
    viewport.maxDepth = 1.0f;
    vkCmdSetViewport(commandBuffer, 0, 1, &viewport);

    // 6. 设置剪刀区域 (Scissor) - 定义实际渲染的区域
    VkRect2D scissor{};
    scissor.offset = {0, 0};
    scissor.extent = swapChainExtent;
    vkCmdSetScissor(commandBuffer, 0, 1, &scissor);

    // 7. 执行绘制命令 - 绘制3个顶点,组成一个三角形
    vkCmdDraw(commandBuffer, 3, 1, 0, 0);

    // 8. 结束渲染通道
    vkCmdEndRenderPass(commandBuffer);

    // 9. 结束命令缓冲区记录
    if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) {
        throw std::runtime_error("Failed to record command buffer!");
    }
}

绘制帧

void HelloTriangle::drawFrame() {
    // 1. 等待当前帧的围栏被信号化,确保上一帧的渲染操作已完成
    vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX);
    // 重置围栏状态,为下一帧做准备
    vkResetFences(device, 1, &inFlightFences[currentFrame]);

    // 2. 从交换链获取下一可用图像,使用当前帧的"图像可用"信号量
    uint32_t imageIndex;
    vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex);
    
    // 3. 重置并记录当前帧的命令缓冲区
    vkResetCommandBuffer(commandBuffers[currentFrame],  0);
    recordCommandBuffer(commandBuffers[currentFrame], imageIndex);

    // 4. 设置提交信息,定义命令执行的依赖关系
    VkSubmitInfo submitInfo{};
    submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;

    // 等待"图像可用"信号量,确保在图像可用后再开始渲染
    VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
    submitInfo.waitSemaphoreCount = 1;
    submitInfo.pWaitSemaphores = &imageAvailableSemaphores[currentFrame];
    submitInfo.pWaitDstStageMask = waitStages;
    
    // 指定要执行的命令缓冲区
    submitInfo.commandBufferCount = 1;
    submitInfo.pCommandBuffers = &commandBuffers[currentFrame];
    
    // 当命令执行完成时,信号化"渲染完成"信号量
    submitInfo.signalSemaphoreCount = 1;
    submitInfo.pSignalSemaphores = &renderFinishedSemaphores[currentFrame];

    // 提交命令到图形队列,并关联围栏以跟踪完成状态
    if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]) != VK_SUCCESS) {
        throw std::runtime_error("Failed to submit draw command buffer!");
    }

    // 5. 设置呈现信息,准备将渲染结果呈现到屏幕
    VkPresentInfoKHR presentInfo{};
    presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
    
    // 等待"渲染完成"信号量,确保渲染完成后再呈现
    presentInfo.waitSemaphoreCount = 1;
    presentInfo.pWaitSemaphores = &renderFinishedSemaphores[currentFrame];

    // 指定要呈现的交换链和图像索引
    presentInfo.swapchainCount = 1;
    presentInfo.pSwapchains = &swapChain;
    presentInfo.pImageIndices = &imageIndex;
    
    // 提交呈现请求到呈现队列
    vkQueuePresentKHR(presentQueue, &presentInfo);

    // 6. 更新当前帧索引,循环使用预分配的同步对象
    currentFrame = (currentFrame + 1) % MAX_CONCURRENT_FRAMES;
}

完整的初始化过程关系图

呈现效果:

四、添加交换链重建功能

当前窗口缩放并不能正常绘制,因为窗口的显示区域大小变化了,需要重新获取区域信息,重建整个交换链。

void HelloTriangle::recreateSwapChain() {
    int width = 0, height = 0;
    glfwGetFramebufferSize(window, &width, &height); // 重新获取窗口显示大小
    // 当前显示区域面积为 0 时,需要轮询等待不为 0 时执行重建交换链操作
    while (width == 0 || height == 0) { 
        glfwGetFramebufferSize(window, &width, &height);
        glfwWaitEvents();
    }

    vkDeviceWaitIdle(device);

    cleanupSwapChain();

    createSwapChain();
    createImageViews();
    createFramebuffers();
}

死锁问题

重置栅栏后获取图片信息,如果判断需要重建交换链然后直接返回,这样没有提交任务导致先前的栅栏一直等待着,发生死锁了。

vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX);
vkResetFences(device, 1, &inFlightFences[currentFrame]); // 这会导致死锁

uint32_t imageIndex;
VkResult result = vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex);


if (result == VK_ERROR_OUT_OF_DATE_KHR) {
    recreateSwapChain(); 
    return; // 重建交换链后立即返回没有提交任务到队列导致栅栏一直等待!
} else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
    throw std::runtime_error("failed to acquire swap chain image!");
}

解决办法是延迟重置栅栏,直到我们确定我们将使用它提交工作。

  vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX);

    uint32_t imageIndex;
    VkResult result = vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex);

    if (result == VK_ERROR_OUT_OF_DATE_KHR) {
        recreateSwapChain();
        return;
    } else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
        throw std::runtime_error("Failed to acquire swap chain image!");
    }

    // Only reset the fence if we are submitting work
    vkResetFences(device, 1, &inFlightFences[currentFrame]);

五、新增成员变量和成员函数

新增/更新的成员变量

// 全局变量
const int MAX_CONCURRENT_FRAMES = 3; // 定义绘制的最大并行帧数


// 类成员变量更新
std::vector<VkCommandBuffer> commandBuffers;  // 为每一帧创建一个命令缓冲
std::vector<VkSemaphore> imageAvailableSemaphores; // 为每一帧创建一个图片信号量
std::vector<VkSemaphore> renderFinishedSemaphores;  // 为每一帧创建渲染器完成信号量
std::vector<VkFence> inFlightFences;  // 为每一帧创建一个栅栏

bool framebufferResized = false; // 窗口是否已经调整

新增成员函数

    void createCommandBuffers();
    void recordCommandBuffer(VkCommandBuffer commandBuffer, uint32_t imageIndex);
    void createSyncObjects();
    void recreateSwapChain();
    void cleanupSwapChain();

    void drawFrame();

main.cpp 更新

// #define GLFW_INCLUDE_VULKAN
// #include <GLFW/glfw3.h>

#include <cstdlib>
#include <iostream>
#include <stdexcept>
#include <vector>

#include "HelloTriangle.h"



GLFWwindow* window;
const uint32_t WIDTH = 800;
const uint32_t HEIGHT = 600;

// 调整窗口大小
static void framebufferResizeCallback(GLFWwindow* window, int width, int height) {
    auto app = reinterpret_cast<HelloTriangle*>(glfwGetWindowUserPointer(window));
    app->framebufferResized = true; // 标记窗口大小已调整
}

void initWindow(HelloTriangle* app) {
    glfwInit();
    glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
    window = glfwCreateWindow(WIDTH, HEIGHT, "Hello Vulkan", nullptr, nullptr);
    glfwSetWindowUserPointer(window, app);
    glfwSetFramebufferSizeCallback(window, framebufferResizeCallback);
}

int main() {
    HelloTriangle app;

    initWindow(&app);
    app.window = window;

    VkDevice &device = app.device;

    try {
        app.run();
        while (!glfwWindowShouldClose(window)) {
            glfwPollEvents();
            app.drawFrame();
        }
        vkDeviceWaitIdle(device);
        app.cleanup();

        glfwDestroyWindow(window);
        glfwTerminate();

    } catch (const std::exception& e) {
        std::cerr << e.what() << std::endl;
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

上一节和本节代码保存为分支 04_render_present

Vulkan被称为OpenGL的接班人,性能果然是霸气外漏,更能够承载下一个时代的图形渲染编程。 GPU高性能渲染的课题进入了一个新的阶段,对于计算细节的控制,多核CPU多线程渲染以及高性能算法的灵活设计需求日益旺盛。图形程序员需要有更加强力且灵活的工具,来“解锁”我们自身的控制能力,OpenGL的较高度封装性以及单纯的状态机模式显然已经无法适应现代化图形渲染的强烈需求。为什么要学习Vulkan?正如前言所说,Vulkan已经成为了下一个时代的图形渲染主流API,早已经被各大商业引擎(Unreal Engine、Unity3D)所支持。那么我们的同学就有如下问题需要明晰:1 作为游戏程序员我们只学会了UE或者Unity3D,那么就只能作为一个普通的程序员,如果能够结合Vulkan学习对商用引擎理解更加深刻,就可以更好的发挥引擎威力甚至更改引擎的源代码,实现更多的可能,让你在各大公司之间都“蛇形走位,游刃有余” 2 作为自研引擎工作人员,你可能在工业软件领域从业、也有可能在影视渲染领域从业、也可能在其他的图形系统领域(军工、GIS、BIM)等领域,那么熟练的掌握Vulkan就可以针对自己公司的不同领域需求进行不同的引擎定制开发,从而获得牢不可破的地位,对于自身职业发展有着极大的优势! 总而言之,越早学习Vulkan,就越能与别人拉开差距,让Vulkan称为你升职加薪、壮大不可替代性的核武器! 课程简介:本课程详细讲解了Vulkan从小白到入门的基础理论+实践知识,对于每一个知识点都会带领学员通过代码来实现功能。其中涵盖了计算机图形学基础理论,计算机图形学数学推导,Vulkan基础系统设计理论,基础单元(实例,设备,交换链), 渲染管线,RenderPass, 指令与多线程, 顶点描述与实验, Uniform与描述符, 图像与采样, 深度与反走样,模型与摄像机等内容;课程中会对Vulkan复杂抽象的API进行一次包装层的封装,将相关的API都进行聚合与接口设计,作为游戏或者图形引擎来讲,这是至关重要的第一步。这一个封装步骤,也被称为API-Wrapper,经过包装后的类库,同学可以在此之上根据自己的具体需求进行扩展,从而得到最适合自己的类库内容。本课程为系列化课程,在铸造基石篇章之后,会继续使用本包装类库进行改良,并且实现Vulkan API下的各类效果以及高级特性的开发教学。 课程优势:1 本课程会从计算机图形学的基础渲染管线原理出发,带领0基础的同学对计算机图形学进行快速认知,且对必要的知识点进行筛选提炼,去掉冗余繁杂的教学内容,更加适合新手对Vulkan渲染体系入门了解。 2 本课程会对计算机图形学所涉及的数学知识及如何应用到渲染当中,进行深入的讲解,带领同学对每一行公式展开认识,从三维世界如何映射到二维的屏幕,在学习完毕后会有清晰的知识体系 3 本课程会带领同学认知每一个Vulkan的API,并且在代码当中插入详细的注释,同学们在学习的时候就可以参照源代码进行一系列尝试以及学后复习 4 本课程所设计的包装层,会带领同学一行一行代码实现,现场进行Debug,对于Vulkan常出现的一些问题进行深入探讨与现场纠正  学习所得:1 同学们在学习后可以完全了解从三维世界的抽象物体,如何一步步渲染称为一个屏幕上的像素点。2 同学们在学习后可以完全掌握基础的Vulkan图形API,并且了解Vulkan当中繁多的对象之间相互的联系,从而可以设计更好的图形程序3 同学们在跟随课程进行代码编写后,可以获得一个轻量级的Vulkan底层API封装库(Wrapper),从而可以在此之上封装上层的应用,得到自己的迷你Vulkan图形渲染引擎当然,在达到如上三点之后,如果可以更进一步学习Vulkan的进阶课程,同学们可以获得更好的职业发展,升职加薪之路会更加清晰,成为公司不可替代的强力工程师 本课程含有全套源代码,同学购买后,可以在课程附件当中下载 完全不懂图形学可以学习么?使用层面上来讲是没有问题的,老师在每个api讲解的时候,都会仔细分析api背后的原理,所以可以跟随下来的话,能够编程与原理相融,学会使用 数学不好可以学习么?学习图形类课程,最好能够入门级别的线性代数,具体说就是: 1 向量操作 2 矩阵乘法 3 矩阵的逆、转置 这几个点就足够 学习后对就业面试有什么作用?目前类似Vulkan渲染知识是一切引擎的基础,只要能够跟随每一节课写代码做下来,游戏公司、工业软件公司等都是非常容易进去的,因为原理层面已经通晓,面试就会特别有优势。同学可以在简历上写熟悉VulkanAPI并且有代码经验,对于建立筛选以及面试都会有很大的帮助,对于薪资也会有大幅度提升
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值