Vulkan Tutorial 1 实例和物理设备

目录

0 基本代码

1 Instance

2 验证层

3 物理设备和队列系列

4 逻辑设备和队列


0 基本代码

首先包括LunarG SDK的Vulkan头,它提供了函数、结构和枚举。stdexcept'和iostream’头文件被包括在内,用于报告和传播错误

  1. 函数将被initVulkan函数调用
  2. 进入主循环,开始渲染帧mainLoop
  3. 一旦窗口关闭,mainLoop返回,取消分配在cleanup函数中使用的资源
  4. 如果在执行过程中发生任何致命的错误,将抛出一个std::runtime_error异常
#include <vulkan/vulkan.h>

#include <iostream>
#include <stdexcept>
#include <cstdlib>

class HelloTriangleApplication {
public:
    void run() {
        initVulkan();
        mainLoop();
        cleanup();
    }

private:
    void initVulkan() {

    }

    void mainLoop() {

    }

    void cleanup() {

    }
};

int main() {
    HelloTriangleApplication app;

    try {
        app.run();
    } catch (const std::exception& e) {
        std::cerr << e.what() << std::endl;
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

之后的每一节都会增加一个新的函数,该函数将从initVulkan中调用,并在cleanup中为需要在最后释放的私有类成员增加一个或多个新的Vulkan对象。-显式销毁

Vulkan对象要么用vkCreateXXX这样的函数直接创建,要么通过vkAllocateXXX这样的函数分配给另一个对象。在确保一个对象不再被用于任何地方后,你需要用对应的vkDestroyXXXvkFreeXXX销毁它。

pAllocator:这是一个可选的参数,允许你指定自定义内存分配器的回调。

#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>

void run() 
{
    initWindow();
    initVulkan();
    mainLoop();
    cleanup();
}

void initWindow()
{
    glfwInit();
//初始化GLFW库。
//GLFW最初被设计为创建一个OpenGL上下文,告诉它不要使用下面的调用创建OpenGL上下文
    glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
    glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
//初始化该窗口
    window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
}

void mainLoop() 
{
    while (!glfwWindowShouldClose(window)) {
        glfwPollEvents();
    }
}

void cleanup()
{
    glfwDestroyWindow(window);

    glfwTerminate();
}

GLFW将包括它自己的定义并自动加载Vulkan头。 为了保持应用程序的运行,直到错误发生或窗口关闭,我们需要在mainLoop函数中添加一个事件循环,它循环并检查事件,如按下X按钮,直到窗口被用户关闭。一旦窗口被关闭,我们需要通过销毁它和终止GLFW本身来清理资源。

1 Instance

Vulkan API使用vkInstance对象来存储所有每个应用的状态。应用程序必须在执行任何其他Vulkan操作之前创建一个Vulkan实例,基本的Vulkan架构看起来是这样的:
 

创建一个instance来初始化Vulkan库,实例是你的应用程序和Vulkan库之间的连接,创建它需要向驱动指定一些关于你的应用程序的细节。

void initVulkan() 
{
    createInstance();
}

void createInstance() {
//填写关于我们应用程序的一些信息
    VkApplicationInfo appInfo{};
//sType成员中明确指定类型
    appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
    appInfo.pApplicationName = "Hello Triangle";
    appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
    appInfo.pEngineName = "No Engine";
    appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
    appInfo.apiVersion = VK_API_VERSION_1_0;

//我们要使用哪些全局扩展和验证层
    VkInstanceCreateInfo createInfo{};
    createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
    createInfo.pApplicationInfo = &appInfo;

//Vulkan是一个与平台无关的API,这意味着你需要一个扩展来与窗口系统对接。
//GLFW有一个方便的内置函数,可以返回它所需要的扩展,我们可以将其传递给结构.
uint32_t glfwExtensionCount = 0;
const char** glfwExtensions;

glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);

createInfo.enabledExtensionCount = glfwExtensionCount;
createInfo.ppEnabledExtensionNames = glfwExtensions;


//启用全局验证层
    createInfo.enabledLayerCount = 0;

//已经指定了Vulkan创建实例所需要的一切,我们最终可以执行vkCreateInstance调用:
if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) {
//几乎所有的Vulkan函数都会返回一个VkResult类型的值,这个值要么是VK_SUCCESS
//要么是一个错误代码
    throw std::runtime_error("failed to create instance!");
}

}

Vulkan中对象创建函数参数遵循的一般模式是。

  • 指向带有创建信息的结构的指针
  • 指向自定义分配器回调的指针,在本教程中总是nullptr
  • 指向存储新对象句柄的变量的指针

