GPU Graphics软件架构

GPU Graphics软件架构

news 2023/6/11 3:51:42

一、GPU Graphics的一些开源代码

NVIDIA GPU:

https://github.com/NVIDIA/open-gpu-kernel-modules

AMDGPU:

https://github.com/GPUOpen-Drivers

https://gitlab.freedesktop.org/agd5f

https://lore.kernel.org/amd-gfx/

https://lists.freedesktop.org/mailman/listinfo/amd-gfx

https://github.com/GPUOpen-LibrariesAndSDKs

libdrm / Mesa 3D

https://dri.freedesktop.org/libdrm/

https://archive.mesa3d.org/

https://docs.mesa3d.org/index.html

To be best:

https://blog.csdn.net/linyingzhan/article/details/8265125

二、Mesa简介

除了游戏等3D应用程序外,现代显示服务(Xorg的Glamor和Wayland的Weston)也使用OpenGL和EGL;因此所有的图形通常都要用到Mesa。

Mesa也称为Mesa3D,是OpenGL、Vulkan和其他图形API规范的开源软件实现,Mesa将这些规范转换为特定于供应商的图形硬件驱动程序。所以可以将mesa看成二部分:

一套兼容OpenGL等标准的实现

这部分是为应用程序提供标准的OpenGL等API。

DRI驱动(通常被称为3D驱动)

其中包括Pipeline的软件实现,也就是说即使GPU没有任何3D计算能力,那么mesa也完全可以使用CPU来完成3D渲染功能;另外3D驱动还负责将3D渲染命令翻译成GPU可以理解并且能执行的指令;不同的GPU有各自的"指令集",因此mesa中不同的GPU都有各自的3D驱动。

其最重要的用户是二个图形驱动程序,它们主要由Intel和AMD为各自的硬件开发和资助(AMD在不推荐的AMD Catalyst上推广其Mesa驱动程序Radeon和RadeonSI, Intel只支持Mesa驱动程序)。专有的图形驱动程序(如Nvidia GeForce驱动程序和Catalyst)取代了所有Mesa,提供了自己的图形API实现,开发名为Nouveau的Mesa Nvidia驱动程序的开源工作主要由社区开发。

DRI的三代变化

DRI的实现有三代,即DRI1、DRI2、DRI3。

DRI1

首先要说,DRI1已经不在考虑使用了,这里说一下它的原理:

DRI1由于当时图形卡内存大小,只有一个屏幕front buffer+back buffer由所有DRI clients和X server使用,front buffer和back buffer就像现在显示系统的双缓冲一样,所有要做渲染操作的实体都直接渲染到back buffer,然后执行swap就更新画面,front变back,back变front。另外所有渲染实体在开始渲染时,都会独占DRM设备,就像互斥锁保护的内存资源,其他的渲染实体就要等,并且渲染实体在释放设备时,所有之前上传的数据(如纹理等)都会丢失,总之,性能不行。

DRL2

DRI2和DRI3现在都在使用,但是DRI3性能更好一些,说一下DRI2原理:

后来的DRI2依赖KMS/GEM,每个DRI应用使用单独的渲染缓冲区,性能上有所改善。

DRI2是进入compositor时代的设计,buffer开始变成离屏buffer,并且离屏buffer可以做直接渲染,DRM也经过了一次大更新;每个DRI client都有自己的back buffer(附带着深度和模版 缓冲区),DRI client在back buffer做完渲染后的swap也不再是直接显示出去,而是变成提交给compositor作为一个compose源,最终的屏幕内容是compositor根据各个源的叠加、透明、边界裁剪得到,并swap到真的front buffer。

为了管理各个DRI client都有自己的back buffer这个事儿,DRM加了新功能,内存管理TTM,但是后来又重写成GEM。但是有了这个内存管理之后,DRI2 client渲染时不再锁定整个DRM设备,并且在暂停渲染时也不必释放所有显存资源。

DRL3

最新的DRI3依赖DMA_BUF,改进了缓冲区对象的传递和共享方式(从GEM名称改成了PRIME DMA_BUF),性能和安全都有所增强。

