尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Vulkan绘制三角形实战:从顶点缓冲到命令提交的完整渲染链路

Vulkan绘制三角形实战:从顶点缓冲到命令提交的完整渲染链路 如果你能一路走到这一篇说明Vulkan里最磨人的实例、物理设备、逻辑设备、交换链这些初始化步骤你已经趟过来了。这一篇要解决的就是最后一个问题怎么把那个三角形真正画出来。很多人学Vulkan会卡在这一步因为前面的代码再复杂好歹每一步都是声明一个对象、创建一个资源逻辑是线性的到了绘制这一步你需要同时操作顶点缓冲、图形管线、渲染通道、帧缓冲、命令缓冲、信号量任何一环没接上屏幕上就是一个黑屏而且Vulkan不像OpenGL那样会给你一个还能看得过去的默认状态它什么都不给少做一步就是什么都不画。这篇笔记不会重复前面已经讲过的交换链创建、逻辑设备枚举这些内容而是聚焦在绘制这条链路上从数据怎么进显存到绘制命令怎么录制再到怎么提交给GPU执行最后把渲染循环完整串起来。适合已经能让Validation Layer安静闭嘴、swapchain能正常创建、但屏幕上还什么都没有的读者。我会把每一步为什么要这么做讲清楚也会把我在这个环节踩过的高频坑单独列一节希望能帮你省掉几个通宵。1. 画一个三角形之前先搞懂Vulkan里的一帧是怎么流转的1.1 从CPU指令到屏幕像素的完整链路很多从OpenGL转过来的人第一次用Vulkan画东西时最大的困惑不是某个API不会调而是整个绘制流程到底被拆成了几段。在OpenGL里你调一个glDrawArrays驱动在背后帮你把一大堆状态、管线、命令都打包好送去GPU在Vulkan里这一步被拆成了几个你可以控制、也必须控制的阶段CPU端准备数据把顶点坐标、颜色这些数据放进CPU能写、GPU能读的缓冲里。CPU端录制命令把绑定管线、绑定顶点缓冲、调用绘制、结束渲染通道这些操作用vkCmdXxx系列函数写进一个命令缓冲这个过程不执行任何GPU任务只相当于写脚本。提交命令把录好的命令缓冲提交到队列queue上GPU从队列里取命令执行。呈现GPU把画好的图像交给交换链扫描输出到屏幕。我习惯把Vulkan的绘制流程理解成做菜OpenGL是你请了一个大厨你说来个番茄炒蛋大厨自己知道该用什么锅、什么火候、什么摆盘Vulkan是你自己当大厨得提前把锅具渲染通道、灶台管线、食材顶点缓冲、菜谱命令缓冲全部准备好然后下锅提交队列炒最后装盘present。自由度大但每一步都要你自己来。1.2 为什么前面铺垫了那么多这一篇才轮到绘制说实话Vulkan的绘制本身并不复杂复杂的是绘制前那一堆资源之间的依赖关系。你要画三角形至少要保证这几件事成立交换链已经创建成功并且能拿到一个图像索引否则你不知道往哪张图像上画渲染通道和帧缓冲已经创建好否则GPU不知道在附件上做什么操作图形管线已经创建好否则GPU不知道用什么着色器、什么光栅化规则去执行绘制顶点缓冲已经创建并填入数据否则GPU取不到顶点位置命令缓冲录制正确否则GPU拿到的是无效指令。所以这篇笔记的顺序其实就是一个三角形从数据到像素的完整流转顺序。你没有必要背下所有API但一定要清楚你在哪个环节、为什么在这里调用这个API。先有这条链路再写代码出错的时候也不用拍脑袋猜。2. 顶点数据送进显存顶点缓冲创建的完整链路2.1 为什么要造一个临时中转站Staging Buffer顶点数据这些东西CPU可以直接写在普通的malloc或std::vector内存里但GPU在执行绘制时访问的是显存。Vulkan里你要让GPU高效读取数据最好用VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT的内存类型这种内存物理上就在GPU那边带宽高、延迟低。但问题来了CPU无法直接往DEVICE_LOCAL内存里写数据。而且Vulkan和OpenGL一个很大的区别是Vulkan不会在你调用glBufferData的时候悄悄帮你拷贝数据它把内存分配和绑定完全暴露给你。常规做法是两步创建一个VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT的目标缓冲分配DEVICE_LOCAL内存绑定。再创建一个VK_BUFFER_USAGE_TRANSFER_SRC_BIT的临时缓冲分配HOST_VISIBLE_BIT | HOST_COHERENT_BIT的内存用memcpy把顶点数组拷进去。最后用vkCmdCopyBuffer在GPU端把数据从临时缓冲拷贝到目标缓冲。这个临时缓冲就叫Staging Buffer。初学者最容易肉疼的就是这一步我画个三角形而已为什么还要多此一举 我一开始也觉得麻烦试过直接往HOST_VISIBLE的缓冲里写数据然后绑定为顶点缓冲确实能画出三角形但性能上非常不理想因为在GPU读的时候要经过PCIe总线带宽有限。等后面模型面数多了帧率会非常难看。提示如果只是做学习性质的三角形、或者顶点数据量很小你确实可以跳过staging buffer直接用一个HOST_VISIBLE缓冲。但建议还是把标准流程跑一遍因为后面做纹理、做模型的顶点上传全都要走staging。2.2 顶点缓冲从创建到绑定的五步走在Vulkan里创建缓冲和分配内存是两件事这一点和OpenGL完全不同。创建顶点缓冲的标准流程拆开是这样的填写创建信息VkBufferCreateInfo指定 size 为sizeof(Vertex) * vertexCountusage 根据用途加上VK_BUFFER_USAGE_VERTEX_BUFFER_BIT。创建缓冲对象调用vkCreateBuffer得到VkBuffer。注意这一步只是创建了一个逻辑对象不分配任何显存。获取内存需求调用vkGetBufferMemoryRequirements拿到VkMemoryRequirements里面最重要的是memoryTypeBits和size。分配内存Vulkan里没有malloc你要通过vkAllocateMemory在设备上分配内存。这一步的关键是遍历物理设备的内存类型找到一个既满足memoryTypeBits掩码、又带有所需属性标志比如HOST_VISIBLE的类型。绑定调用vkBindBufferMemory把缓冲和内存绑在一起。最后一步也是初学者最容易忽略的在把数据拷进内存之前必须先用vkMapMemory把内存映射到CPU地址空间用完再vkUnmapMemory。如果你是HOST_COHERENT属性不需要手动flush但映射这个动作不能省。2.3 顶点输入描述告诉GPU数据是怎么排列的这一步是很多人的思维盲区。你把一个包含 position 和 color 的Vertex结构体数据传给了GPU但GPU怎么知道缓冲里第几个字节是x坐标、第几个字节是颜色Vulkan里有专门的描述结构分成两层VkVertexInputBindingDescription描述的是这份顶点数据每隔多少字节算一个顶点。比如stride sizeof(Vertex)binding 0。VkVertexInputAttributeDescription描述的是每个属性位置、颜色从哪个偏移开始、是什么格式、对应着色器里的哪个location。比如 position 是VK_FORMAT_R32G32_SFLOAT两个floatoffset 0color 是VK_FORMAT_R32G32B32_SFLOAToffset offsetof(Vertex, color)。如果把GPU比作一条流水线顶点数据就是传送带上的零件binding告诉工人每个零件多大attribute告诉工人哪个槽位装什么规格的螺丝。这两组数据要在创建图形管线时传给管线创建信息也就是说顶点格式是编译期就定死进管线里的。这里有个很麻烦但很重要的点如果后面你想给三角形加UV坐标、加法线改了Vertex结构体同时也要改这两个description否则着色器解析数据的偏移错位画出来的东西就会乱掉。我在项目里见过同事改完结构体忘改偏移结果模型整体拉伸变形的诡异问题排查了整整一个下午。3. 图形管线把渲染状态提前焊死的代价与收益3.1 管线是Vulkan和OpenGL最大的分水岭OpenGL里你可以随时切换Shader、随时修改混合模式、随时开关深度测试这些状态存在于一个巨大的状态机里每次绘制时驱动都要检查一遍当前状态这就是OpenGL绘制效率上不去的原因之一。Vulkan把这个状态机拆掉了你要在创建VkPipeline时把顶点输入、输入装配、视口、光栅化、多重采样、深度模板、颜色混合等所有状态全部指定好GPU在执行绘制时不关心状态切换只认你绑定的管线。你可以把VkPipeline想成是一个焊死的加工台所有旋钮、开关、挡板在出厂时就固定好了每个加工台只执行一种特定的加工任务。这样做的收益就是驱动不需要在每次draw call时反复检查状态CPU开销大幅降低代价就是创建管线极其繁琐而且几乎每次修改渲染状态都要重新创建管线。3.2 着色器模块与SPIR-V编译管线创建里的着色器部分Vulkan和OpenGL也有一个巨大差异Vulkan不接受GLSL源代码。你要先把GLSL用glslc或glslangValidator编译成SPIR-V二进制再通过vkCreateShaderModule包装成Vulkan能用的模块管线创建时引用这个模块。三角形只需要两个着色器顶点着色器和片段着色器。下面是最简的顶点和片段着色器GLSL写法// triangle.vert #version 450 layout(location 0) in vec2 inPosition; layout(location 1) in vec3 inColor; layout(location 0) out vec3 fragColor; void main() { gl_Position vec4(inPosition, 0.0, 1.0); fragColor inColor; }// triangle.frag #version 450 layout(location 0) in vec3 fragColor; layout(location 0) out vec4 outColor; void main() { outColor vec4(fragColor, 1.0); }编译命令很简单glslc triangle.vert -o vert.spv glslc triangle.frag -o frag.spv一个容易踩的坑是location编号必须与顶点输入的VkVertexInputAttributeDescription里的location对应上。你在C里给position配了location 0到了GLSL里也必须是0错一个就会得到黑屏Validation Layer报错的组合套餐。3.3 几个最容易让初学者懵的状态配置创建VkGraphicsPipelineCreateInfo时里面要填一大堆VkPipelineXxxStateCreateInfo我第一次看的时候心态直接崩了。实际上画三角形核心是这几个顶点输入状态填上一节说的binding和attribute描述如果没有顶点输入比如直接用vkCmdDraw的顶点ID生成数据这里传空。输入装配状态指定拓扑结构三角形就用VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST这是告诉GPU三个顶点画一个三角形。视口状态指定视口大小和裁剪矩形。这里有个设计上的选择——如果你把VkPipelineViewportStateCreateInfo里的pViewports和pScissors设为null同时把它们加入VkPipelineDynamicStateCreateInfo的动态状态列表就可以在渲染时用vkCmdSetViewport动态设置这样窗口变化时不用重建管线。我强烈建议三角形学习阶段就用动态viewport因为后面做窗口缩放很常见。光栅化状态三角形没有线框模式的话polygonMode用VK_POLYGON_MODE_FILLcullMode可以先设VK_CULL_MODE_NONE。注意如果你的顶点顺时针/逆时针方向不小心反了开了背面剔除以后屏幕上什么都不会有我第一次实验时就被这个坑过一次。多重采样状态先用VK_SAMPLE_COUNT_1_BIT就行等后面需要抗锯齿再改。颜色混合状态三角形不涉及混合把blendEnable设为VK_FALSE即可但VkPipelineColorBlendAttachmentState这个结构体必须填不能空。深度模板状态三角形用不到深度测试直接开VK_FALSE。实际上Vulkan里管线状态里还有几个不显眼但很实用的字段很多人没注意VkPipelineInputAssemblyStateCreateInfo里的primitiveRestartEnable无索引绘制时可以不管但如果你后面做索引绘制开启后可以通过索引0xFFFFFFFF实现多条线/多个图元这是一个隐藏的高效功能。VkPipelineRasterizationStateCreateInfo里的depthBiasEnable画三角形用不到但做阴影贴图shadow map时必须开启否则深度偏差问题会非常明显。VkPipelineColorBlendStateCreateInfo里的logicOpEnable平时保持VK_FALSE即可。这些字段的存在感极低但它们正是Vulkan让你掌控一切的体现。你不知道它们的时候每个字段都是噪音等你知道什么时候要用它们的时候你会发现这就是Vulkan最值钱的部分。4. 渲染通道与帧缓冲画面到底画到哪里去4.1 渲染通道Vulkan的场景画布定义在Vulkan里你不能直接对一张图像随便执行绘制命令必须先定义好一个渲染通道Render Pass。渲染通道描述了这一步绘制会用到哪些附件Attachment以及每个附件在每个阶段做什么操作。为什么Vulkan要搞渲染通道这个东西因为它可以在GPU执行前就知道这一帧绘制需要读哪些内存、写哪些内存、这些内存是否需要清空、绘制结束后是否保留给后续用途。这样GPU和驱动就能提前做好内存布局优化。说人话就是你在画布上画画之前得先说明这张画布是用来打草稿还是用来展出、画之前要不要擦干净、画完要不要保留给下个人看。三角形这个案例里我们只需要一个颜色附件。它的VkAttachmentDescription里有两个字段你必须理解清楚loadOp渲染开始前对附件做什么。三角形要清空背景色用VK_ATTACHMENT_LOAD_OP_CLEAR如果你上一帧已经把画面画满了不需要清屏用VK_ATTACHMENT_LOAD_OP_LOAD会更高效。storeOp渲染结束后要不要保留结果。因为结果要交给交换链去显示所以必须用VK_ATTACHMENT_STORE_OP_STORE。还有一个极关键的点颜色附件的格式必须和交换链图像的格式完全一致。交换链在创建时你选了什么格式比如VK_FORMAT_B8G8R8A8_SRGB渲染通道附件就必须用什么格式。不一致的话Validation Layer会直接报错管线创建也会失败。4.2 帧缓冲把画布安装到具体的交换链图像上渲染通道定义的是画布规格帧缓冲Framebuffer才是把画布和具体图像绑定在一起的实体。交换链动态管理着2~3张可供渲染的图像每一帧你会从交换链取出一张图像然后必须为这张图像创建一个帧缓冲或者复用同一套帧缓冲取决于你的交换链设计。创建VkFramebuffer时的关键信息关联的renderPass帧缓冲必须指定它服务于哪个渲染通道二者附件数量和格式要匹配。attachment传入一个VkImageView数组。对三角形而言就是交换链图像的VkImageView。宽高必须和视口一致也就是交换链的分辨率。一个比较常见的困惑是交换链有3张图像你是不是每张都要单独创建一个帧缓冲是的。交换链的每个图像在创建时都会生成一个VkImageView你需要为每张图像创建一个帧缓冲。但注意帧缓冲可以只创建一次在初始化时然后在每一帧取出图像后选择对应图像索引的帧缓冲来使用。如果交换链重建了比如窗口resize所有帧缓冲都要销毁重建。5. 录制命令缓冲把一次绘制打包成一段清晰的指令流5.1 为什么Vulkan要让你自己录命令在OpenGL里你每次调用一个绘制命令驱动都会现场解析、现场判断状态、现场生成GPU指令而Vulkan把生成GPU指令这件事提前到了录制命令缓冲这一阶段。也就是说你在CPU端提前把整段绘制脚本写好GPU拿到的是一份固定的、已经优化过的指令列表执行效率自然更高。命令缓冲是从VkCommandPool分配的。命令池的概念很有意思它是对队列族queue family的一种细分归类你从哪个队列族的命令池分配出来的命令缓冲最终就必须提交到哪个队列族的队列上去执行。如果你用图形队列族创建了一个命令池却把命令缓冲提交到计算队列族GPU会直接把你的提交拒掉。命令缓冲分一级和二级我们绘制时用一级命令缓冲就够了二级命令缓冲主要用于在渲染通道内进行命令复用比如把画天空盒这组操作录制成二级命令缓冲多帧复用性能优化手段之一现在不用管。5.2 一次三角形绘制的完整录制清单命令缓冲的录制过程很像写代码vkBeginCommandBuffer是打开一个文件开始写vkEndCommandBuffer是保存关闭。在 begin 和 end 之间你要把所有绘制动作按顺序写进去。一个最标准的三角形录制流程如下vkBeginCommandBuffer(cmdBuffer, beginInfo); // 1. 开始渲染通道同时用背景色清空附件 vkCmdBeginRenderPass(cmdBuffer, renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE); // 2. 绑定图形管线 vkCmdBindPipeline(cmdBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); // 3. 绑定顶点缓冲 vkCmdBindVertexBuffers(cmdBuffer, 0, 1, vertexBuffer, offsets); // 4. 动态设置视口如果你选择了动态viewport vkCmdSetViewport(cmdBuffer, 0, 1, viewport); vkCmdSetScissor(cmdBuffer, 0, 1, scissor); // 5. 绘制三个顶点 vkCmdDraw(cmdBuffer, 3, 1, 0, 0); // 6. 结束渲染通道 vkCmdEndRenderPass(cmdBuffer); vkEndCommandBuffer(cmdBuffer);有几个细节值得展开说一下vkCmdBeginRenderPass里的VkSubpassContents传VK_SUBPASS_CONTENTS_INLINE表示命令直接写在主命令缓冲里另一个枚举值VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS表示后续命令写在二级命令缓冲里。三角形用前者。vkCmdDraw的五个参数分别是vertexCount3、instanceCount1、firstVertex0、firstInstance0。三角形不需要实例化instanceCount 给 1 就好。5.3 提交给GPU执行的必要同步命令录好了如果不提交给队列GPU什么都不会发生。提交命令调用的是vkQueueSubmitVkSubmitInfo submitInfo{}; submitInfo.commandBufferCount 1; submitInfo.pCommandBuffers cmdBuffer; // 初始化、录制、提交但是这里有个初学者搞不定的核心问题多帧渲染时命令缓冲不能一边被GPU执行、一边又被CPU重写。假如你只有一个命令缓冲CPU在录制第2帧时GPU还在执行第1帧数据就冲突了。解决办法是帧内多缓冲常见做法是准备2~3个命令缓冲和交换链图像数量一致或设置成飞行帧数每一帧轮流使用其中一个。更简单粗暴的办法是每帧都调vkQueueWaitIdle强制GPU干完活再开始录下一帧——对三角形这个学习案例完全可行但会浪费GPU性能因为CPU和GPU在大部分时间里都在互相等待。提交队列时还牵扯到一个让所有Vulkan新手都非常头疼的概念信号量Semaphore和栅栏Fence。它们都是同步原语但适用场景完全不同信号量在GPU内部队列操作之间做同步CPU不能等待它。栅栏用于CPU等待GPU完成某件事也可以用于队列内的同步但学习阶段你只需要让CPU等它。三角形的单帧流程中你至少需要三个同步点获取图像完成后imageAvailable信号量、渲染完成后renderFinished信号量、处理完成后CPU可以继续inFlightFence栅栏。6. 渲染循环的完整骨架从获取图像到呈现屏幕6.1 三种同步工具的实际用法分工先画一张我脑海里的流程图文字版CPU调用vkAcquireNextImageKHR从交换链拿图像这个函数可能是异步的交换链可能还没有新的空余图像所以它挂了一个imageAvailable信号量GPU拿到图像后就signal它然后CPU把命令缓冲提交到队列提交时告诉GPU等imageAvailable信号量发出后再开画画完后signalrenderFinished最后CPU调用vkQueuePresentKHRpresent操作等renderFinished后再把图像交给屏幕。如果你漏掉了其中任何一个等待关系最常见的后果就是画面撕裂tearing或者画到一张正被扫描输出的图像上。三角形还好图形复杂后这个问题会非常显眼。命令走上正轨之后你再考虑加一个栅栏inFlightFence来控制CPU的重录节奏在vkQueueSubmit之前用vkWaitForFences等上一帧的栅栏确认GPU执行完了再开始录制新命令vkAcquireNextImageKHR获取到图像之后要vkResetFences保证栅栏可以再次触发。如果不reset栅栏将永远处于已触发状态vkWaitForFences会直接返回失去保护作用——这个坑我确实踩过。6.2 一个正确但足够简单的单帧渲染循环这里的简单指的不是功能简单而是每个阶段都用一个缓冲不搞多帧在飞multiple frames in flight的优化。虽然性能上会浪费一些GPU能力但能在最大程度上减少同步错误适合学习验证。void drawFrame() { // 1. 等待本帧的栅栏确保上一帧的GPU工作彻底结束 vkWaitForFences(device, 1, inFlightFence, VK_TRUE, UINT64_MAX); vkResetFences(device, 1, inFlightFence); // 2. 从交换链获取一张可渲染的图像 uint32_t imageIndex; vkAcquireNextImageKHR(device, swapchain, UINT64_MAX, imageAvailableSemaphore, VK_NULL_HANDLE, imageIndex); // 3. 重新录制命令缓冲注意要reset vkResetCommandBuffer(commandBuffer, 0); recordCommandBuffer(commandBuffer, imageIndex); // 4. 提交命令GPU等待imageAvailable后再绘制绘制完触发renderFinished VkSubmitInfo submitInfo{}; submitInfo.waitSemaphoreCount 1; submitInfo.pWaitSemaphores imageAvailableSemaphore; submitInfo.pWaitDstStageMask waitStages; // VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT submitInfo.commandBufferCount 1; submitInfo.pCommandBuffers commandBuffer; submitInfo.signalSemaphoreCount 1; submitInfo.pSignalSemaphores renderFinishedSemaphore; vkQueueSubmit(graphicsQueue, 1, submitInfo, inFlightFence); // 5. 呈现等renderFinished信号量后再展示 VkPresentInfoKHR presentInfo{}; presentInfo.waitSemaphoreCount 1; presentInfo.pWaitSemaphores renderFinishedSemaphore; presentInfo.swapchainCount 1; presentInfo.pSwapchains swapchain; presentInfo.pImageIndices imageIndex; vkQueuePresentKHR(presentQueue, presentInfo); }这里面最容易被忽略的还有一个pWaitDstStageMask。提交信息里你的imageAvailable信号量到底在GPU管线哪个阶段等待如果你设成VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT最顶层那么GPU会在任何工作开始前就等待更合适的做法是设成VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT颜色附件输出阶段表示只有等图像真正可用后才开始往颜色缓冲里写。这个只在极高性能要求下才看得出差别但理解它对理解GPU执行模型很有帮助。6.3 验证结果能看到三角形就说明整条链路通了当第一次运行后你看到一个由背景色和三角形组成的画面说明实例、设备、交换链、渲染通道、管线、帧缓冲、命令缓冲、同步机制全部正常工作。我第一次跑通的时候屏幕上那个三角形甚至有点发灰因为我clear value和顶点颜色都是黑色系但我盯着屏幕看了好一会儿那种成就感确实很特别。验证通过以后可以尝试一下把背景色改成明显的颜色如深灰色把三角形三个顶点设成红绿蓝这样能一眼看出顶点颜色是否被正确传递。有些时候三角形出来了但颜色不对大概率是片段着色器里的location编号和顶点着色器输出没对上或者顶点属性格式写错了比如把RGB写成了RGBA。7. 第一次跑出三角形以后高频黑屏问题与排查顺序7.1 我把常见问题按照出现频率排了个序画三角形这步虽然只有几百行代码但出问题的环节非常多。我把我在社区答疑和自己踩坑时反复遇到的问题整理成了下面这张表你遇到黑屏时按这个顺序排查基本能解决90%的问题。现象最可能的原因排查与解决Validation Layer报错但画面正常某个状态忘记填、某个对象忘记销毁打开Validation Layer挨个修千万不要关闭报错硬跑全屏黑背景色都没有vkCmdBeginRenderPass没调或者渲染通道没执行检查命令缓冲是否真的提交了提交队列是否是图形队列背景色有但三角形不显示背面剔除把三角形剔掉了检查顶点绕序或把cullMode设为VK_CULL_MODE_NONE背景色有三角形变成了细线/点输入装配拓扑错误或顶点格式错误确认topology是TRIANGLE_LIST确认stride和offset正确三角形显示但严重变形视口大小和交换链尺寸不一致检查viewport和scissor的宽高应该跟随窗口大小我改了三角形坐标但没有变化顶点缓冲里的数据没更新或没重新上传确认memcpy后做了flush非coherent内存确认顶点缓冲绑定正确窗口有个白框或者残影交换链图像没有正确清空或storeOp错误检查loadOp和storeOp的形态逻辑除了排查问题还有一个非常实用的建议开发期间一定全程开着Validation Layer。它在性能上确实有一些开销但它能在GPU真正把内存搞乱之前告诉你问题在哪。很多初学者为了跑快点把Validation Layer关了结果出现诡异的花屏问题排查起来比开着Validation Layer慢十倍都不止。7.2 交换链失效与窗口缩放隐藏的地雷你以为画出了三角形就万事大吉了只要你在桌面环境上运行就会有用户去拖拽窗口、缩放窗口。交换链一旦失效VK_ERROR_OUT_OF_DATE_KHR你继续往交换链上呈现就会出错最常见的表现是窗口变成黑屏或者显示异常。处理方式也不复杂每次vkAcquireNextImageKHR返回VK_ERROR_OUT_OF_DATE_KHR时说明交换链跟窗口表面不匹配了每次vkQueuePresentKHR返回同样错误时也需要重建交换链。重建交换链不是一个简单调用的过程你要销毁旧的帧缓冲、物理交换链重新按新窗口尺寸创建交换链和帧缓冲回到管线创建阶段之前。对三角形这个阶段最省事的处理方式是在vkQueuePresentKHR返回out of date后延迟到下一帧再重建这样可以避免在刷新中间砍掉一帧导致画面闪烁。还有一个小细节很多人不提你必须在vkAcquireNextImageKHR返回成功后立刻使用该图像索引并且在这个索引对应的命令缓冲执行完成之前不要重新获取同一索引。用栅栏控制飞行帧数就是干这个的。如果暴力开了3帧在飞又只有一个命令缓冲Vulkan会非常大方地让你写出数据竞争。7.3 三角形之后往哪个方向继续深入如果你已经成功画出彩色三角形下一步最自然的方向是给三角形加上变换矩阵。这个阶段会第一次接触Uniform Buffer和Descriptor Set也是从硬编码管线走向可编程绘制的关键一步。另一个方向是给三角形加索引缓冲vkCmdDrawIndexed这样后面对复杂模型就有办法处理了。我个人很推荐先做Uniform Buffer因为你会把Vulkan资源模型里的最后一个大板块——描述符——补上之后就基本理解Vulkan 90%的资源交互方式了。
返回列表