如何解决Vulkan+OpenXR自定义游戏引擎中的MSAA性能问题?
本人拥有25年专业游戏开发经验,大部分问题都能解决,但当前遇到的难题必须在项目演示前修复(无抗锯齿时画面效果糟糕)。
正在开发一款基于Vulkan + OpenXR的自定义游戏引擎,在Quest 2设备上,关闭抗锯齿时场景可满帧率运行(72fps,即设备刷新率)。但启用4x硬件MSAA后,帧率异常骤降至15fps,而理论上MSAA对性能影响应极小,两种场景仅差一个MSAA resolve附件。
已确认更高帧率是可实现的:测试同一场景时,使用4x超采样替代MSAA,帧率可达36fps(是MSAA帧率的两倍多),尽管超采样理论上比MSAA慢得多。此外,在普通Android设备上运行不含OpenXR的相同代码时,MSAA性能表现正常,对帧率影响极小。
已开启Vulkan验证层和XR调试消息,但未发现任何错误或警告;全程检查返回码,均无错误(为简化展示,代码中未体现错误检查)。还测试了关闭验证层的情况,帧率无变化。
已实施推荐的MSAA优化方案,包括使用VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT使MSAA resolve完全在分片内存中进行(据相关文章,这应让MSAA几乎无性能开销),但问题仍未解决。
以下是创建swapchain、MSAA缓冲区及渲染管线相关部分的代码:color_format为枚举得到的格式,在该设备上为VK_FORMAT_R8G8B8A8_SRGB;swapChainExtent为从OpenXR获取的推荐尺寸(1440 x 1584)。当前无深度缓冲区(最初有深度缓冲区,为排查问题已移除,有无深度缓冲区问题均存在)。
XrSwapchainCreateInfo swapchain_create_info = { .type = XR_TYPE_SWAPCHAIN_CREATE_INFO, .next = null, .createFlags = 0, .usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT, .format = color_format, .sampleCount = 1, .width = swapChainExtent.width, .height = swapChainExtent.height, .faceCount = 1, .arraySize = 1, .mipCount = 1, }; XrResult result = xrCreateSwapchain(m_xrInfo->session, &swapchain_create_info, &m_xrSwapchain);
// allocate MSAA buffer VkImageCreateInfo imageInfo = { .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .flags = 0, .imageType = VK_IMAGE_TYPE_2D, .format = color_format, .extent = { swapChainExtent.width, swapChainExtent.height, 1}, .mipLevels = 1, .arrayLayers = 1, .samples = VK_SAMPLE_COUNT_4_BIT, .tiling = VK_IMAGE_TILING_OPTIMAL, .usage = VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, .sharingMode = VK_SHARING_MODE_EXCLUSIVE, .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, }; vkCreateImage(m_device, &imageInfo, nullptr, &image); VkMemoryRequirements memRequirements; vkGetImageMemoryRequirements(m_device, image, &memRequirements); VkMemoryAllocateInfo allocInfo{}; allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; allocInfo.allocationSize = memRequirements.size; allocInfo.memoryTypeIndex = FindMemoryType(memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT); vkAllocateMemory(m_device, &allocInfo, nullptr, &bufferMemory);
VkAttachmentDescription colorAttachment{}; colorAttachment.format = color_format; colorAttachment.samples = VK_SAMPLE_COUNT_4_BIT; colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; 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_COLOR_ATTACHMENT_OPTIMAL; attachments.Add(colorAttachment); VkAttachmentReference colorAttachmentRef{}; colorAttachmentRef.attachment = attachments.Count() - 1; colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; colorAttachments.Add(colorAttachmentRef); VkAttachmentDescription msaaAttachmentResolve{}; msaaAttachmentResolve.format = colorFormat; msaaAttachmentResolve.samples = VK_SAMPLE_COUNT_1_BIT; msaaAttachmentResolve.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; msaaAttachmentResolve.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; msaaAttachmentResolve.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; msaaAttachmentResolve.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; msaaAttachmentResolve.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; msaaAttachmentResolve.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; attachments.Add(msaaAttachmentResolve); VkAttachmentReference msaaAttachmentResolveRef{}; msaaAttachmentResolveRef.attachment = attachments.Count() - 1; msaaAttachmentResolveRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; subpass.colorAttachmentCount = colorAttachments.Count(); subpass.pColorAttachments = colorAttachments.GetPointer(); subpass.pResolveAttachments = &msaaAttachmentResolveRef; VkRenderPassCreateInfo renderPassInfo{}; renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; renderPassInfo.attachmentCount = attachments.Count(); renderPassInfo.pAttachments = attachments.GetPointer(); renderPassInfo.subpassCount = 1; renderPassInfo.pSubpasses = &subpass;
- 验证MSAA图像的内存分配是否正确应用
VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT:检查FindMemoryType函数是否严格优先选择同时包含VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT和VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT的内存类型,避免误选仅满足单个标志的类型。可通过打印内存类型的属性值确认分配结果。 - 修改MSAA颜色附件的
storeOp:将colorAttachment.storeOp从VK_ATTACHMENT_STORE_OP_STORE改为VK_ATTACHMENT_STORE_OP_DONT_CARE。MSAA附件作为临时渲染目标,resolve完成后无需保留内容,此修改可避免GPU额外的内存写入操作,降低开销。 - 优化RenderPass的布局设置:
- 将
msaaAttachmentResolve的initialLayout设为VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,减少布局转换的开销; - 确认Swapchain图像在acquire后已正确转换为
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,再作为resolve目标使用。
- 将
- 添加合理的Subpass依赖:当前RenderPass未设置subpass依赖,可能导致GPU流水线调度效率低下。添加包含
VK_SUBPASS_EXTERNAL的依赖项,指定正确的管线阶段和访问掩码,确保资源同步逻辑符合硬件调度预期。 - 测试不同MSAA样本数:先尝试2x MSAA,观察帧率变化,排查4x MSAA是否在Quest 2硬件上存在特殊的性能瓶颈。
- 检查OpenXR Swapchain的后端兼容性:确认XR Swapchain的
usageFlags正确映射到Vulkan的VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,可通过查询Swapchain对应的Vulkan图像属性验证。 - 使用Quest 2性能分析工具:借助Oculus Performance Toolkit分析GPU各阶段耗时,定位是MSAA光栅化阶段还是resolve操作占用了过多时间,精准锁定性能瓶颈。
- 排查图像复用逻辑:确认MSAA图像数量与Swapchain图像数量匹配,且每帧复用已创建的MSAA图像,避免频繁创建销毁图像带来的额外开销。
- 尝试调整MSAA图像的tiling模式:临时将
VK_IMAGE_TILING_OPTIMAL改为VK_IMAGE_TILING_LINEAR测试,排除硬件对特定tiling模式的MSAA支持问题(此操作仅用于排查,通常OPTIMAL更优)。
内容的提问来源于stack exchange,提问作者Johnathan