DRI3 client申请自己的渲染buffer,而不是去调用X server来做申请(这是DRI2 支持的方法),这带来了易于改变窗口大小、重复利用之前的buffer等特性(compositor刷帧只需要更新部分缓存内容即可);DRI3放弃了不安全的基于GEM共享buffer的机制,转而使用DMA的fd来传递,并且DRI3PixmapFromBuffer和DRI3BufferFromPixmap可以完成X server buffer与DMA buffer的转换,同样一块内存,在DRI client和X server之间传递。

三、2D渲染(结合X窗口系统,以Intel GPU为例)

可以形象地将2D渲染比喻为绘画,其中有二个关键地方,一个是画布,另一个是画笔。X Server启动后,将加载GPU的2D驱动,2D驱动将请求内核中的DRM创建帧缓冲,这个帧缓存就相当于画布。然后X Server按照绘图需要,从画笔盒子中挑选合适的画笔(CPU or GPU)进行绘画(比如绘制矩形,绘制圆弧,绘制实心多边形等)。

当然最初的这些绘制操作均是由CPU负责完成,也就是我们常说的软件渲染,X中的fb层就是软件渲染的实现(xorg-server/fb/fbgc.c),随着GPU的发展,这部分工作就慢慢开始由GPU来做了。

X的渲染架构: XAA/EXA/UXA/SNA/Glamor

X的渲染架构也随着GPU的发展而不断演进。在XFree86 3.3的时候,X的开发者设计了XAA(XFree86 acceleration Architecture)架构;在X.org Server 6.9版本,开发者用改进的EXA取代了XAA;当DRM中使用了GEM后,Intel的GPU驱动开发者门重新实现了EXA,并命名为UXA(Unified Acceleration Architecture);随着Intel推出Sandy Bridge及ivy Birdge芯片组,Intel又开发了SNA(SandyBridge's New Acceleration)。这里需要注意Nvidia和AMD显卡只有EAX。

在Intel的2D GPU驱动里可以发现在UXA架构下,X的画笔盒子定义(xf86-video-intel/uxa/uxa-accel.c)如下。在uxa_ops里有一部分画笔来自GPU,有一部分呢来自CPU。

const GCOps uxa_ops = {uxa_fill_spans,...uxa_poly_lines,uxa_poly_segment,miPolyRectangle,uxa_check_poly_arc,miFillPolygon,...
};

最后,X Server采用了类似Wayland的方法,所有的图形加速全部使用3D驱动取代,将2D驱动精简成一个包装层,这就是Glamor加速方法

X的2D渲染过程

以UXA架构为例,对于每一个绘图操作:

如果GPU支持这个绘制操作,UXA首先将绘制的命令翻译成GPU可以识别的指令,并将这个指令、绘制所需要的相关数据,以及保存像素阵列的BO在显存地址空间中的地址,一同保存在用户空间的批量缓冲(Batch Buffer),然后通过DRM将用户空间的批量缓冲复制到内核为批量缓冲创建的BO,之后通知GPU从BO中读取指令和数据进行绘制。实际上,DRM按照Intel GPU的要求在批量处理缓冲和GPU之间还组织了一个环形缓冲区(RingBuffer),但是我们暂时忽略它,这对于理解2D渲染过程没有任何影响。

如果GPU不支持这个操作,那么UXA将代表帧缓冲的BO映射到X Server的用户空间,X Server借助fb层中的实现,使用CPU进行绘制。

创建前缓冲

在X环境下,在不开启复合(Composite)扩展的情况下,所有的程序共享一个前缓冲。对于2D程序,所有的绘制动作生成的图像的像素阵列最终都输出到这个前缓冲上,窗口只不过是前缓冲中的一块区域而已。这里需要注意一旦开启了复合扩展,那么每个窗口都将被分配一个离屏(offscreen)的缓冲,类似于OpenGL环境中的后缓冲;应用将生成的像素阵列输出到这个离屏的缓冲中,在绘制完后,X Server将向复合管理器(Composite Manager)发送Damage事件,复合管理器收到这个事件后,将离屏缓冲区的内容合成到前缓冲。这里的2D渲染流程和接下来的3D渲染流程都暂时不涉及复合扩展。

在X中,Window和Pixmap是2个绘制发生的地方,Window代表屏幕上的窗口,Pixmap代表离屏的一个存储区域。所以自然而然地,X使用数据结构Pixmap来表示前缓冲。因为这个前缓冲对应整个屏幕,而且不属于某一个应用,因为开发者也将代表前缓冲地这个Pixmap称为Screen Pixmap。显然这个Screen Pixmap也是显示器(Screen)的资源。

1)创建前缓冲的BO:前缓冲的BO是在X Server启动过程中,2D驱动程序初始化设备时向DRM模块申请创建的。

