Vulkan学习笔记【一】 创建窗口与实例

Vulkan介绍

        Vulkan 是作为一个跨平台的图形 API 设计的。以往许多图形 API 采用固定功能渲染管线设计,应用程序按照一 定格式提交顶点数据,配置光照和着色选项。 随着显卡架构逐渐成熟,提供了越来越多的可编程功能,这些功能被集成到原有的 API 中。造成驱动程序要做的工 作越来越复杂,应用程序开发者要处理的兼容性问题也越来越多。随着移动浪潮到来,人们对移动 GPU 的要求也 越来越高,但以往的图形 API 不能够进行更加精准地控制来提升效率,对多线程的支持也非常不足,导致没有发挥 出图形硬件真正的潜力。 由于没有历史包袱,Vulkan 完全按照现代图形架构设计,提供了更加详细的 API 给开发者,大大减少了驱动程序 的开销,允许多个线程并行创建和提交指令,使用标准化的着色器字节码,将图形和计算功能进行统一。

使用GLFW创建窗口

        Vulkan 可以在完全没有窗口的情况下工作,通常,在离屏渲染时会这样做。但一般而言,还是需要一个窗口来显示 渲染结果给用户。接下来,我们要完成的就是窗口相关操作。glfwInit 函数来初始化 GLFW 库,由于 GLFW 库最初是为 OpenGL 设计的,所 以我们需要显式地设置 GLFW 阻止它自动创建 OpenGL 上下文:

glfwInit();
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);

VkApplicationInfo——指定应用程序信息的结构

        在创建Vulkan应用时,首先我们需要指定应用的一些信息(可选)

// Provided by VK_VERSION_1_0
typedef struct VkApplicationInfo {
    VkStructureType    sType;      //标识该结构的VkStructureType值
    const void*        pNext;      //为NULL或指向扩展此结构的结构的指针。目前Vulkan规范要求必须为BuLL
    const char*        pApplicationName;      //是NULL或者是指向一个以空结尾的UTF-8字符串的指针,该字符串包含应用程序的名称。
    uint32_t           applicationVersion;   //一个无符号整数变量,其中包含开发人员提供的应用程序的版本号。
    const char*        pEngineName;      //是NULL或指向一个以空结尾的UTF-8字符串的指针,该字符串包含用于创建应用程序的引擎(如果有的话)的名称。
    uint32_t           engineVersion;   //是一个无符号整数变量,其中包含用于创建应用程序的引擎的开发人员提供的版本号。
    uint32_t           apiVersion;   //必须是应用程序设计使用的Vulkan的最高版本。在创建实例对象时,忽略apiVersion中指定的补丁版本号。实例的变体版本必须与apiVersion中请求的版本匹配。
} VkApplicationInfo;

如果apiVersion大于1.0,Vulkan 1.0实现需要返回VK_ERROR_INCOMPATIBLE_DRIVER。支持Vulkan 1.1或更高版本的实现不能为apiVersion的任何值返回VK_ERROR_INCOMPATIBLE_DRIVER。

  • Vulkan 1.0可以与实例和所有物理设备一起使用。
  • Vulkan 1.1可以与实例以及支持Vulkan 1.1和Vulkan 1.2的物理设备一起使用。
  • Vulkan 1.2可与支持Vulkan 1.2的物理设备配套使用

VkInstance——指定新创建实例的参数

// Provided by VK_VERSION_1_0
typedef struct VkInstanceCreateInfo {
    VkStructureType             sType;    //标识该结构的VkStructureType值。
    const void*                 pNext;    //为NULL或指向扩展该结构的结构的指针。
    VkInstanceCreateFlags       flags;    //VkInstanceCreateFlagBits的位掩码,指示实例的行为。
    const VkApplicationInfo*    pApplicationInfo;  //为NULL或指向VkApplicationInfo结构体的指针。如果不是NULL,则此信息有助于实现识别应用程序类固有的行为
    uint32_t                    enabledLayerCount;  //启用的校验层数
    const char* const*          ppEnabledLayerNames;  //一个指向数组的指针,该数组由enabledLayerCount以空结束的UTF-8字符串组成,其中包含要为创建的实例启用的层名。这些层按照它们在这个数组中列出的顺序加载,第一个数组元素是最接近应用程序的,最后一个数组元素是最接近驱动程序的。
    uint32_t                    enabledExtensionCount;   //要启用的全局扩展的数量。
    const char* const*          ppEnabledExtensionNames;  //一个指向enabledExtensionCount大小的数组的指针,该数组包含了要启用的扩展名,以空字符结尾的UTF-8字符串。
} VkInstanceCreateInfo;

        为了捕获在创建或销毁实例时发生的事件,应用程序可以使用VkDebugReportCallbackCreateInfoEXT结构或VkDebugUtilsMessengerCreateInfoEXT结构链接到vkCreateInstance的VkInstanceCreateInfo结构的pNext元素。这个回调只在vkCreateInstance和vkDestroyInstance调用期间有效。使用vkCreateDebugReportCallbackEXT或vkCreateDebugUtilsMessengerEXT创建持久的回调对象。

