2.5 呈现与图像采集


文档摘要

2.5 呈现与图像采集 本节摘要:帧循环的骨架是「采集一幅后台图像、渲染、递交呈现」的轮转,驱动这一轮转的是交换链的展台规则与两根同步凭证:栅栏管 CPU 与 GPU 的节奏,信号量管采集与呈现的接力。本节交付一个健壮的帧循环,并把 OUTOFDATE 与 SUBOPTIMAL 两种退件处理成标准流程。 为什么交换链里的图像不能随便挑一幅来画,而必须先「申请」?因为前台正展示着其中一幅——画它的同时把它搬上台面,观众就会看到半成品。交换链因此是个受控轮转系统:采集(vkAcquireNextImageKHR)是领一张后台展台的号牌,呈现(vkQueuePresentKHR)是把画好的展台送回轮转。本节是第 2 章的收官手续,把前面四节的产出接成一个会呼吸的循环。

2.5 呈现与图像采集

本节摘要:帧循环的骨架是「采集一幅后台图像、渲染、递交呈现」的轮转,驱动这一轮转的是交换链的展台规则与两根同步凭证:栅栏管 CPU 与 GPU 的节奏,信号量管采集与呈现的接力。本节交付一个健壮的帧循环,并把 OUT_OF_DATE 与 SUBOPTIMAL 两种退件处理成标准流程。

为什么交换链里的图像不能随便挑一幅来画,而必须先「申请」?因为前台正展示着其中一幅——画它的同时把它搬上台面,观众就会看到半成品。交换链因此是个受控轮转系统:采集(vkAcquireNextImageKHR)是领一张后台展台的号牌,呈现(vkQueuePresentKHR)是把画好的展台送回轮转。本节是第 2 章的收官手续,把前面四节的产出接成一个会呼吸的循环。

一、最小帧循环的骨架

下面的循环刻意只用本章概念就能读懂。两个信号量(imageAvailable、renderFinished)与一条栅栏(inFlight)的分工,是本节的核心:

// 每帧开始:等上一帧用完的工单真正执行完,才能重录 vkWaitForFences(device, 1, &inFlightFence, VK_TRUE, UINT64_MAX); vkResetFences(device, 1, &inFlightFence); // 领号牌:从交换链领一幅后台图像的索引 uint32_t imageIndex; VkResult acq = vkAcquireNextImageKHR( device, swapchain, UINT64_MAX, // 最长等待;有信号量时通常不设限 imageAvailableSemaphore, // 信号:这幅图像可用了 VK_NULL_HANDLE, &imageIndex); // 采集阶段退件:交换链已过期,重建后本帧直接跳过 if (acq == VK_ERROR_OUT_OF_DATE_KHR) { recreateSwapchain(); return; } else if (acq != VK_SUCCESS && acq != VK_SUBOPTIMAL_KHR) { /* 真错误,上抛 */ } // 录制并提交(渲染命令引用 swapchainImages[imageIndex] 对应的视图) recordAndSubmit(imageIndex, waitStage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, waitSemaphore = imageAvailableSemaphore, // 渲染要等图像可用 signalSemaphore = renderFinishedSemaphore); // 渲染完发出信号 // 呈现:等渲染完成的信号,把图像交回轮转 VkPresentInfoKHR pi = {VK_STRUCTURE_TYPE_PRESENT_INFO_KHR}; pi.waitSemaphoreCount = 1; pi.pWaitSemaphores = &renderFinishedSemaphore; pi.swapchainCount = 1; pi.pSwapchains = &swapchain; pi.pImageIndices = &imageIndex; VkResult prs = vkQueuePresentKHR(presentQueue, &pi); if (prs == VK_ERROR_OUT_OF_DATE_KHR || prs == VK_SUBOPTIMAL_KHR) swapchainNeedsRebuild = true; // 呈现阶段退件:下一帧重建

三件同步工具各管一段,不能互相替代。栅栏(inFlightFence)拦住 CPU:上一帧的提交没执行完,命令缓冲不能重录——这是 GPU 与 CPU 之间的节奏控制。imageAvailable 信号量拦住渲染:图像没采集到,渲染不能写它。renderFinished 信号量拦住呈现:渲染没写完,图像不能上台。为什么采集不用栅栏?因为 CPU 不需要等采集——它可以提前录下一帧的命令,采集何时完成只与渲染命令有关,交给信号量最省。