2)将前缓冲保存到屏幕对象:创建了前缓冲的BO后,X Server就会为前缓冲创建Pixmap对象(即Screen Pixmap),然后将前缓冲保存到屏幕对象。

3)窗口与前缓冲的绑定:前缓冲已经建立起来了,但是显然需要将窗口与前缓冲关联起来,否则在窗口上的绘制并不能体现到屏幕上。我们在编写具有图形界面的程序时,在绘制之前首先需要创建绘制所在的窗口。恰恰是在创建窗口时,窗口与前缓冲绑定了。X中创建窗口的函数(xorg-server/fb/fbwindow.c)如下:

Bool fbCreateWindow(WindowPtr pWin)
{dixSetPrivate(&pWin->devPrivates, fbGetWinPrivateKey(), fbGetScreenPixmap(pWin->drawable.pScreen));
}

fbCreateWindow调用函数fbGetScreenPixmap获取Screen Pixmap,并将窗口对象与Screen Pixmap绑定。

显然,所谓的创建窗口事实上就是将窗口与前缓冲关联起来,以后凡是发生在窗口上的绘制,都将直接绘制到前缓冲中。

GPU渲染和CPU渲染

GPU又称硬件渲染/硬件加速,从软件层面所做工作就是将数学模型按照GPU的规定,翻译为GPU可以识别的指令和数据,传给GPU,生成像素阵列等图像密集型计算则由GPU完成。在Intel GPU的2D驱动中定义了使用一种所谓的批量缓冲来保存这些命令和数据,将驱动准备的命令和数据放到这个缓冲,然后批量让GPU来读取。GPU内部处理分为多个微核,处理不同的命令,处理2D指令的微核称为BLT引擎。

CPU渲染又称为软件渲染,BO是由DRM模块在内核空间分配的,因此运行在用户空间的X(2D驱动)想要访问这个内存,必须首先要将其映射到用户空间,这是由uxa_prepare_access来完成的。然后X使用CPU在映射到用户空间的BO上进行绘制。

所谓的软件渲染和硬件加速,本质都是生成图像的像素阵列,只不过一个是由CPU来计算,另一个是由GPU来计算。当然对于硬件加速,CPU要充当一个翻译,将数学模型按照GPU的要求翻译为其可以识别的指令和数据。

四、3D渲染(基于X窗口系统和Mesa,以Intel GPU为例)

直接渲染

X-Window所提供的XLib只提供2D渲染功能,3D功能是由Mesa提供的。现在的Mesa实现了DRI(Direct Rendering Infrastructure)功能,不通过X-Server,直接与内核/硬件进行交互。

OpenGL应用程序和窗口系统的结合

GLX扩展和DRI扩展

Pipeline最后将生成好的像素阵列输出到帧缓冲,但是这这还不够,因为最后的输出需要显示到屏幕上。而屏幕的显示是由具体的窗口系统控制的,因此帧缓冲还需要与具体的窗口系统相结合。但是X的核心协议并不包含OpenGL等相关的协议,因此开发者门开发了GL的扩展GLX(GL Extension)。为了支持DRI开发者又开发了DRI扩展。显然GLX以及DRI扩展在X和Mesa中均需要实现。

运行在X窗口系统上的OpenGL应用程序的渲染过程(可以分为三个阶段,如上图所示)