为了在创建实例之前检索支持的扩展列表,有一个vkEnumerateInstanceExtensionProperties函数。它需要一个存储扩展数量的变量指针和一个VkExtensionProperties数组来存储扩展的细节。它还需要一个可选的第一个参数,允许我们通过一个特定的验证层来过滤扩展,我们现在将忽略这个参数。

//首先需要知道有多少个
uint32_t extensionCount = 0;
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr);

//分配一个数组来保存扩展的细节
std::vector<VkExtensionProperties> extensions(extensionCount);

//查询扩展的详细信息
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, extensions.data());

//每个VkExtensionProperties结构包含一个扩展的名称和版本
std::cout << "available extensions:\n";

for (const auto& extension : extensions) {
    std::cout << '\t' << extension.extensionName << '\n';
}

 VkInstance应该只在程序退出前销毁。它可以在cleanup中用vkDestroyInstance函数销毁。vkDestroyInstance函数的参数是直接的。

2 验证层

Vulkan要求你对你所做的一切都要非常明确

Vulkan为此引入了一个被称为验证层的优雅系统。验证层是可选的组件,它与Vulkan函数调用挂钩以应用额外的操作。验证层中的常见操作是。

  • 对照规范检查参数值,以发现误用情况
  • 跟踪对象的创建和销毁以发现资源泄漏
  • 通过跟踪调用来源的线程来检查线程的安全性
  • 将每个调用及其参数记录到标准输出中
  • 跟踪Vulkan调用以进行分析和回放
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);
}

验证层可以自由堆叠, Vulkan没有内置任何验证层,但LunarG Vulkan SDK提供了一套不错的验证层,可以检查常见的错误。只有当验证层被安装到系统上时,才能使用它们。

  1. 首先在程序中添加两个配置变量,以指定要启用的层和是否启用它们。
  2. 添加一个新的函数checkValidationLayerSupport来检查所有请求的图层是否可用
  3. 检查validationLayers中的所有图层是否存在于availableLayers列表中
  4. 最后,修改VkInstanceCreateInfo结构实例,以包括验证层名称(如果它们被启用)
const uint32_t WIDTH = 800;
const uint32_t HEIGHT = 600;

const std::vector<const char*> validationLayers = {
    "VK_LAYER_KHRONOS_validation"
};

#ifdef NDEBUG
//NDEBUG宏是C++标准的一部分,意味着 “非调试”。
    const bool enableValidationLayers = false;
#else
    const bool enableValidationLayers = true;
#endif


bool checkValidationLayerSupport() {
    uint32_t layerCount;
    vkEnumerateInstanceLayerProperties(&layerCount, nullptr);

    std::vector<VkLayerProperties> availableLayers(layerCount);
    vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data());

//检查validationLayers中的所有图层是否存在于availableLayers列表中
    for (const char* layerName : validationLayers) {
            bool layerFound = false;

            for (const auto& layerProperties : availableLayers) {
                if (strcmp(layerName, layerProperties.layerName) == 0) {
                    layerFound = true;
                    break;
                }
            }

            if (!layerFound) {
                return false;
            }
        }

        return true;
}

 消息回调使用VK_EXT_debug_utils扩展来设置一个带有回调的调试信使。

  1. 首先创建一个getRequiredExtensions函数,它将根据是否启用验证层来返回所需的扩展列表
std::vector<const char*> getRequiredExtensions() {
    uint32_t glfwExtensionCount = 0;
    const char** glfwExtensions;
    glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);

    std::vector<const char*> extensions(glfwExtensions, glfwExtensions + glfwExtensionCount);

    if (enableValidationLayers) {
        extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
//使用了VK_EXT_DEBUG_UTILS_EXTENSION_NAME宏,它等于字面字符串 VK_EXT_debug_utils
    }

    return extensions;
}

现在让我们看看调试回调函数是什么样子的。用PFN_vkDebugUtilsMessengerCallbackEXT的原型添加一个新的静态成员函数,叫做debugCallbackVKAPI_ATTRVKAPI_CALL确保该函数具有正确的签名,以便Vulkan调用它。

static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback(
//pMessage: 调试信息是一个空尾的字符串
//pObjects: 与该消息相关的Vulkan对象句柄的数组
//objectCount: 数组中对象的数量
//最后,pUserData参数包含一个在设置回调时指定的指针,允许你向它传递你自己的数据。
    VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
    VkDebugUtilsMessageTypeFlagsEXT messageType,
    const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData,
    void* pUserData) {

    std::cerr << "validation layer: " << pCallbackData->pMessage << std::endl;

    return VK_FALSE;//回调返回一个布尔值,表明触发验证层消息的Vulkan调用是否应该被终止。
}
//true,那么该调用将被中止