应用程序可以通过将VkDirectDriverLoadingListLUNARG结构体添加到vkCreateInstance的VkInstanceCreateInfo结构体的pNext元素中来添加额外的驱动程序。

注意:

  • 如果VkInstanceCreateInfo的pNext包含VkDebugReportCallbackCreateInfoEXT结构,则ppEnabledExtensionNames中已启用的扩展列表必须包含VK_EXT_debug_report
  • 如果VkInstanceCreateInfo的pNext包含VkDebugUtilsMessengerCreateInfoEXT结构,则ppEnabledExtensionNames中已启用的扩展列表必须包含VK_EXT_debug_utils
  • 如果VkInstanceCreateInfo的pNext包含VkDirectDriverLoadingListLUNARG结构,则ppEnabledExtensionNames中已启用的扩展列表必须包含VK_LUNARG_direct_driver_loading
  • pNext中任何结构(包括此结构)的每个pNext成员必须为NULL或指向VkDebugReportCallbackCreateInfoEXT, VkDebugUtilsMessengerCreateInfoEXT, VkDirectDriverLoadingListLUNARG, VkExportMetalObjectCreateInfoEXT, VkValidationFeaturesEXT或VkValidationFlagsEXT的有效实例的指针
  • 如果flags设置了VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR位,则ppEnabledExtensionNames中启用的扩展列表必须包含VK_KHR_portability_enumeration

实例扩展

        在创建实例的过程过程中,我们需要指定使用的全局扩展,Vulkan 是 平台无关的 API,所以需要一个和窗口系统交互的扩展。GLFW 库包含了一个可以返回这一扩展的函数,我们可以直接使用它。

uint32_t glfwExtensionCount = 0;
const char** glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);

校验层

        Vulkan API 的设计是紧紧围绕最小化驱动程序开销进行的,所以,默认情况下,Vulkan API 提供的错误检查 能非常有限。很多很基本的错误都没有被 Vulkan 显式地处理,遇到错误程序会直接崩溃或者发生未被明确定义的行为。Vukan 需要我们显式地定义每一个操作,所以就很容易在使用过程中产生一些小错误,比如使用了一个新的 GPU 特性,却忘记在逻辑设备创建时请求这一特性。 然而,这并不意味着我们不能将错误检查加入 API 调用。Vulkan 引入了校验层来优雅地解决这个问题。校验层是 一个可选的可以用来在 Vulkan API 函数调用上进行附加操作的组件。校验层常被用来做下面的工作:

  • 检测参数值是否合法
  • 追踪对象的创建和清除操作,发现资源泄漏问题 • 追踪调用来自的线程,检测是否线程安全。
  • 将 API 调用和调用的参数写入日志
  • 追踪 API 调用进行分析和回放

        要使用验证层,需要创建VkInstance时在VkInstanceCreateInfo中指定要使用的验证层,但必须要确保instance支持该验证层,这要通过使用API查询来实现。第一次调用vkEnumerateInstanceLayerProperties查询所有受支持的验证层的数量,第二次调用vkEnumerateInstanceLayerProperties查询受支持的验证层的具体信息。最后查询应用程序希望启用的验证层是否在受支持的列表中。过去,Vulkan 提供了多个验证层,而这些验证层需要按特定顺序启用。从 1.1.106.0 Vulkan SDK 版本开始,您的应用只需启用单个验证层 VK_LAYER_KHRONOS_validation,即可获取旧版验证层的所有功能。

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

#ifdef NDEBUG
    const bool enableValidationLayers = false;
#else
    const bool enableValidationLayers = true;
#endif

