图形API学习工程(3):建立更方便调试的机制

工程GIT地址:https://gitee.com/yaksue/yaksue-graphics

目标

当工程变大时,好的调试机制可以更快解决问题。目前工程中没有这方面的机制,所以我决定优先建立起来,具体包括:

  • C++的异常处理(throw)。调用图形API时如果返回了错误,我需要将其“抛出”。
  • 图形API自带的debug机制。例如Vulkan的validation layers;D3D12也有ID3D12Debug::EnableDebugLayer提供类似的机制。

异常处理

关于异常处理,我用得很少,因此我决定对其基础先进行下学习(主要参考的资料是 C++ 异常处理 | 菜鸟教程 ),然后在对各个图形API建立各自的机制。

基础

异常处理使用起来结构上还是很简单明了的。
如下代码是一个简单的测试,其中假设function的参数不能是3,否则就报出异常

#include <iostream>

void function(int a)
{
	if (a == 3)
	{
		throw "不应该是3";
	}

	std::cout << "执行完成" << std::endl;
}

int main()
{
	try 
	{
		function(3);
	}
	catch (const char* msg) 
	{
		std::cerr << msg << std::endl;
	}

	return 0;
}

执行:
在这里插入图片描述
会发现std::cerr << msg << std::endl;语句输出了捕捉到的异常,并且function并没有执行下去。

此部分要注意的就是:使用throw抛出的异常是const char*类型,为了捕捉这种类型的异常,catch也要指定这个类型。


不使用try+catch也是可以的,这样就会触发断点:
在这里插入图片描述
提示“未经处理的异常”。(如果使用try+catch,但是并没有对应类型的catch,也会有这个错误)

此时点“继续”之后,程序会继续执行下去。
在这里插入图片描述

图形API的异常抛出

当调用一个图形API的函数时,返回值可以代表是否成功,如果没能成功,则希望作为一个“异常”立马抛出,这样就更有利于发现问题。

D3D12官方范例和《DX12龙书》中都有类似的行为:ThrowIfFailed函数。
但不同的是,《龙书》中有更详细的信息:包含了调用的函数、调用的是哪个文件中的哪一行。这样,ThrowIfFailed就不能是一个函数,而是一个了,因为需要用到__FILE____LINE__这两个内建的宏。
我希望仿照《龙书》建立类似的机制:


首先,我定义一个APICallErrorInfo作为随后抛出的异常:

#pragma once

#include<string>

//图形接口API调用失败信息
class APICallErrorInfo
{
	std::string FunctionCallString;	//调用函数的代码字符串
	std::string ReturnCodeString;		//调用失败的返回值字符串
	std::string FileName;				//文件名
	int LineNumber;			//行数

public:
	APICallErrorInfo(const char* InFunctionCallString
		, const char* InReturnCodeString
		, const char* InFileName
		, int InLineNumber)
	{
		FunctionCallString = InFunctionCallString;
		ReturnCodeString = InReturnCodeString;
		FileName = InFileName;
		LineNumber = InLineNumber;
	}

	std::string ToString()
	{
		std::string result = ""; 
		
		result += "调用失败:\n";
		result += FunctionCallString;
		result += "\n";
		
		result += "返回值:\n";
		result += ReturnCodeString;
		result += "\n";
		
		result += "代码位置:\n";
		result += FileName;
		result += ":";
		
		char int_str[64] = {};
		sprintf_s(int_str, "%d", LineNumber);
		result += int_str;

		return result;
	}
};

然后在Application::Run()中捕捉这个异常,并用MessageBox弹出小窗口。

void Application::Run()
{
    try
    {
        InitWindow();
        InitRenderer();
        MainLoop();
    }
    catch (APICallErrorInfo error)
    {
        MessageBoxA(nullptr, error.ToString().c_str(), "调用图形API失败", MB_OK);
    }
	
}
Vulkan的ThrowIfFailed

首先,我需要一个能将VkResult转换为字符串的函数,这个函数体的代码我用《制作一个小工具:自动生成“将特定枚举值转换成字符串的C++函数”的代码》中的工具自动生成。

ThrowIfFailed宏定义如下:

#define ThrowIfFailed(FunctionCallResult)\
{\
	if (FunctionCallResult != VK_SUCCESS)\
	{\
		throw APICallErrorInfo(#FunctionCallResult,GetEnumString_VkResult(FunctionCallResult),__FILE__,__LINE__);\
	}\
}

作为测试,在创建VulkanInstance的时候,故意将extensions中放入一个错误的名字:

//创建Instance的信息:
VkInstanceCreateInfo createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
createInfo.enabledLayerCount = 0;//暂时没有ValidationLayers

auto extensions = getRequiredExtensions();
extensions[0] = "Wrong Extension";//故意放一个错误的Extension来触发异常
createInfo.enabledExtensionCount = extensions.size();
createInfo.ppEnabledExtensionNames = extensions.data();

//创建Instance
ThrowIfFailed(vkCreateInstance(&createInfo, nullptr, Instance.replace())
	,"创建VulkanInstance");

便会跳出窗口
在这里插入图片描述

DirectX的ThrowIfFailed

类似,我也需要一个能将HRESULT转换为更具提示性信息的函数,这个函数体的代码我用《制作一个小工具:自动生成“获得特定HRESULT对应信息的函数”的代码》中的工具自动生成了。

ThrowIfFailed宏定义如下:

#define ThrowIfFailed(FunctionCallResult)\
{\
	if (FAILED(FunctionCallResult))\
	{\
		throw APICallErrorInfo(#FunctionCallResult, GetHRESULTMessage(FunctionCallResult),__FILE__,__LINE__);\
	}\
}

作为测试,我在CommandList的时候,故意让参数和CommandAllocator的不一致:

//创建 CommandAllocator
Device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&CommandAllocator));