第一个参数指定消息的严重性,它是以下标志之一。

  • vk_debug_utils_message_severity_verbose_bit_ext: 诊断消息
  • vk_debug_utils_message_severity_info_bit_ext: 像创建资源的信息消息
  • vk_debug_utils_message_severity_warning_bit_ext: 关于不一定是错误的行为的消息,但很可能是您的应用程序中的一个错误。
  • vk_debug_utils_message_severity_error_bit_ext: 关于无效行为的信息,可能导致崩溃

这个枚举的值是这样设置的,你可以使用比较操作来检查一个消息与某个严重程度相比是否相等或更坏,例如:

if (messageSeverity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) {
    // Message is important enough to show
}

我们需要在一个结构中填写关于信使及其回调的细节:这个结构应该被传递给vkCreateDebugUtilsMessengerEXT函数来创建VkDebugUtilsMessengerEXT对象。不幸的是,由于这个函数是一个扩展函数,它不会被自动加载。我们必须自己使用vkGetInstanceProcAddr来查找它的地址。

VkDebugUtilsMessengerCreateInfoEXT createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
//回调被调用的严重程度类型
createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
//消息类型
createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
//回调函数的指针。你可以选择传递一个指向pUserData字段的指针
createInfo.pfnUserCallback = debugCallback;
createInfo.pUserData = nullptr; // Optional

//这个结构应该被传递给vkCreateDebugUtilsMessengerEXT函数来创建VkDebugUtilsMessengerEXT对象
VkResult CreateDebugUtilsMessengerEXT(VkInstance instance, const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDebugUtilsMessengerEXT* pDebugMessenger) {
    auto func = (PFN_vkCreateDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkCreateDebugUtilsMessengerEXT");
    if (func != nullptr) {
        return func(instance, pCreateInfo, pAllocator, pDebugMessenger);
    } else {
        return VK_ERROR_EXTENSION_NOT_PRESENT;
    }
}

VkDebugUtilsMessengerEXT对象也需要通过调用vkDestroyDebugUtilsMessengerEXT来清理。与vkCreateDebugUtilsMessengerEXT类似,该函数需要明确加载。在CreateDebugUtilsMessengerEXT下面创建另一个代理函数。

void DestroyDebugUtilsMessengerEXT(VkInstance instance, VkDebugUtilsMessengerEXT debugMessenger, const VkAllocationCallbacks* pAllocator) {
    auto func = (PFN_vkDestroyDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT");
    if (func != nullptr) {
        func(instance, debugMessenger, pAllocator);
    }
}

现在让我们故意犯一个错误,看看验证层的作用。暂时删除cleanup函数中对DestroyDebugUtilsMessengerEXT的调用,然后运行你的程序。一旦它退出,你应该看到类似这样的东西。 

3 物理设备和队列系列

在通过VkInstance初始化Vulkan库后,我们需要在系统中寻找并选择一个支持我们所需功能的显卡。事实上,我们可以选择任何数量的显卡并同时使用它们,但在本教程中,我们将坚持使用第一个适合我们需求的显卡。

void initVulkan() {
    createInstance();
    setupDebugMessenger();
    pickPhysicalDevice();
}
//我们最终选择的显卡将被存储在一个VkPhysicalDevice句柄中,它被作为一个新的类成员添加
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;



void pickPhysicalDevice() {
//开始时只查询数字
    uint32_t deviceCount = 0;
    vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr);
//如果有0台设备支持Vulkan,那么就没有必要再继续下去了
    if (deviceCount == 0) {
        throw std::runtime_error("failed to find GPUs with Vulkan support!");
    }

//分配一个数组来保存所有的VkPhysicalDevice句柄
    std::vector<VkPhysicalDevice> devices(deviceCount);
    vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data());

    for (const auto& device : devices) 
    {
        if (isDeviceSuitable(device)) 
        {
            physicalDevice = device;
            break;
        }
    }

    if (physicalDevice == VK_NULL_HANDLE)
    {
        throw std::runtime_error("failed to find a suitable GPU!");
    }
}

//每一个进行评估,并检查它们是否适合于我们想要执行的操作
bool isDeviceSuitable(VkPhysicalDevice device) {
    return true;
}

为了评估一个设备的适用性,我们可以从查询一些细节开始。基本的设备属性,如名称、类型和支持的Vulkan版本,可以使用vkGetPhysicalDeviceProperties进行查询。

VkPhysicalDeviceProperties deviceProperties;
vkGetPhysicalDeviceProperties(device, &deviceProperties);

纹理压缩、64位浮点和多视口渲染(对VR有用)等可选特性的支持可以用vkGetPhysicalDeviceFeatures查询:

VkPhysicalDeviceFeatures deviceFeatures;
vkGetPhysicalDeviceFeatures(device, &deviceFeatures);