二、轮转模型:展台为什么通常要三块

把帧循环跑起来后,展台数量的影响立刻显现。假设 CPU 与 GPU 每帧耗时相近:

图 2-4:双缓冲与三缓冲的节奏对比

图 2-4:双缓冲与三缓冲的节奏对比

展台数与呈现模式的组合才是完整答案。FIFO 模式下呈现调用进队列按垂直同步节拍上台,是「不撕裂」的保证;MAILBOX 允许新帧替换排队中的旧帧,配合三缓冲是低延迟场景的常用解;IMMEDIATE 不排队,画完立刻上台,快但可能撕裂。2.4 节的清单挑选策略在这里变成体感差异。

三、案例:一次撕裂故障的归因

背景:某采样回放工具在用户拖动进度条时画面上下两半错位,像两张不同时刻的画面被硬拼在一起。开发者最初怀疑是纹理上传竞态,排查了两天上传代码。

操作:按「撕裂只在呈现层可能发生」的思路检查呈现配置,发现该工具为追求最低延迟把呈现模式设成了 IMMEDIATE,且拖动进度条时帧内容变化剧烈。撕裂的机理是:上台动作没有与显示器刷新对齐,上半屏显示的是上一帧、下半屏已是下一帧。

结果:提供设置项把默认模式改为 FIFO(必须存在的保底项),拖动时画面连贯;保留 IMMEDIATE 作为高级选项并在界面上注明风险。顺带把 swapchainNeedsRebuild 的检查加进循环,消除了一次用户报错。

解读:撕裂不是 bug 而是模式的定义——IMMEDIATE 本来就承诺不对齐。选型时把「内容变化剧烈程度」与「用户对撕裂的容忍度」纳入呈现模式决策,比事后排查有效得多。

变式:FIFO_RELAXED 是个折中项:垂直同步守得住时是 FIFO,失守(帧率低于刷新率)时降级为立即,延迟更平滑但会偶尔撕裂,适合「偶尔掉帧比持续高延迟更可接受」的场景。

四、退件处理:把例外变成流程

OUT_OF_DATE 与 SUBOPTIMAL 的区别要分清:前者是「参数已经失效,必须重建才能继续」,后者是「还能继续用,但已经不匹配,建议找时机重建」。采集与呈现两个阶段都可能返回它们,处理策略一致但时机不同——采集阶段退件当帧直接放弃,呈现阶段退件记个标记下帧处理。触发重建的还有窗口事件(2.4 节案例),工程做法是把两类来源汇入同一个 needsRebuild 标记,帧循环开头统一检查,避免两处代码各改各的。

重建时的 vkDeviceWaitIdle 要慎用:它等的是所有队列全部空闲,代价大;只在重建这类低频事件里使用,帧循环内部绝不用它做同步——那是第 6 章栅栏的职责。

五、常见坑

其一,waitStage 忘记设置:提交时必须告诉驱动渲染命令从哪个阶段开始等 imageAvailable,漏填 COLOR_ATTACHMENT_OUTPUT 位会导致验证层报错或更糟的死等。其二,每帧只建一套同步对象:帧与帧之间会并发,标准做法是按「在途帧数」(通常与展台数一致)各建一套栅栏与信号量轮换使用。其三,把 SUBOPTIMAL 当硬错误上抛:它不是失败,把它当失败会让程序在显示器热插拔时崩溃。

本节要点回顾

  • 轮转受控:采集领号牌、呈现交还展台,绕开申请直接绘制前台图像是未定义行为。
  • 三工具各管一段:栅栏管 CPU 等 GPU,采集信号量管渲染等图像,完成信号量管呈现等渲染,不可互相替代。
  • 展台数量是权衡:三缓冲换吞吐、双缓冲换低延迟,与呈现模式组合出最终体感。
  • 两级退件:OUT_OF_DATE 必须重建、SUBOPTIMAL 建议重建,采集与呈现两个阶段都要接住。
  • 在途帧并行:同步对象按帧建套轮换,vkDeviceWaitIdle 只留给重建这类低频事件。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U