在使用vulkan实现各项渲染工作的时候,我们经常会因为各种不经意的坑导致渲染失败,所以调试工作就尤为重要,所以今天我们就来说一下如何可视化vulkan的各项进程来调试程序。
一、简介
首先我们来说一下vulkan验证层:LunarG SDK附带的Vulkan验证层是在运行时调试应用程序所必需的,它们对于使应用程序根据规范进行验证并确保跨不同实现的可移植性至关重要。
但是它们无法捕获的是逻辑错误,因此即使启用了所有验证层并消除了所有错误,您仍然可能看不到期望的位置,并且需要逐步调试渲染的其他层。
因此,我们可以使用“ VK_EXT_debug_marker ”拓展(类似于OpenGL的GL_KHR_Debug扩展,可以增加命名和标记对象以及插入调试区域和标记的功能,以供脱机调试器显示)和RenderDoc离线图形调试应用程序(RenderDoc能够捕获应用程序的框架,包括所有API调用,对象和完整的管道状态,并在可视化的UI中显示所有这些信息)。
因此我们下边就来演示其在Vulkan应用程序和图形调试器中的用法。
二、调试标记功能封装
话不多说,直接上代码,通过注释你可以看到各函数作用:
//此扩展只有在从脱机调试应用程序运行时才会出现
namespace DebugMarker
{
bool active = false;
bool extensionPresent = false;
PFN_vkDebugMarkerSetObjectTagEXT vkDebugMarkerSetObjectTag = VK_NULL_HANDLE;
PFN_vkDebugMarkerSetObjectNameEXT vkDebugMarkerSetObjectName = VK_NULL_HANDLE;
PFN_vkCmdDebugMarkerBeginEXT vkCmdDebugMarkerBegin = VK_NULL_HANDLE;
PFN_vkCmdDebugMarkerEndEXT vkCmdDebugMarkerEnd = VK_NULL_HANDLE;
PFN_vkCmdDebugMarkerInsertEXT vkCmdDebugMarkerInsert = VK_NULL_HANDLE;
// 从设备中获取用于调试报告扩展的函数指针
void setup(VkDevice device, VkPhysicalDevice physicalDevice)
{
// 检查是否存在调试标记扩展(从图形调试器运行时就是这种情况)
uint32_t extensionCount;
vkEnumerateDeviceExtensionProperties(physicalDevice, nullptr, &extensionCount, nullptr);
std::vector<VkExtensionProperties> extensions(extensionCount);
vkEnumerateDeviceExtensionProperties(physicalDevice, nullptr, &extensionCount, extensions.data());
for (auto extension : extensions) {
if (strcmp(extension.extensionName, VK_EXT_DEBUG_MARKER_EXTENSION_NAME) == 0) {
extensionPresent = true;
break;
}
}
if (extensionPresent) {
//调试标记扩展不是核心的一部分,因此需要手动加载函数指针
vkDebugMarkerSetObjectTag = (PFN_vkDebugMarkerSetObjectTagEXT)vkGetDeviceProcAddr(device, "vkDebugMarkerSetObjectTagEXT");
vkDebugMarkerSetObjectName = (PFN_vkDebugMarkerSetObjectNameEXT)vkGetDeviceProcAddr(device, "vkDebugMarkerSetObjectNameEXT");
vkCmdDebugMarkerBegin = (PFN_vkCmdDebugMarkerBeginEXT)vkGetDeviceProcAddr(device, "vkCmdDebugMarkerBeginEXT");
vkCmdDebugMarkerEnd = (PFN_vkCmdDebugMarkerEndEXT)vkGetDeviceProcAddr(device, "vkCmdDebugMarkerEndEXT");
vkCmdDebugMarkerInsert = (PFN_vkCmdDebugMarkerInsertEXT)vkGetDeviceProcAddr(device, "vkCmdDebugMarkerInsertEXT");
// 如果存在至少一个函数指针,则设置标志
active = (vkDebugMarkerSetObjectName != VK_NULL_HANDLE);
}
else {
std::cout << "Warning: " << VK_EXT_DEBUG_MARKER_EXTENSION_NAME << " not present, debug markers are disabled.";
std::cout << "Try running from inside a Vulkan graphics debugger (e.g. RenderDoc)" << std::endl;
}
}
// 设置调试对象的名称
// Vulkan中的所有对象都由64位句柄表示,这些句柄被传递给这个函数以及对象类型
void setObjectName(VkDevice device, uint64_t object, VkDebugReportObjectTypeEXT objectType, const char *name)
{
//检查有效的函数指针(如果不在调试应用程序中运行,可能不存在)
if (active)
{
VkDebugMarkerObjectNameInfoEXT nameInfo = {};
nameInfo.sType = VK_STRUCTURE_TYPE_DEBUG_MARKER_OBJECT_NAME_INFO_EXT;
nameInfo.objectType = objectType;
nameInfo.object = object;
nameInfo.pObjectName = name;
vkDebugMarkerSetObjectName(device, &nameInfo);
}
}
// 设置对象的标记
void setObjectTag(VkDevice device, uint64_t object, VkDebugReportObjectTypeEXT objectType, uint64_t name, size_t tagSize, const void* tag)
{
//检查有效的函数指针(如果不在调试应用程序中运行,可能不存在)
if (active)
{
VkDebugMarkerObjectTagInfoEXT tagInfo = {};
tagInfo.sType = VK_STRUCTURE_TYPE_DEBUG_MARKER_OBJECT_TAG_INFO_EXT;
tagInfo.objectType = objectType;
tagInfo.object = object;
tagInfo.tagName = name;
tagInfo.tagSize = tagSize;
tagInfo.pTag = tag;
vkDebugMarkerSetObjectTag(device, &tagInfo);
}
}
// 启动一个新的调试标记区域
void beginRegion(VkCommandBuffer cmdbuffer, const char* pMarkerName, glm::vec4 color)
{
//检查有效的函数指针(如果不在调试应用程序中运行,可能不存在)
if (active)
{
VkDebugMarkerMarkerInfoEXT markerInfo = {};
markerInfo.sType = VK_STRUCTURE_TYPE_DEBUG_MARKER_MARKER_INFO_EXT;
memcpy(markerInfo.color, &color[0], sizeof(float) * 4);
markerInfo.pMarkerName = pMarkerName;
vkCmdDebugMarkerBegin(cmdbuffer, &markerInfo);
}
}
// 在命令缓冲区中插入一个新的调试标记
void insert(VkCommandBuffer cmdbuffer, std::string markerName, glm::vec4 color)
{
//检查有效的函数指针(如果不在调试应用程序中运行,可能不存在)
if (active)
{
VkDebugMarkerMarkerInfoEXT markerInfo = {};
markerInfo.sType = VK_STRUCTURE_TYPE_DEBUG_MARKER_MARKER_INFO_EXT;
memcpy(markerInfo.color, &color[0], sizeof(float) * 4);
markerInfo.pMarkerName = markerName.c_str();
vkCmdDebugMarkerInsert(cmdbuffer, &markerInfo);
}
}
// 结束当前调试标记区域
void endRegion(VkCommandBuffer cmdBuffer)
{
//检查有效的函数(如果不在调试应用程序中运行,可能不存在)
if (vkCmdDebugMarkerEnd)
{
vkCmdDebugMarkerEnd(cmdBuffer);
}
}
};
三、渲染中添加调试标记信息
接下来我们需要在渲染场景的时候添加调试标记来供离屏调试可视化使用:
首先在buildCommandBuffers中(简化版):
void buildCommandBuffers()
{
vkBeginCommandBuffer(drawCmdBuffers[i], &cmdBufInfo);
//启动一个新的调试标记区域
DebugMarker::beginRegion(drawCmdBuffers[i], "Render scene", glm::vec4(0.5f, 0.76f, 0.34f, 1.0f));
vkCmdBeginRenderPass(drawCmdBuffers[i], &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
// 启动一个新的调试标记区域
DebugMarker::beginRegion(drawCmdBuffers[i], "Toon shading draw", glm::vec4(0.78f, 0.74f, 0.9f, 1.0f));
DebugMarker::endRegion(drawCmdBuffers[i]);
// Draw scene
...
// 线框模式
if (wireframe)
{
// 启动一个新的调试标记区域
DebugMarker::beginRegion(drawCmdBuffers[i], "Wireframe draw", glm::vec4(0.53f, 0.78f, 0.91f, 1.0f));
DebugMarker::endRegion(drawCmdBuffers[i]);
// Draw scene
...
}
// 后处理
if (glow)
{
// 启动一个新的调试标记区域
DebugMarker::beginRegion(drawCmdBuffers[i], "Apply post processing", glm::vec4(0.93f, 0.89f, 0.69f, 1.0f));
DebugMarker::endRegion(drawCmdBuffers[i]);
// Draw scene
...
}
vkCmdEndRenderPass(drawCmdBuffers[i]);
DebugMarker::endRegion(drawCmdBuffers[i]);
vkEndCommandBuffer(drawCmdBuffers[i]);
}
对场景模型的每个部分我们也分别在draw函数中定义各调试标记:
std::vector<std::string> modelPartNames{ "hill", "crystals", "rocks", "cave", "tree", "mushroom stems", "blue mushroom caps", "red mushroom caps", "grass blades", "chest box", "chest fittings" };
void draw(VkCommandBuffer cmdBuffer)
{
VkDeviceSize offsets[1] = { 0 };
vkCmdBindVertexBuffers(cmdBuffer, 0, 1, &model.vertices.buffer, offsets);
vkCmdBindIndexBuffer(cmdBuffer, model.indices.buffer, 0, VK_INDEX_TYPE_UINT32);
for (auto i = 0; i < model.parts.size(); i++)
{
// 添加调试网格名称标记
DebugMarker::insert(cmdBuffer, "Draw \"" + modelPartNames[i] + "\"", glm::vec4(0.0f));
vkCmdDrawIndexed(cmdBuffer, model.parts[i].indexCount, 1, model.parts[i].indexBase, 0, 0);
}
}
简单的设置这些调试标识后,我们运行程序生产exe文件。
四、可视化(RenderDoc使用)
4.1 加载程序
RenderDoc是一个独立的图形调试器,已与LunarG Vulkan SDK一起发布,因此也是最早支持Vulkan的图形调试器之一。
从文章开始链接处下载并安装,启动RenderDoc并选择示例应用程序的二进制文件进行捕获。您可能还想在“操作”下将捕获排队,以便RenderDoc自动捕获第二帧(如果没有捕获,则随时使用F12捕获):
4.2 运行
运行后,我们可以看到默认截取两帧的图片,并在Event Browser中可以看到对应帧画面调用的所有API:
此时你可以看到左侧我们在程序中插入的一些调试标识,点开对应的对应的渲染子项,我们可以看到我们在draw函数中设置的细分调试标识:
点击对应的地方,我们可以看到不同绘制(vkCmdDrawIndexed)的显示效果:
小结
使用VK_EXT_debug_marker扩展和RenderDoc等脱机调试工具,在Vulkan中调试实际的渲染代码可以更加直观。而验证层将可以帮助您根据规范验证代码并避免错误,而离线调试使您可以详细检查渲染组成和资源使用情况,并可以定义自己的调试区域和标记并命名其他Vulkan对象,甚至调试复杂的应用程序现在都应该变得容易得多。