Vulkan Programming Guide 第一章(3)

来源:互联网 发布:lamp一键安装包ubuntu 编辑:程序博客网 时间:2024/06/07 08:41

对象类型和函数约定

事实上,Vulkan中的所有内容都被表示为一个由句柄引用的对象。句柄分为两大类:可分派对象和不可分散对象。在大多数情况下,这与应用程序无关,仅影响API的结构以及系统级组件(如Vulkan加载器和层)与这些对象的互操作性。
可分派对象是内部包含调度表的对象。这是各种组件使用的功能表,用于确定当您的应用程序调用Vulkan时要执行的代码部分。这些类型的对象通常是较大较复杂的结构,目前由实例(VkInstance),物理设备(VkPhysicalDevice),逻辑设备(VkDevice),命令缓冲区(VkCommandBuffer)和队列(VkQueue)组成)构成。所有其他对象被认为是不可分散的。
任何Vulkan函数的第一个参数总是一个可调度的对象。此规则的唯一例外是与实例的创建和初始化相关的功能。

管理内存

Vulkan提供两种类型的内存:主机内存和设备内存。由Vulkan API创建的对象通常需要一定数量的主机内存。这就是Vulkan实现将存储对象状态以及实现Vulkan API所需的任何数据。资源对象(如缓冲区和图像)需要一定量的设备内存。这是存储在资源中的数据的存储器。
您的应用程序可以管理Vulkan实现的主机内存,并且需要您的应用程序管理设备内存。为此,您将需要创建一个设备内存管理子系统。您可以查询您创建的每个资源,以获得支持所需的内存量和类型。应用程序将分配正确的内存量并将其附加到资源对象,然后才能使用它。在诸如OpenGL这样的更高级别的API中,这个“魔术”是由驱动程序代表你的应用程序执行的。然而,一些应用程序需要非常大量的小资源,而其他应用程序需要较少数量的非常大的资源。一些应用程序在执行过程中创建和销毁资源,而其他应用程序在启动时创建所有资源,并且在终止之前不释放它们。
在这些情况下使用的分配策略可能会有很大的不同。没有一刀切的策略。 OpenGL驱动程序不知道您的应用程序将如何运行,因此必须调整分配策略以尝试适应您的使用模式。另一方面,您,应用程序开发人员,您的应用程序的行为准确无误。您可以将资源分成长期和短暂的组。您可以将将一起使用的资源分成少量池化分配。您可以决定应用程序使用的分配策略
重要的是要注意,每个“实时”内存分配会给系统带来一些成本。因此,将分配对象的数量保持在最小值是很重要的。建议设备内存分配器以大块分配内存。许多小资源可以放置在少得多的设备内存块内。第2章“内存和资源”中讨论了设备内存分配器的一个例子,它们更详细地讨论了内存分配。

Vulkan的多线程

支持多线程应用程序是Vulkan设计的一个组成部分。 Vulkan通常假定应用程序将确保没有两个线程同时在同一对象上进行变异。这被称为外部同步。在Vulkan的性能关键部分(如构建命令缓冲区)中的绝大多数Vulkan命令根本不提供同步。
为了具体定义各种Vulkan命令的线程命令,必须保护不受主机并发访问的每个参数被标记为外部同步。在某些情况下,对象或其他数据的句柄将被包含在数据结构中,包含在数组中,或以其他方式传递给命令。这些参数也必须是外部同步的。
这样做的意图是,Vulkan实现不需要在内部采用互斥体或使用其他同步原语来保护数据结构。这意味着多线程程序很少停止或跨线程阻塞。
除了要求主机在线程间使用共享对象的同时访问之外,Vulkan还提供了许多更高级别的功能,专门用于允许线程执行工作而不会相互阻止。这些包括以下内容:
•主机内存分配可以通过传递给对象创建功能的主机内存分配结构来处理。通过使用每个线程的分配器,该分配器中的数据结构不需要被保护。主机内存分配器在第2章“内存和资源”中有介绍。
•命令缓冲区是从池分配的,对池的访问是外部同步的。如果应用程序每个线程使用单独的命令池,则可以从这些池中分配命令缓冲区,而不会相互阻止。命令缓冲区和池在第3章“队列和命令”中有介绍。
•描述符按描述符池分配。描述符是在设备上运行的着色器使用的资源的表示。它们在第6章“着色器和管道”中有详细描述。如果每个线程都使用单独的池,则可以从这些池中分配描述符集,而不会使线程相互阻塞。
•二级命令缓冲区允许并行生成大型renderpass(必须包含在单个命令缓冲区中)的内容,然后在从主命令缓冲区调用时分组。第13章“Multipass Rendering”中详细介绍了二级命令缓冲区。
当您构建一个非常简单的单线程应用程序时,创建从其分配对象的池的需求可能看起来很多不必要的间接。然而,随着应用程序在线程数量上的扩展,这些对象是实现高性能的必不可少的。
在本书的其余部分中,关于线程的任何特殊要求将在引入命令时注明。