//创建 command list.(type参数故意和CommandAllocator不一致来引发异常)
ThrowIfFailed(Device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_BUNDLE, CommandAllocator.Get(), nullptr, IID_PPV_ARGS(&CommandList)));

在这里插入图片描述

图形API自身的debug机制

Vulkan的“validation layers”

关于什么是validation layers官方教程中有如下介绍:

What are validation layers?
The Vulkan API is designed around the idea of minimal driver overhead and one
of the manifestations of that goal is that there is very limited error checking in
the API by default. Even mistakes as simple as setting enumerations to incorrect
values or passing null pointers to required parameters are generally not explicitly
handled and will simply result in crashes or undefined behavior. Because Vulkan
requires you to be very explicit about everything you’re doing, it’s easy to make
many small mistakes like using a new GPU feature and forgetting to request it
at logical device creation time.
However, that doesn’t mean that these checks can’t be added to the API. Vulkan
introduces an elegant system for this known as validation layers. Validation
layers are optional components that hook into Vulkan function calls to apply
additional operations. Common operations in validation layers are:
• Checking the values of parameters against the specification to detect misuse
• Tracking creation and destruction of objects to find resource leaks
• Checking thread safety by tracking the threads that calls originate from
• Logging every call and its parameters to the standard output
• Tracing Vulkan calls for profiling and replaying

简单来说,validation layers是Vulkan的一个可选内容,提供了一层检测问题的封装。现在所用的validation layers就是【LunarG】的VulkanSDK所提供的。其实他的原理不复杂,就是在调用“真正”的API之前,先做一些检测,如果发现问题则打印log。例如,其中的一个封装可能如下:

VkResult vkCreateInstance(const VkInstanceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkInstance* instance) 
{
	if (pCreateInfo == nullptr || instance == nullptr)
	{
		log("Null pointer passed to required parameter!");
		return VK_ERROR_INITIALIZATION_FAILED;
	}

	return real_vkCreateInstance(pCreateInfo, pAllocator, instance);
}

其配置方式,依照官方教程的【Using validation layers】,不过我并没有按照【Message callback】的方法设置回调函数。


配置好之后,我立马可以看到控制台中输出的错误:
在这里插入图片描述
信息一直在输出,可以判断有问题的操作在每帧都执行了。

从中大概能依稀看出两个错误:
1.

VUID-vkBeginCommandBuffer-commandBuffer-00050(ERROR / SPEC): msgNum: -1303445259 - Validation Error: [ VUID-vkBeginCommandBuffer-commandBuffer-00050 ] Object 0: handle = 0x27df8baa2a8, type = VK_OBJECT_TYPE_COMMAND_BUFFER; Object 1: handle = 0x731f0f000000000a, type = VK_OBJECT_TYPE_COMMAND_POOL; | MessageID = 0xb24f00f5 | Call to vkBeginCommandBuffer() on VkCommandBuffer 0x27df8baa2a8[] attempts to implicitly reset cmdBuffer created from VkCommandPool 0x731f0f000000000a[] that does NOT have the VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT bit set. The Vulkan spec states: If commandBuffer was allocated from a VkCommandPool which did not have the VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT flag set, commandBuffer must be in the initial state (https://vulkan.lunarg.com/doc/view/1.2.148.0/windows/1.2-extensions/vkspec.html#VUID-vkBeginCommandBuffer-commandBuffer-00050)
    Objects: 2
        [0] 0x27df8baa2a8, type: 6, name: NULL
        [1] 0x731f0f000000000a, type: 25, name: NULL

attempts to implicitly reset cmdBuffer created from VkCommandPool 0x731f0f000000000a[] that does NOT have the VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT bit set。
大致意思是CommandPool需要VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT
2.

VUID-vkCmdEndRenderPass-commandBuffer-recording(ERROR / SPEC): msgNum: -308264800 - Validation Error: [ VUID-vkCmdEndRenderPass-commandBuffer-recording ] Object 0: handle = 0x27df8bab2f8, type = VK_OBJECT_TYPE_COMMAND_BUFFER; | MessageID = 0xeda040a0 | You must call vkBeginCommandBuffer() before this call to vkCmdEndRenderPass(). The Vulkan spec states: commandBuffer must be in the recording state (https://vulkan.lunarg.com/doc/view/1.2.148.0/windows/1.2-extensions/vkspec.html#VUID-vkCmdEndRenderPass-commandBuffer-recording)
    Objects: 1
        [0] 0x27df8bab2f8, type: 6, name: NULL

You must call vkBeginCommandBuffer() before this call to vkCmdEndRenderPass()。
意思是必须在vkCmdEndRenderPass之前调用vkBeginCommandBuffer。这确实是目前工程中的错误。

修复后就看不到控制台中报出的错误了。

D3D12的“debug layers”

D3D12启用debug layers相对容易,只需要在创建Device之前:

#if defined(_DEBUG)
    // Enable the debug layer (requires the Graphics Tools "optional feature").
    // NOTE: Enabling the debug layer after device creation will invalidate the active device.
    {
        ComPtr<ID3D12Debug> debugController;
        if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController))))
        {
            debugController->EnableDebugLayer();

            // Enable additional debug layers.
            dxgiFactoryFlags |= DXGI_CREATE_FACTORY_DEBUG;
        }
    }