为了使验证层检查到的错误信息能被程序员看到,还需要一种把错误信息进行输出的方法(比如输出到控制台)。这就需要在开启验证层时,必须同时开启VK_EXT_DEBUG_UTILS_EXTENSION_NAME扩展(或者VK_EXT_DEBUG_REPORT_EXTENSION_NAME扩展)。然后在结构体VkDebugUtilsMessengerCreateInfoEXT(或VkDebugReportCallbackCreateInfoEXT)中指定如何处理错误消息的回调函数,最后使用创建好的VkInstance创建VkDebugUtilsMessengerEXT(或VkDebugReportCallbackEXT)

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);
    }

    return extensions;
}

回调函数 

static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback(
    VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
    VkDebugUtilsMessageTypeFlagsEXT messageType,
    const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData,
    void* pUserData) {

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

    return VK_FALSE;
}

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

  • 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
}

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:进行了可能影响 Vulkan 性能的行为

pCallbackData 参数是一个指向 VkDebugUtilsMessengerCallbackDataEXT 结构体的指针,这一结构体包含了 下面这些非常重要的成员:

  • pMessage:一个以 null 结尾的包含调试信息的字符串
  • pObjects:存储有和消息相关的 Vulkan 对象句柄的数组
  • objectCount:数组中的对象个数

        回调函数返回了一个布尔值,用来表示引发校验层处理的 Vulkan API 调用是否被中断。如果返回值为 true,对应 Vulkan API 调用就会返回 VK_ERROR_VALIDATION_FAILED_EXT 错误代码。通常,只在测试校验层本 身时会返回 true,其余情况下,回调函数应该返回 VK_FALSE。

        现在剩下的就是告诉Vulkan回调函数。也许有些令人惊讶的是,即使是Vulkan中的调试回调也是用一个需要显式创建和销毁的句柄来管理的。这样的回调是调试消息的一部分,您可以根据需要拥有任意多个回调。

VkDebugUtilsMessengerCreateInfoEXT - 指定新创建的调试消息的参数的结构

// Provided by VK_EXT_debug_utils
typedef struct VkDebugUtilsMessengerCreateInfoEXT {
    VkStructureType                         sType;
    const void*                             pNext;
    VkDebugUtilsMessengerCreateFlagsEXT     flags;
    VkDebugUtilsMessageSeverityFlagsEXT     messageSeverity;  //messageSeverity是VkDebugUtilsMessageSeverityFlagBitsEXT的位掩码,指定将导致调用此回调的事件的严重性。
    VkDebugUtilsMessageTypeFlagsEXT         messageType;  //messageType是VkDebugUtilsMessageTypeFlagBitsEXT的位掩码,指定哪种类型的事件将导致调用该回调函数。
    PFN_vkDebugUtilsMessengerCallbackEXT    pfnUserCallback;  //pfnUserCallback是要调用的应用程序回调函数。
    void*                                   pUserData;  //pUserData是要传递给回调的用户数据
} VkDebugUtilsMessengerCreateInfoEXT;

 填写完结构体信息后,我们将它作为一个参数调用 vkCreateDebugUtilsMessengerEXT 函数来创建 VkDebugUtilsMessengerEXT 对象。由于 vkCreateDebugUtilsMessengerEXT 函数是一个扩展函数,不会被 Vulkan 库自动加载,所以需要我们自己使用 vkGetInstanceProcAddr 函数来加载它。在这里,我们创建了一个代理函数, 来载入 vkCreateDebugUtilsMessengerEXT 函数:

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类似,该函数需要显式加载。

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

 实例创建与销毁的调试

        尽管我们现在已经在程序中添加了带有验证层的调试,但我们还没有涵盖所有内容。vkCreateDebugUtilsMessengerEXT调用要求已经创建了一个有效的实例,并且vkDestroyDebugUtilsMessengerEXT必须在实例销毁之前调用。这使得我们目前无法调试vkCreateInstance和vkDestroyInstance调用中的任何问题。但是,如果您仔细阅读扩展文档,就会发现有一种方法可以专门为这两个函数调用创建单独的调试utils消息。它需要你简单地传递一个指针到VkInstanceCreateInfo的pNext扩展字段中的VkDebugUtilsMessengerCreateInfoEXT结构体。

VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{};
populateDebugMessengerCreateInfo(debugCreateInfo);
createInfo.pNext = &debugCreateInfo;

运行代码 