数学概念

计算机图形学和大多数异构计算应用程序都是以数学作为基础。 大多数Vulkan设备都是基于非常强大的计算处理器。 在撰写本文时,即使是适中的移动处理器也能够提供许多千兆位的处理能力,而高端桌面和工作站处理器则提供了数千倍的数字处理能力。 因此,真正有趣的应用程序将建立在偏重数学的着色器上。 此外,Vulkan处理管道的几个固定功能部分建立在硬连接到设备和规范中的数学概念上。

向量和矩阵

任何图形应用程序的基本构建块之一是向量。无论它们是位置,方向,颜色还是其他数量的表示,在整个图形文献中都使用向量。向量的一种常见形式是均匀矢量,它是一个空间中的矢量,该空间的一维高于其所代表的数量。这些向量用于存储投影坐标。将均匀矢量乘以任何标量产生表示相同投影坐标的新矢量。要投射一个点矢量,通过其最后一个分量进行分割,产生x,y,z,1.0形式的向量(对于四分量矢量)。

要将矢量从一个坐标空间转换到另一个坐标空间,将该矢量乘以一个矩阵。正如3D空间中的一点被表示为四分量均匀矢量,在3D均匀矢量上运行的变换矩阵是4×4矩阵。
3D空间中的点通常表示为常规称为x,y,z和w的四个分量的均匀矢量。对于一个点,w分量通常以1.0开始,并且随着向量通过投影矩阵变换而改变。在通过w分量分配之后,该点通过以下变量进行投影:如果没有一个变换是投影变换,则w保持为1.0,除以1.0对矢量没有影响。如果矢量进行投影变换,则w不等于1.0,但是通过它将会投射点并将w返回到1.0。
同时,3D空间中的方向也表示为w分量为0.0的均匀矢量。用正确构造的4×4投影矩阵乘以方向矢量将使w分量保持在0.0,对任何其他分量都不起作用。通过简单地丢弃附加组件,您可以将3D方向向量通过与4D均匀3D点向量相同的变换,并使其经常进行旋转,缩放和其他变换。

坐标系