1)应用创建OpenGL的上下文,包括向X Server申请创建帧缓冲。OpenGL应用程序为什么不自己直接向内核的DRM模块请求创建帧缓冲呢?从技术上讲,OpenGL应用自己请求DRM创建帧缓存没有任何问题,但是为了将帧缓冲与具体的窗口系统绑定,OpenGL应用只能委屈一些,放低姿态请求X Server为指定窗口创建帧缓冲对应的BO,帧缓冲中包含多个缓冲,当然是创建多个BO了,X Server收到应用程序的请求后,为各个缓冲创建BO(在X中帧缓冲由XServer创建,然后创建完后,将BO的名字等相关信息告知OpenGL应用,应用收到BO信息后便可以更新GPU的状态,比如告诉GPU画板在哪里)。这样X Server就掌握了应用的帧缓存的一手资料,在需要时将帧缓冲显示到屏幕。帧缓冲是OpenGL应用的"画板",因此创建完成后,X Server需要将帧缓冲的BO信息返回给OpenGL应用。

2)OpenGL应用创建数学模型,并通过OpenGL的API将数学模型的数据写入顶点缓冲区Vertex Buffer;更新GPU的状态,若指定后缓冲,用来存储Pipeline输出的像素阵列;然后启动Pipeline进行渲染。

3)渲染完成后,OpenGL应用向X Server发出swap请求。这里的swap有二种方式,一种是copy,即将后缓冲的内容复制到前缓冲,这里是由GPU的BLT引擎负责的。但是这种copy的方式效率相对较低,所以开发者又设计了一种称为页反转(page flip)的模式,在这种模式下不需要copy动作,而是通过GPU的显示引擎控制显示控制器扫描哪个帧缓冲,这个被扫描的缓冲此时扮演前缓冲,而另外一个不被扫描的帧缓冲则作为应用的"画板",也就是所说的后缓冲。

渲染Pipeline

与2D渲染相比,3D渲染要复杂得多。就如同有些复杂得绘画过程,要分成几个阶段一样,OpenGL标准也将3D的渲染过程划分为一些阶段,并将由这些阶段组成的这一过程称为Pipeline。

应用程序建立基本的模型包括在对象坐标中的顶点数据、顶点的各种属性(比如颜色),以及如何连接这些顶点(如是连接成直线还是连接成三角形)等,统一存储在顶点缓冲中,然后作为pipeline的输入,最终生成像素阵列,输出到后缓冲的BO中。

OpenGL的标准规定了一个参考Pipeline,但是各家GPU的实现与这个参考还是有很多差别的,有的GPU将相应的阶段合并,有的GPU将个别阶段拆分了,有的可能增加了一些阶段,有的又砍了一些阶段,但是大体上整个过程如下图所示。

1)Vertex operations

OpenGL使用顶点的集合来定义或逼近对象,应用程序建模实际上就是组织这些顶点。顶点处理单元将几何对象的顶点从对象坐标系变换到视点坐标系,也就是将三维空间的坐标投影到二维坐标,并为每个顶点赋颜色值,并进行光照处理等。

2)Primitive assembly

很多操作是不能以顶点单独进行处理的,比如裁剪、光栅化等。需要顶点组装成几何图形。Pipeline将处理过程的顶点连接成为一些最基本的图元,包括点、线、三角形等。这个过程称为图元装配。

任何一个曲面都是由多个平面无限逼近的,而最基本的三点表示一个平面。所以理论上GPU将曲面都划分为若干个三角形,也就是使用三角形进行装配。但是也不排除现代GPU的设计者们使用其他的更有效的图元,比如梯形,进行装配。

3)Rasterization

图形是使用像素阵列来表示的。所以图元最终要转换为像素阵列,这个过程称为光栅化,我们可以把光栅理解成像素的阵列。经过光栅化之后,图元被分解为一些片段(fragment),每个片段对应一个像素,其中有位置值(像素位置)、颜色、纹理坐标和深度等属性。

4)Fragment operations

在Pipeline更新帧缓冲前,Pipeline执行最后一系列的针对每个片段的操作。对于每一个片段,首先进行相关的测试,比如深度测试、模板测试。以深度测试为例,只有当片段的深度值小于深度缓存中与片段相对应的像素的深度值时,颜色缓冲、深度缓冲中的与片段相对应的像素的值才会被这个片段中对应的信息更新。

Pipeline可全部由软件实现(CPU),也可全部由硬件实现(GPU),或者二者混合。

GPU中的命令流

前缓冲和后缓冲的交换(swap)

应用程序绘制完成后需要将后缓冲交换(swap)到前缓冲,其中有三个问题需要考虑。

谁来负责swap