#define GLFW_INCLUDE_VULKAN
#include<GLFW/glfw3.h>
#include <exception>
#include <iostream>
#include <vector>

#define WIDTH 800
#define HEIGHT 600

#if NDEBUG
const bool enableValidationLayers = false;
#else
const bool enableValidationLayers = true;
#endif // enableValidationLayers

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

VkResult CreateDebugUtilsMessengerEXT(VkInstance instance, const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDebugUtilsMessengerEXT* pCallback) {
	auto func = (PFN_vkCreateDebugUtilsMessengerEXT)
		vkGetInstanceProcAddr(instance, "vkCreateDebugUtilsMessengerEXT");
	if (func != nullptr) {
		return func(instance, pCreateInfo, pAllocator, pCallback);
	}
	else {
		return VK_ERROR_EXTENSION_NOT_PRESENT;
	}
}

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

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

private:
	GLFWwindow* window;

	VkInstance instance;
	VkDebugUtilsMessengerEXT debugMessenger;


	void initWindow() {
		glfwInit();

		glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
		glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);

		window = glfwCreateWindow(WIDTH, HEIGHT, "Learn Vulkan", nullptr, nullptr);
	}

	void initVulkan() {
		createInstance();
		setupDebugMessenger();        //设置vulkan使用过程中的调试信息
	}

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

	void cleanup() {
		if (enableValidationLayers) {
			DestroyDebugUtilsMessengerEXT(instance, debugMessenger, nullptr);
		}

		vkDestroyInstance(instance, nullptr);

		glfwDestroyWindow(window);

		glfwTerminate();
	}

	void createInstance() {
		if (enableValidationLayers && !checkValidationLayerSupport()) {
			throw std::runtime_error("validation layers requested, but not available");
		}

		VkApplicationInfo appInfo = {};
		appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
		appInfo.pApplicationName = "Hello Vulkan";
		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;

		std::vector<const char*> extensions = getRequireExtensions();
		createInfo.enabledExtensionCount = extensions.size();
		createInfo.ppEnabledExtensionNames = extensions.data();

		if (enableValidationLayers) {
			createInfo.enabledLayerCount = validationLayers.size();
			createInfo.ppEnabledLayerNames = validationLayers.data();

			// 调试vkCreateInstance和vkDestroyInstance
			VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{};
			populateDebugMessengerCreateInfo(debugCreateInfo);
			createInfo.pNext = &debugCreateInfo;
		}
		else {
			createInfo.enabledLayerCount = 0;
			createInfo.pNext = nullptr;
		}

		VkResult result = vkCreateInstance(&createInfo, nullptr, &instance);
		if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) {
			throw std::runtime_error("Failed to create Vulkan Instance!");
		}
	}

	void populateDebugMessengerCreateInfo(VkDebugUtilsMessengerCreateInfoEXT& createInfo) {
		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;
		createInfo.pfnUserCallback = debugCallback;
	}

	void setupDebugMessenger() {
		if (!enableValidationLayers) return;

		VkDebugUtilsMessengerCreateInfoEXT createInfo;
		populateDebugMessengerCreateInfo(createInfo);

		if (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) {
			throw std::runtime_error("failed to set up debug messenger!");
		}
	}


	std::vector<const char*> getRequireExtensions() {
		uint32_t glfwExtensionCount = 0;
		const char** glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);

		std::vector<const char*> extensions(glfwExtensions, glfwExtensions + glfwExtensionCount);
		if (enableValidationLayers) {
			extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
		}
		return extensions;
	}

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

		std::vector<VkLayerProperties> availabelLayers(layertCount);
		vkEnumerateInstanceLayerProperties(&layertCount, availabelLayers.data());

		for (auto& validationlayer : validationLayers) {
			bool layerFound = false;
			for (auto& availabelayer : availabelLayers) {
				if (strcmp(validationlayer, availabelayer.layerName) == 0) {
					layerFound = true;
					break;
				}
			}

			if (!layerFound) {
				return false;
			}
		}

		return true;
	}

	static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, VkDebugUtilsMessageTypeFlagsEXT messageType, const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData) {
		std::cerr << "validation layer: " << pCallbackData->pMessage << std::endl;
		return VK_FALSE;
	}

};

int main() {
	HelloVulkanApplication helloVulkan;

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

	return EXIT_SUCCESS;
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

w还是晒太阳吧

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值