Vulkan通过将其端​​点或角点表示为3D空间中的点来处理图形基元(如线条和三角形)。这些图元被称为顶点。 Vulkan系统的输入是相对于它们所属对象的原点的3D坐标空间中的顶点坐标(以w分量为1.0表示为均匀矢量)。这个坐标空间被称为对象空间或有时是模型空间。
通常,管道中的第一个着色器将将该顶点转换为视图空间,该空间是相对于查看器的位置。通过将顶点的位置矢量乘以变换矩阵来进行该变换。这通常被称为对象视图矩阵或模型视图矩阵。
有时,需要一个顶点的绝对坐标,例如当找到一个顶点相对于某个其他对象的坐标时。这个全球空间被称为世界空间,是相对于全球起点的顶点的位置。
从视图空间,顶点的位置被转换为剪辑空间。这是Vulkan的几何处理部分使用的最终空间,并且是将顶点推入典型3D应用程序使用的投影空间时通常会转换顶点的空间。剪辑空间是所谓的,因为它是大多数实现执行剪辑的坐标空间,其中移除位于被呈现的可见区域之外的原始图元。
从剪切空间中,顶点位置通过划分其W分量进行归一化。这产生称为标准化设备坐标或NDC的坐标空间,并且该过程通常被称为透视分割。在该空间中,坐标系的可见部分在x和y方向上为-1.0至1.0,在z方向为0.0至1.0。在透视分割之前,这个地区之外的任何东西都将被剪掉。
最后,顶点的归一化设备坐标由视口变换,它描述了NDC如何映射到要渲染图片的窗口或图像中。
( OpenGL视图变换:[http://www.songho.ca/opengl/index.html])

增强 Vulkan

虽然Vulkan的核心API规范相当广泛,但绝非全然。 一些功能是可选的,而更多的可用层(其修改或增强现有行为)和扩展(向Vulkan添加新功能)的形式。 这两个增强机制将在以下部分进行描述。
图层
层是Vulkan的特征,允许修改其行为。 层通常拦截全部或部分Vulkan,并添加诸如日志记录,跟踪,提供诊断,分析等功能。 可以在实例级别添加一个层,在这种情况下,它会影响整个Vulkan实例以及可能由其创建的每个设备。 或者,可以在设备级别添加该层,在这种情况下,该层仅影响其启用的设备。
要发现系统上实例可用的层,请调用
vkEnumerateInstanceLayerProperties(),其原型是

VkResult vkEnumerateInstanceLayerProperties (uint32_t*                   pPropertyCount, VkLayerProperties*          pProperties);

如果pProperties为nullptr,那么pPropertyCount应指向一个将被Vulkan可用层数计数覆盖的变量。 如果pProperties不是nullptr,那么它应该指向一个VkLayerProperties结构的数组,它将填充有关系统注册的层的信息。 在这种情况下,pPropertyCount指向的变量的初始值是由pProperties指向的数组的长度,并且该变量将被该命令覆盖的数组中的条目数量覆盖。
属性数组的每个元素都是Layer Properties结构的一个实例,它的定义是

typedef struct VkLayerProperties {
char        layerName[VK_MAX_EXTENSION_NAME_SIZE]; uint32_t    specVersion;
uint32_t    implementationVersion;
char        description[VK_MAX_DESCRIPTION_SIZE]; } VkLayerProperties;

每个层都有一个正式的名称,它存储在VkLayerProperties结构的layerName成员中。由于每个层的规范可能会随着时间的推移而改进,澄清或附加,因此在specVersion中报告了层实现的版本。
随着规格随着时间的推移而改进,这些规范的实现也是如此。实现版本存储在VkLayerProperties结构的implementationVersion字段中。这允许实现提高性能,修复错误,实现更广泛的可选功能集等。应用程序写入器可以识别层的特定实现,并且仅当该实现的版本超过特定版本时选择使用它,其中例如已知固定的关键错误。
最后,描述层的人类可读字符串存储在描述中。此字段的唯一目的是在用户界面中记录或显示,仅供参考。
清单1.4说明了如何查询Vulkan系统支持的实例层。

uint32_t numInstanceLayers = 0;std::vector<VkLayerProperties> instanceLayerProperties; // Query the instance layers.  vkEnumerateInstanceLayerProperties( &numInstanceExtensions, nullptr); // If there are any layers, query their properties.if (numInstanceLayers != 0) {       instanceLayerProperties.resize(numInstanceLayers);    vkEnumerateInstanceLayerProperties(nullptr,                                      &numInstanceLayers,    instanceLayerProperties.data());} 

如上所述,不仅在实例级别,层可以被注入。 层也可以在设备级应用。 要确定哪些层可用于设备,请调用
vkEnumerateDeviceLayerProperties(),其原型是

VkResult vkEnumerateDeviceLayerProperties (    VkPhysicalDevice            physicalDevice,        uint32_t*               pPropertyCount,        VkLayerProperties*      pProperties);

可用于系统中的每个物理设备的层可以是不同的,因此每个物理设备可以报告不同的层集合。 要查询的层的物理设备在physicalDevice中传递。 对vkEnumerateDeviceLayerProperties()的pPropertyCount和pProperties参数与vkEnumerateInstanceLayerProperties()的相同命名的参数类似。 设备层也由VkLayerProperties结构的实例描述。
要启用实例级别的层,请将其名称包含在用于创建实例的VkInstanceCreateInfo结构的ppEnabledLayerNames字段中。 同样,为了在创建与系统中的物理设备相对应的逻辑设备时启用层,请在用于创建设备的VkDeviceCreateInfo的ppEnabledLayerNames成员中包含层名称。
官方SDK中包含了几个层次,其中大部分内容与调试,参数验证和日志记录有关。 这些包括以下内容:

•VK_LAYER_LUNARG_api_dump将Vulkan调用及其参数和值打印到控制台。
•VK_LAYER_LUNARG_core_validation对描述符集,流水线状态和动态状态中使用的参数和状态执行验证;验证SPIR-V模块和图形管道之间的接口;并跟踪并验证用于返回对象的GPU内存的使用情况。
•VK_LAYER_LUNARG_device_limits确保将值传递给Vulkan命令作为参数或数据结构成员属于设备支持的功能集限制。
•VK_LAYER_LUNARG_image验证图像使用情况与支持的格式一致。 •VK_LAYER_LUNARG_object_tracker对Vulkan对象执行跟踪,尝试执行
捕获泄漏,使用后自由错误和其他无效对象使用。
•VK_LAYER_LUNARG_parameter_validation确认传递给Vulkan函数的所有参数值都有效。
•VK_LAYER_LUNARG_swapchain对第5章“演示文稿”中描述的WSI(Windows系统集成)扩展功能提供的功能进行验证。
•VK_LAYER_GOOGLE_threading确保了对于线程的Vulkan命令的有效使用,确保没有两个线程在不同时间访问同一个对象。
•VK_LAYER_GOOGLE_unique_objects确保每个对象都有一个唯一的句柄,以便应用程序更容易的跟踪,避免实现可能会重复使用相同参数表示对象的句柄。
除此之外,大量单独的层被分组成一个更大的单层,称为VK_LAYER_LUNARG_standard_validation,使其易于打开。该书的应用程序框架在内置在调试模式下时可以启用此层,当内置在释放模式时,所有层都被禁用。

扩展

扩展是任何跨平台,开放API(如Vulkan)的基础。它们允许实施者进行实验,创新,并最终推动技术向前发展。最终,作为扩展功能最初引入的有用功能在该领域得到证明后可以进入未来版本的API。但是,扩展不是没有成本。有些可能需要实现跟踪附加状态,在命令缓冲区构建期间进行额外的检查,或者即使扩展不是直接使用也会带来一些性能损失。因此,应用程序必须显式启用扩展,才能使用它们。这意味着不使用扩展的应用程序在性能或复杂性方面不支付费用,并且几乎不可能不小心使用扩展功能,从而提高了可移植性。
扩展程序分为两类:实例扩展和设备扩展。实例扩展是通常在平台上增强整个Vulkan系统的扩展。这种类型的扩展是通过与设备无关的层提供的,或者仅仅是由系统上的每个设备暴露的扩展,并被提升为一个实例。设备扩展是扩展系统中的一个或多个设备的功能,但不一定在每个设备上可用。
每个扩展可以定义新的功能,新的类型,结构,枚举等。一旦启用,扩展程序可以被认为是应用程序可用的API的一部分。必须在创建Vulkan实例时启用实例扩展,并且必须在创建设备时启用设备扩展。这些让我们有一个鸡和蛋谁先有的情况:我们如何知道在初始化Vulkan实例之前支持哪些扩展?
查询支持的实例扩展是在创建Vulkan实例之前可以使用的几个Vulkan功能之一。这是使用vkEnumerateInstanceExtensionProperties()函数执行的,其原型是
VkResult vkEnumerateInstanceExtensionProperties (
const char* pLayerName,
uint32_t* pPropertyCount,
VkExtensionProperties* pProperties);
pLayerName是可能提供扩展名的图层的名称。 现在,将其设置为nullptr。 pPropertyCount是一个指向包含查询Vulkan的实例扩展数量的变量的变量,pProperties是一个指向将被填充有关受支持扩展的信息的VkExtensionProperties结构数组的指针。 如果pProperties为nullptr,则pPropertyCount指向的变量的初始值将被忽略,并覆盖支持的实例扩展数。
如果pProperties不为nullptr,则数组中的条目数假定为pPropertyCount指向的变量,并且直到该数组的许多条目都填充有关受支持扩展名的信息。 pPropertyCount指向的变量将被实际写入pProperties的条目数覆盖。

要正确查询所有受支持的实例扩展,请调用vkEnumerateInstanceExtensionProperties()两次。 第一次调用pProperties设置为nullptr来检索支持的实例扩展的数量。 然后适当地调整数组以接收扩展属性并再次调用vkEnumerateInstanceExtensionProperties(),此时将pProperties中的数组的地址传递给它。 清单1.5演示了如何做到这一点。
清单1.5:查询实例扩展

uint32_t numInstanceExtensions = 0; std::vector<VkExtensionProperties> instanceExtensionProperties; // Query the instance extensions. vkEnumerateInstanceExtensionProperties(nullptr,                                          &numInstanceExtensions, nullptr);// If there are any extensions, query their properties.if (numInstanceExtensions != 0) { instanceExtensionProperties.resize(numInstanceExtensions);vkEnumerateInstanceExtensionProperties(nullptr,&numInstanceExtensions,                                instanceExtensionProperties.data());} 

在清单1.5中的代码完成执行后,instanceExtensionProperties将包含实例支持的扩展列表。 VkExtensionProperties数组的每个元素都描述了一个扩展。 VkExtensionProperties的定义是

typedef struct VkExtensionProperties {char extensionName[VK_MAX_EXTENSION_NAME_SIZE]; uint32_t specVersion; } VkExtensionProperties;

VkExtensionProperties结构只包含扩展名和扩展名的版本。扩展可能随着时间的推移而增加功能,因为扩展的新修订版本将被生成。 specVersion字段允许更新扩展,而不需要创建一个全新的扩展,以便添加次要功能。扩展名的名称存储在extensionName中。
如前所述,创建Vulkan实例时,VkInstanceCreateInfo结构中有一个名为ppEnabledExtensionNames的成员,它是一个指向要启用扩展名的字符串数组的指针。如果平台上的Vulkan系统支持扩展,该扩展将包含在从vkEnumerateInstanceExtensionProperties()返回的数组中,并且可以通过VkInstanceCreateInfo结构的ppEnabledExtensionNames字段将其名称传递给vkCreateInstance()。
查询对设备扩展的支持是一个类似的过程。要做到这一点,调用vkEnumerateDeviceExtensionProperties(),其原型是

VkResult vkEnumerateDeviceExtensionProperties (VkPhysicalDevice        physicalDevice, const char*             pLayerName,
uint32_t*               pPropertyCount, VkExtensionProperties*  pProperties);

vkEnumerateDeviceExtensionProperties()的原型与vkEnumerateInstanceExtensionProperties()的原型几乎相同,只是额外添加了physicalDevice参数。 physicalDevice参数是其查询扩展名的设备的句柄。与vkEnumerateInstanceExtensionProperties()一样,如果pProperties为nullptr,则vkEnumerateDeviceExtensionProperties()将覆盖pPropertyCount与支持的扩展数量,如果pProprties不为nullptr,则使用有关所支持扩展的信息填充该数组。相同的VkExtensionProperties结构用于设备扩展和实例扩展。
创建物理设备时,VkDeviceCreateInfo结构的ppEnabledExtensionNames字段可能包含指向从vkEnumerateDeviceExtensionProperties()返回的一个字符串的指针。
一些扩展提供了您可以调用的附加入口点形式的新功能。这些显示为函数指针,其值在启用扩展后必须从实例或设备查询。实例函数是对整个实例有效的函数。如果扩展扩展了实例级功能,则应该使用实例级函数指针来访问新功能。
要检索一个实例级的函数指针,调用vkGetInstanceProcAddr(),其原型是

PFN_vkVoidFunction vkGetInstanceProcAddr ( VkInstance instance, const char* pName);

实例参数是要检索新函数指针的实例的句柄。 如果您的应用程序使用多个Vulkan实例,则从此命令返回的函数指针仅对被引用实例拥有的对象有效。 函数的名称以pName传递,该名称是一个非终止的UTF-8字符串。 如果识别功能名称并启用扩展名,则vkGetInstanceProcAddr()的返回值是可从应用程序调用的函数指针。
PFN_vkVoidFunction是指向以下声明的函数的指针的类型定义:
VKAPI_ATTR void VKAPI_CALL vkVoidFunction(void);
Vulkan没有功能具有这种特殊的签名,扩展不太可能引入这样的功能。 很可能,您需要将生成的函数指针转换为相应签名的指针,然后才能调用它。
假设创建对象的设备(或设备本身,如果设备上的功能调度)支持扩展,并且该设备的扩展名已启用,则实例级别的函数指针对于实例拥有的任何对象都是有效的。 因为每个设备可能会在不同的Vulkan驱动程序中实现,所以实例函数指针必须通过一个间接层分配到正确的模块中。 管理这种间接可能会产生一些开销; 为了避免这种情况,您可以获得直接转到相应驱动程序的特定于设备的功能指针。
要获取设备级功能指针,请调用vkGetDeviceProcAddr(),其原型是
PFN_vkVoidFunction vkGetDeviceProcAddr (
VkDevice device, const char* pName);
将函数指针指向的设备传递到设备中。 再次,您正在查询的函数的名称作为nul终止的UTF-8字符串在pName中传递。 所产生的函数指针仅在设备中指定的设备有效。 设备必须参考设备
它支持提供新功能的扩展,扩展已被启用。
由vkGetDeviceProcAddr()生成的函数指针特定于设备。 即使使用相同的物理设备创建具有完全相同参数的两个或多个逻辑设备,您必须使用结果函数指针与其查询的设备。
彻底关闭
在您的程序退出之前,您应该自己清理。在许多情况下,操作系统将清理您的应用程序终止时分配的资源。但是,代码结束并不总是这样,应用程序结束。如果您正在编写较大应用程序的组件,例如,该应用程序可能会终止使用Vulkan的呈现或计算操作,而不会实际退出。
清理时,一般做法如下:
•在与Vulkan相关的所有线程中,完成或以其他方式终止您的应用程序在主机和设备上进行的所有工作。
•按照与创建顺序相反的顺序销毁对象。
逻辑设备可能是您在应用程序初始化期间创建的最后一个对象(除了运行时使用的对象)。在销毁设备之前,您应该确保它不会代表您的应用程序执行任何工作。要做到这一点,调用vkDeviceWaitIdle(),其原型是
VkResult vkDeviceWaitIdle (VkDevice device);
设备的手柄通过设备传递。 当vkDeviceWaitIdle()返回时,代表您的应用程序提交给设备的所有工作都将保证已经完成 - 除非您在此期间向设备提交更多的工作。 您应该确保可能正在提交到设备的任何其他线程已被终止。
一旦确保设备空闲,您可以安全地销毁它。 要做到这一点,调用vkDestroyDevice(),其原型是
void vkDestroyDevice (
VkDevice device, const VkAllocationCallbacks* pAllocator);
要销毁的设备的句柄在设备参数中传递,必须外部访问其访问。请注意,对于任何其他命令,对设备的访问不需要是外部同步的。然而,应用程序应确保在其他任何访问它的命令仍在另一个线程中执行时,设备不会被销毁。
pAllocator应指向与用于创建设备的分配结构兼容的分配结构。一旦设备对象被破坏,就不能再提交任何命令了。此外,不再可能使用设备句柄作为任何功能的参数,包括

将设备句柄作为其第一个参数的其他对象销毁功能。这是为什么你应该从创建顺序的相反顺序销毁对象的另一个原因。
一旦与Vulkan实例关联的所有设备都已被销毁,可以安全地销毁该实例。这是通过调用其原型的vkDestroyInstance()函数来实现的
void vkDestroyInstance (
VkInstance instance, const VkAllocationCallbacks* pAllocator);
要破坏的实例的句柄被实例传递,与vkDestroyDevice()一样,指向与分配实例的分配结构兼容的指针应在pAllocator中传递。 如果pAllocator参数为vkCreateInstance()为nullptr,那么pAllocator参数vkDestroyInstance()也应该是。
请注意,没有必要销毁物理设备。 物理设备不是通过专用的创建功能创建的,因为逻辑设备是。 相反,物理设备从调用返回到vkEnumeratePhysicalDevices(),并被认为是由实例拥有的。 因此,当实例被销毁时,该实例与每个物理设备相关联的资源也被释放。
概要
本章向您介绍了Vulkan。 你已经看到一个实例中包含了全部的Vulkan状态。 该实例提供对物理设备的访问,每个物理设备暴露了可用于执行工作的多个队列。 您已经看到如何创建与物理设备相对应的逻辑设备。 您已经了解了如何扩展Vulkan,如何确定实例和设备可用的扩展,以及如何启用这些扩展。 您已经看到如何通过等待设备完成应用程序提交的工作,破坏设备句柄,最终摧毁实例句柄来干净地关闭Vulkan系统。

接下来章节的翻译请看链接:
Vulkan其他章节翻译

0 0
原创粉丝点击