#endif

(其中的dxgiFactoryFlags是之后创建DXGIFactory时用到)


做个测试,在EndRecordCommandList中故意不关闭Commandlist:

void D3D12Interface::EndRecordCommandList()
{
    // Indicate that the back buffer will now be used to present.
    CommandList->ResourceBarrier(1, 
        &CD3DX12_RESOURCE_BARRIER::Transition(RenderTargets[CurrentBackBufferIndex].Get()
            , D3D12_RESOURCE_STATE_RENDER_TARGET
            , D3D12_RESOURCE_STATE_PRESENT));

    //作为测试,故意不close这个CommandList,期望看到错误的信息
    //CommandList->Close();
}

可在VS的“输出”窗口中看到错误的信息
在这里插入图片描述

D3D12 ERROR: ID3D12GraphicsCommandList::Reset: Reset fails because the command list was not closed. [ EXECUTION ERROR #544: COMMAND_LIST_OPEN]
D3D12 ERROR: ID3D12CommandQueue::ExecuteCommandLists: Command lists must be closed before execution. [ EXECUTION ERROR #837: EXECUTECOMMANDLISTS_OPENCOMMANDLIST]
0x00007FFC788D3B29 处(位于 YaksueGraphics.exe 中)引发的异常: Microsoft C++ 异常: _com_error,位于内存位置 0x00000057624FED80 处。

D3D11的调试机制

在创建Device之前,声明一个flag变量,在启用调试的情况下加入D3D11_CREATE_DEVICE_DEBUG这个flag:

UINT createDeviceFlags = 0;
#if defined(_DEBUG)
    createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif

随后,在创建Device的时候,将flag这个参数传入:

 hr = D3D11CreateDeviceAndSwapChain(NULL, outDriverType, NULL, createDeviceFlags, featureLevels, ARRAYSIZE(featureLevels),
            D3D11_SDK_VERSION, &swapchainDescription, &SwapChain, &Device, &outFeatureLevel, &ImmediateContext);

即可启用调试了。


作为测试,在创建RenderTargetView的时候故意输入错误的参数,期望获得错误信息

//输出合并阶段(Output-Merger Stage)设置RenderTarget
ImmediateContext->OMSetRenderTargets(1, &RenderTargetView, NULL);

//故意输入错误的参数,期望获得错误信息
ImmediateContext->OMSetRenderTargets(1, 0, NULL);

随后即可在VS的输出窗口中看到错误信息:
在这里插入图片描述

D3D11 CORRUPTION: ID3D11DeviceContext::OMSetRenderTargets: Second parameter corrupt or unexpectedly NULL. [ MISCELLANEOUS CORRUPTION #14: CORRUPTED_PARAMETER2]

OpenGL的调试机制

关于OpenGL的调试,我这里是使用 glDebugMessageCallback 函数。它设置了一个“回调函数”,用来接收错误信息。
而关于这个“回调函数”,我使用了 教程:调试 - LearnOpenGL CN 中的函数,我将其拷贝过来命名为“DebugCallback”。
然后,在初始化阶段:

//启用调试
#if defined(_DEBUG)
    glDebugMessageCallback(&DebugCallback, nullptr);
#endif

并在创建窗口之前调用:(告诉GLFW启用调试)

#if defined(_DEBUG)
        glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GL_TRUE);
#endif

作为测试,在Clear中故意调用一个函数并传入错误的参数来触发调试的函数

void OpenGLInterface::Clear(float r, float g, float b, float a)
{
    //清理屏幕所用的颜色:
    glClearColor(r, g, b, a);
    //清理屏幕
    glClear(GL_COLOR_BUFFER_BIT);

    //作为测试,故意调用一个函数并传入错误的参数来触发调试的函数
    glBindTexture(0, 0);
}

可看到控制台输出了错误信息:
在这里插入图片描述

后续

  • 虽然建立起了ThrowIfFailed机制,但是当前工程中的很多地方还没有使用上,我准备在随后回顾一遍接口时候补充上。
  • 各个图形API自身的debug机制所返回的信息我看着还比较晦涩,也有学习空间。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值