假设我们认为我们的应用程序只适用于支持几何着色器的专用图形卡。那么 isDeviceSuitable函数会是这样的:

bool isDeviceSuitable(VkPhysicalDevice device) {
    VkPhysicalDeviceProperties deviceProperties;
    VkPhysicalDeviceFeatures deviceFeatures;
    vkGetPhysicalDeviceProperties(device, &deviceProperties);
    vkGetPhysicalDeviceFeatures(device, &deviceFeatures);

    return deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU &&
           deviceFeatures.geometryShader;
}

Queue families

之前已经简单地提到过,Vulkan中几乎所有的操作,从绘图到上传纹理,都需要将命令提交给一个队列。有不同类型的队列,它们来自不同的队列家族,每个队列家族只允许一个子集的命令。例如,可能有一个队列家族只允许处理计算命令,或者一个只允许内存传输相关的命令。

现在我们只想寻找支持图形命令的队列,所以这个函数可以是这样的:

struct QueueFamilyIndices {
//std::optional是一个包装器,在你给它赋值之前不包含任何值。
//在任何时候,你都可以通过调用其has_value()成员函数来查询它是否包含一个值
    std::optional<uint32_t> graphicsFamily;

    bool isComplete() {
        return graphicsFamily.has_value();
    }
    
};

//但是如果一个队列家族不可用呢?我们可以在findQueueFamilies中抛出一个异常
QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) {
    QueueFamilyIndices indices;
    // Assign index to queue families that could be found
    uint32_t queueFamilyCount = 0;
    vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr);

    std::vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);
    vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, queueFamilies.data());

    
    int i = 0;
    for (const auto& queueFamily : queueFamilies) 
    {
        if (queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT) 
        {
            indices.graphicsFamily = i;
        }

        i++;
    }

    return indices;
}

//在isDeviceSuitable函数中使用它作为检查
bool isDeviceSuitable(VkPhysicalDevice device) {
    QueueFamilyIndices indices = findQueueFamilies(device);

    return indices.isComplete();
}

4 逻辑设备和队列

 

 Vulkan 逻辑设备与队列,在选择要使用的物理设备之后,我们需要设置一个逻辑设备用于交互。逻辑设备创建过程与instance创建过程类似,也需要描述我们需要使用的功能。因为我们已经查询过哪些队列簇可用,在这里需要进一步为逻辑设备创建具体类型的命令队列。如果有不同的需求,也可以基于同一个物理设备创建多个逻辑设备。

VkDevice device;//存储逻辑设备句柄。

 添加一个createLogicalDevice函数,从initVulkan中调用。

void createLogicalDevice() 
{
    //要的单个队列家族的队列数量
    QueueFamilyIndices indices = findQueueFamilies(physicalDevice);

    VkDeviceQueueCreateInfo queueCreateInfo{};
    queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
    queueCreateInfo.queueFamilyIndex = indices.graphicsFamily.value();
    queueCreateInfo.queueCount = 1;

    //Vulkan让你为队列分配优先级,以影响命令缓冲区执行的调度
    float queuePriority = 1.0f;
    queueCreateInfo.pQueuePriorities = &queuePriority;
    //查询支持的特性
    VkPhysicalDeviceFeatures deviceFeatures{};

    //首先添加指向队列创建信息和设备特征结构的指针
    createInfo.pQueueCreateInfos = &queueCreateInfo;
    createInfo.queueCreateInfoCount = 1;

//填写主VkDeviceCreateInfo结构
    VkDeviceCreateInfo createInfo{};
    createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;

   createInfo.queueCreateInfoCount = static_cast<uint32_t>(queueCreateInfos.size());
   createInfo.pQueueCreateInfos = queueCreateInfos.data();

   createInfo.pEnabledFeatures = &deviceFeatures;

   createInfo.enabledExtensionCount = static_cast<uint32_t>(deviceExtensions.size());
   createInfo.ppEnabledExtensionNames = deviceExtensions.data();

   if (enableValidationLayers) {
         createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());
            createInfo.ppEnabledLayerNames = validationLayers.data();
   }
   else {
            createInfo.enabledLayerCount = 0;
   }
//实例化这个逻辑设备
   if (vkCreateDevice(physicalDevice, &createInfo, nullptr, &device) != VK_SUCCESS) {
            throw std::runtime_error("failed to create logical device!");
   }
//存储图形队列的句柄
//vkGetDeviceQueue函数来检索每个队列家族的队列柄

        vkGetDeviceQueue(device, indices.graphicsFamily.value(), 0, &graphicsQueue);
        vkGetDeviceQueue(device, indices.presentFamily.value(), 0, &presentQueue);
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值