如果应用自己负责将后缓冲更新到前缓冲,那么当有多个应用同时更新前缓冲时如何协调?显然将swap动作交给更擅长管理窗口的X Server统一管理更合理。

如果X Server开启了复合扩展,更需要知道应用已经更新前缓冲了,因为X Server需要通知复合管理器重新合成前缓冲。

综上,应该由X Server来负责swap前后缓冲。

对于GPU支持swap的情况,X Server通过2D驱动请求GPU进行交换。否则X Server只能将前缓冲和后缓冲的BO映射到用户空间,使用CPU逐位复制。

swap的时机

与2D应用不同,3D程序通常涉及复杂的动画和图像,如果显示控制器正在扫描前缓冲的同时,X Server更新了前缓冲,那么可能导致屏幕出现撕裂(tearing)现象。所谓的撕裂就是指本应该分为2帧显示在屏幕上的图像同时显示在屏幕上,上半部分是一帧的上半部分,而下半部分是另外一帧的下半部分,情况严重的将导致屏幕出现闪烁(flicker)。

以一个刷新率为60HZ的显示器为例,显示控制器每隔1/60s从前缓存读取数据传给显示器,每扫完一帧显示控制器回溯(retrace)到第一行的第一个点的位置,等待下一帧开始扫描。

更新完一帧图像远不需要1/60s,从更新完最后一行的最右侧一个点,到开始扫描下一帧之间的间隙称为垂直空闲(vertival blank),简称为"vblank"。显然如果在vblank这段时间更新前缓冲就不会导致上述的撕裂和闪烁现象出现了。

swap的方法

交换后缓冲和前缓冲通常有2种方法:第一是复制模式,在绘制完成后,X Server将后缓冲中的数据复制到前缓冲。

但是这种方法效率较低,所以开发者门设计了页反转模式(page flip)。该模式不进行数据复制,而是将显示控制器指向后缓冲。后缓冲与前缓冲的角色进行互换,后缓冲摇身一变成为前缓冲,显示控制器将扫描后缓冲的数据到屏幕,而原来的前缓冲则变成了后缓冲,应用程序在前缓冲上进行绘制。

页反转模式虽然效率高,但也不是所有的情况都适用。典型的,当一个应用程序处于全屏模式时,可以采用页反转模式互换前缓冲和后缓冲。但是这对于使用复合管理器的图形系统来说,其实已经大大的提升效率了,因为复合管理器控制着整个屏幕的显示,所以复合管理器可以使用页反转模式交换前缓冲和后缓冲。

五、从X到Wayland

X Composite Manager

将所有的图形全部交给X Server绘制这种设计在2D应用为主的时代一切还好。但是基于数据量大得多3D的应用越来越多,应用于X Server之间需要传递大量的数据,性能就称为瓶颈。

为了解决这个问题,X的开发者们设计了DRI机制,即应用程序不再将绘制图形的任务发给X Server,而是由应用自行绘制(render)。这种设计与X最初的设计原则虽然有些格格不入,但是从某种程度上确实缓解了3D应用的效率问题。

但是好景不长,人们逐渐不再满足于看上去比较"呆板"的图形用户应用,为了追求具有更华丽的3D特效的图形用户界面,比如窗口弹出和关闭时的放大/缩小动画、窗口之间的透明等。于是为X设计了复合扩展,并效仿窗口管理器设计了一个所谓的复合(Composite)管理器来实现这些效果。下面以2D绘制过程为例看下什么是复合扩展和复合管理器(Composite Manager)。

开启复合扩展后,最大的一个区别是所有的窗口都不再共享一个前缓冲,而是有了各自的离屏区域。X Server在各个窗口的离屏区域上进行绘制。绘制好了后,X Server向另外一个特殊的应用复合管理器发出Damage通知。然后由复合管理器请求X Server对这离屏的窗口缓冲区进行合成,最后请求X Server显示到前缓冲。下面的代码展示了复合管理器为窗口创建离屏缓冲的过程:

xorg-server/composite/compinit.c

Bool compScreenInit(ScreenPtr pScreen)
{......pScreen->CreateWindow = comCreateWindow;......
}

xorg-server/composite/compwindow.c

Bool compCreateWindow(WindowPtr pwin)
{......compRedirectWindow(...);......
}

可以看到,在开启复合扩展后,屏幕中的指针CreateWindow已经指向了复合扩展中实现的函数compCreateWindow。而在compCreateWindow中,调用compRedirectWindow将窗口从前缓冲重定向到一个离屏区域。

在这个复合过程中,就是制造那些绚丽效果的地方。比如在合成的过程中,我们使用下图的方法,就可以使窗口看起来是以放大效果出现的。

Wayland

虽然X中使用的Composite Manager绚丽效果有了,但是X被诟病的基于网络通信的客户/服务器模式的问题变得更严重了,因为除了X Server与应用之间通信外,为了合成X Server与Composite Manager之间又多了一层通信关系。事实上在DRI的演进过程中,X不断被拆分和瘦身,开发者从X中移除了大量的与渲染有关的功能到内核和各种程序中。慢慢的,X所做的事情越来越少,替代X已经一件相对容易的事了,于是Wayland就出现了。

Wayland去掉了X的客户/服务器架构,但是继承了X为提高绘制(render)效率而努力的的成果:DRI。除了逻辑上设计不同外,Wayland基本渲染原理与前面的2D和3D渲染原理完全相同。基本上Wayland的架构如下所示:

Wayland本身是一个协议,其具体的实现包括一个合成器(Compositor)以及一套协议实现库。当然图形库为了与Compositor进行通信,在图形库中需要加入Wayland协议的相关模块,也就是图的Wayland backend部分,当然这些都可以基于Wayland提供的库,而不必从头再将Wayland协议实现一遍。

在Wayland下所有的图形绘制完全由应用自己负责。其绘制过程与前面的2D和3D的绘制过程完全相同。只不过2D的绘制部分也搬到图形库中了,绘制动作和Compositor没有丝毫关系。而在绘制后,应用将前缓冲与后缓冲进行对调,并向Compositor发送Damage通知,当然颜色缓冲不一定是前后二个,在具体的实现中,有的图形系统可能使用3个、4个甚至更多。在收到Damage通知后,Compositor将应用的前缓冲合成到自己的后缓冲中。而Compositor的这个合成过程,与普通应用的绘制过程并无本质区别,也是通过图形库完成。

在合成完成后,Compositor对调后缓冲与前缓冲,并设置显示控制器指向新的前缓冲,即原来的后缓冲。此前的前缓冲作为新的后缓冲,并作为Compositor下一次合成的现场;而原来的后缓冲则变为现在的前缓冲,用于显示控制器的扫描输出。

六、基于Wayland和Mesa的渲染

Wayland Compositor虽然也管理窗口并提供服务,但是它不包含2D驱动,而是通过EGL与3D驱动交互,因此可以认为Wayland架构里都是直接渲染。

虽然X Server和Wayland Compositor都会直接调用LibDRM,但是前者包含2D加速和模式设置,后者只有模式设置(2D和3D加速在Wayland中都采用3D驱动实现)

七、应用程序到显卡驱动的调用关系

2D用户驱动叫DRV驱动,由X Server提供(/usr/lib/xorg/modules/drivers/xxx_drv.so),3D用户驱动叫DRI驱动,由mesa提供(/usr/lib/dri/xxx_dri.so)。

所有访问都通过DRM进行:

Wayland 通过 EGL 实现直接渲染

在内核3.12中引入了渲染节点; DRM 和 KMS 驱动程序被拆分,Wayland 通过 EGL 实现直接渲染。

1)X图形应用程序(2D) > X Server(提供DRV驱动) > LibDRM > Kernel(提供DRM驱动) >显卡硬件

2)X图形应用程序(3D) > X Server(提供DRV驱动) > Mesa(提供DRI驱动) > LibDRM > Kernel(提供DRM驱动) > 显卡硬件

3)WayLand图形应用程序 > Wayland Compositor > Mesa(提供DRI驱动) > LibDRM > Kernel(提供DRM驱动) > 显卡硬件

4)OpenGL图形应用程序 > Mesa(提供DRI驱动) > LibDRM > Kernel(提供DRM驱动) >显卡硬件

reference:

Security of the Intel Graphics Stack - Part 1 - Introduction - Igor’s Blog (igor-blue.github.io)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值