移动端图形API之内存

1. 图形 API 的内存管理

图形 API(如 OpenGL、DirectX、Vulkan 等)在内存管理方面的主要任务是为渲染过程提供必要的资源和状态管理。它们通过特定的接口来分配和管理内存,主要涉及以下几个方面:

1.1 Host 内存
  • 定义:

    • Host 内存是指在 CPU 侧分配的内存,主要用于存储与渲染相关的状态信息、命令缓冲区、上下文管理等。
  • 用途:

    • 渲染上下文管理: API 需要在 Host 内存中维护渲染上下文,包括当前的渲染状态、视图矩阵、投影矩阵等。
    • 命令缓冲区: 在某些 API 中,渲染命令会被存储在 Host 内存中的命令缓冲区中,等待 GPU 执行。
    • 资源管理: API 可能会在 Host 内存中维护对 GPU 资源(如纹理、缓冲区等)的引用和状态信息。
  • 不可控性:

    • 开发者通常无法直接控制 Host 内存的分配和释放。这部分内存的使用量可能会随着 API 调用的增加而显著增加,有时甚至超过实际的 GPU 资源占用。
1.2 Device 内存
  • 定义:

    • Device 内存是指在 GPU 侧(显存)分配的内存,主要用于存储实际的渲染资源,如纹理、顶点缓冲区、索引缓冲区等。
  • 用途:

    • 渲染资源: 开发者通过 API 显式地请求分配 Device 内存,以存储纹理、几何数据等。
    • 数据传输: 在 CPU 和 GPU 之间传输数据时,开发者需要管理 Host 和 Device 内存之间的数据流动。
  • 控制:

    • 开发者可以通过 API 调用(如 glTexImage2DglBufferData 等)来控制 Device 内存的分配和释放。

2. Host 和 Device 内存的关系

  • 物理位置:

    • 在某些平台(尤其是移动设备)上,Host 和 Device 内存可能在物理上是相邻的,甚至共享同一块物理内存。这种情况下,数据传输的开销可能会降低,但仍然需要通过 API 来管理数据的使用。
  • 性能影响:

    • Host 内存的管理和使用效率直接影响到渲染性能。如果 Host 内存的使用过多,可能会导致性能瓶颈,影响渲染速度和帧率。

3. 内存管理的挑战

  • 内存泄漏:

    • 由于 Host 内存的分配通常是自动的,开发者可能会面临内存泄漏的问题,尤其是在频繁调用 API 时。
  • 调试和分析:

    • 监控和分析 Host 内存的使用情况可能比较困难。开发者需要使用特定的工具(如内存分析器、性能分析器等)来识别和优化内存使用。

4. 总结

图形 API 在内存管理方面的复杂性主要体现在 Host 和 Device 内存的分配和管理上。Host 内存用于管理渲染状态和上下文,而 Device 内存则用于存储实际的渲染资源。开发者需要理解这两者之间的关系,并在设计图形应用时考虑内存的使用效率,以优化性能和减少内存浪费。

Host 内存

1. Host 内存的作用

Host 内存在图形 API 中主要用于以下几个方面:

  • 渲染上下文管理:

    • 图形 API 需要维护渲染上下文,包括当前的渲染状态、视口设置、光照参数等。这些信息通常存储在 Host 内存中,以便在渲染过程中快速访问。
  • 命令缓冲区:

    • 在某些图形 API 中,渲染命令会被存储在 Host 内存中的命令缓冲区中。这些命令在 GPU 执行时需要从 Host 内存中读取。
  • 资源管理:

    • API 需要在 Host 内存中维护对 GPU 资源(如纹理、缓冲区等)的引用和状态信息。这包括资源的创建、更新和销毁等操作。

2. 不可控性

  • 自动分配:

    • Host 内存的分配通常是自动的,开发者无法直接控制这些内存的分配和释放时机。这意味着在调用 API 时,可能会产生额外的内存开销,而开发者对此并不知情。
  • 内存占用:

    • 在某些情况下,Host 内存的占用可能会显著超过实际的图形资源占用。例如,API 可能会为每个渲染对象分配额外的状态信息或缓存,这些都占用了 Host 内存。

3. 内存分析与监控

  • 内存分析工具:

    • 开发者可以使用内存分析工具(如 Android Profiler、Xcode Instruments 等)来监控应用的内存使用情况。这些工具可以帮助识别 Host 内存的使用情况,但对于 API 内部的内存分配,可能并不总是透明。
  • 差异分析:

    • 在进行内存分析时,开发者可能会发现自己在 C++ 代码中分配的内存与平台统计的 native 内存之间存在差异。这种差异往往是由于 API 的 Host 内存分配造成的。

4. 内存浪费与优化

  • 内存浪费:

    • Host 内存的浪费可能会导致应用的内存占用过高,影响性能和用户体验。开发者需要关注这一点,尽量减少不必要的 API 调用,以降低 Host 内存的使用。
  • 优化策略:

    • 尽量减少频繁的 API 调用,尤其是在渲染循环中。可以考虑批量处理渲染命令,减少上下文切换和状态更改。
    • 使用合适的资源管理策略,确保在不再需要时及时释放资源,尽量减少内存占用。

5. 总结

Host 内存在图形 API 中扮演着重要的角色,主要用于管理渲染上下文和资源状态。由于其不可控性,开发者需要关注 Host 内存的使用情况,并利用内存分析工具进行监控和优化。通过合理的设计和优化策略,可以减少 Host 内存的浪费,提高应用的性能和用户体验。

OpenGL ES(GLES)中 Host 内存管理

1. GLES 中 Host 内存的不可控性

  • 自动分配:

    • 在 GLES 中,Host 内存的分配是由图形 API 和底层驱动自动管理的。开发者无法直接控制何时分配或释放这些内存,这使得内存使用的透明度降低。
  • API 调用的副作用:

    • 每次调用 GLES API 时,可能会产生额外的 Host 内存开销。这些开销可能来自于状态管理、命令缓冲区、资源引用等,导致内存使用量超出预期。

2. 内存统计的挑战

  • 难以统计:

    • 由于 Host 内存的分配是隐式的,开发者很难准确统计这部分内存的大小。常规的内存分析工具可能无法提供详细的内存使用情况。
  • 使用 Hook 技术:

    • 为了更好地了解 Host 内存的使用情况,开发者可以考虑使用 Hook 技术,拦截底层驱动(如 adreno.so)的内存分配调用。这可以帮助开发者获取更详细的内存分配信息。
  • pmap 和 mmap:

    • 使用 pmap 命令可以查看进程的内存映射情况,特别是与 kgsl(Kernel Graphics Support Layer)相关的内存映射。这可以帮助开发者识别与 GLES 相关的内存使用情况。

3. Host 内存浪费的影响

  • 性能影响:

    • Host 内存的浪费可能导致应用的内存占用过高,影响性能,尤其是在内存受限的移动设备上。过高的内存占用可能导致频繁的垃圾回收、页面交换等,进一步影响应用的流畅性。
  • 用户体验:

    • 内存浪费可能导致应用崩溃或响应缓慢,影响用户体验。因此,开发者需要关注 Host 内存的使用情况,尽量减少不必要的内存开销。

4. 优化建议

  • 减少 API 调用:

    • 尽量减少频繁的 GLES API 调用,尤其是在渲染循环中。可以考虑批量处理渲染命令,减少上下文切换和状态更改。
  • 资源管理:

    • 在不再需要某些资源时,及时释放它们。虽然 Host 内存的管理是自动的,但开发者仍然可以通过合理的资源管理策略来减少内存占用。
  • 监控和分析:

    • 定期使用内存分析工具监控应用的内存使用情况,识别潜在的内存泄漏和浪费。结合使用 Hook 技术和 pmap 命令,可以更全面地了解 Host 内存的使用情况。

5. 总结

在 GLES 中,Host 内存的管理是一个复杂且不可控的过程,开发者需要关注这部分内存的使用情况,以优化性能和用户体验。通过使用 Hook 技术、内存分析工具以及合理的资源管理策略,可以有效减少 Host 内存的浪费,提高应用的整体性能。

Vulkan 和 Metal 中 Host 内存管理

Vulkan 中的 Host 内存管理

  1. 自定义内存分配:

    • Vulkan 提供了 VkAllocationCallbacks 结构体,允许开发者自定义内存分配策略。这种灵活性使得开发者可以使用自己封装的内存分配器,从而更好地管理内存使用。
    • 通过实现 pfnAllocationpfnReallocationpfnFree 函数,开发者可以控制内存的分配、重新分配和释放。这对于优化内存使用和性能非常有帮助。
  2. 内存分配通知:

    • Vulkan 还提供了 pfnInternalAllocationpfnInternalFree 回调,允许开发者在 API 内部进行内存分配和释放时接收通知。这使得开发者能够监控和分析内存使用情况,尽管不能直接控制这些操作。
  3. 配对管理:

    • 在实现自定义的分配和释放函数时,开发者需要确保它们是成对调用的,以避免内存泄漏或未定义行为。这种配对管理是内存管理中的一个重要实践。
  4. 内存分析:

    • 由于 Vulkan 提供了更高的透明度和控制能力,开发者可以更容易地进行内存分析和优化。这对于大型引擎和复杂应用程序尤为重要。

Metal 中的 Host 内存管理

  1. 内存分配的不可控性:

    • 在 Metal 中,虽然开发者不能直接控制内存的分配,但可以通过 Xcode 中的工具(如 Allocations 和 Memory Graph)来监控 Metal API 的内存使用情况。这些工具提供了对内存分配的可视化分析,帮助开发者识别潜在的内存问题。
  2. 内存使用的透明性:

    • Metal 的设计旨在提供高性能和低开销的图形 API,尽管它不提供 Vulkan 那样的自定义内存分配能力,但通过 Xcode 的工具,开发者仍然可以获得有关内存使用的有价值信息。
  3. 性能优化:

    • 开发者可以利用 Xcode 的内存分析工具来识别内存泄漏、过度分配和其他潜在问题,从而优化应用的内存使用和性能。

总结

  • Vulkan:

    • 提供了更高的灵活性和控制能力,允许开发者自定义内存分配策略,并通过回调函数接收内存分配和释放的通知。这使得 Vulkan 在内存管理方面更具优势,尤其适合需要精细控制内存使用的复杂应用和引擎。
  • Metal:

    • 尽管不提供自定义内存分配的能力,但通过 Xcode 的工具,开发者可以有效地监控和分析内存使用情况。这对于开发者在 Apple 生态系统中优化应用性能仍然是非常有用的。

在选择图形 API 时,开发者需要根据项目的需求和目标平台的特性来考虑内存管理的灵活性和可控性。Vulkan 适合需要高性能和精细内存管理的应用,而 Metal 则在 Apple 设备上提供了良好的性能和易用性。

备内存(Device Memory)在图形 API 中的分配和释放

以下是对设备内存的分配、使用和管理的进一步分析,特别是在 Vulkan、OpenGL ES 和 Metal 中的不同方式。

设备内存的概述

设备内存是 GPU 上用于存储渲染资源的空间,主要包括以下几种类型:

  1. Image:

    • 用于存储图像数据,通常用于纹理(Texture)和渲染目标(Render Target)。Image 可以是多维的,具有特定的格式(如 RGB、RGBA、深度等)。
  2. Buffer:

    • 一维内存数据块,通常用于存储顶点数据、索引数据、Uniform 数据等。Buffer 的格式相对简单,通常是线性存储。
  3. Shader:

    • 存储着色器程序的代码,通常在编译时加载到 GPU 上。
  4. Sampler:

    • 用于定义如何从纹理中读取数据的对象。
  5. Framebuffer:

    • 用于存储渲染结果的对象,通常与 Image 结合使用。

设备内存的分配方式

设备内存的分配主要有两种方式,分别对应于 Vulkan 和其他图形 API(如 OpenGL ES 和 Metal):

1. Vulkan 的内存分配 + 对象绑定方式
  • 内存分配:

    • 在 Vulkan 中,设备内存的分配是通过 vkAllocateMemory 函数进行的。开发者需要指定内存类型、大小和其他属性。
  • 对象绑定:

    • 分配内存后,开发者需要将内存与 Buffer 或 Image 对象绑定,使用 vkBindBufferMemoryvkBindImageMemory 函数。这种方式提供了更高的灵活性和控制能力,允许开发者精确管理内存的使用。
  • 内存管理:

    • Vulkan 允许开发者选择不同的内存类型(如设备本地内存、主机可见内存等),并根据性能需求进行优化。这种灵活性使得 Vulkan 在高性能应用中非常受欢迎。
2. OpenGL ES 和 Metal 的简单对象创建方式
  • 对象创建:

    • 在 OpenGL ES 和 Metal 中,设备内存的分配通常是通过对象创建函数(如 glGenBuffersglBindBufferMTLCreateBuffer)来隐式完成的。开发者不需要手动管理内存的分配和绑定。
  • 内存管理:

    • 这种方式虽然简化了内存管理,但也限制了开发者对内存使用的控制。API 会自动处理内存的分配和释放,适合快速开发和较简单的应用场景。

Image 和 Buffer 的内存使用

  • Buffer:

    • Buffer 是一段线性内存,通常用于存储简单的数据结构。开发者可以通过不同的方式(如映射内存、直接写入等)来操作 Buffer 的内容。
  • Image:

    • Image 是多维的,具有特定格式的内存对象。它可以被 GPU 读取(作为纹理)或写入(作为渲染目标)。Image 的内存管理相对复杂,因为它涉及到多种格式和维度。

总结

  • Vulkan 提供了更高的灵活性和控制能力,允许开发者精确管理设备内存的分配和使用。这对于需要高性能和复杂内存管理的应用非常重要。

  • OpenGL ES 和 Metal 则提供了更简单的内存管理方式,适合快速开发和较简单的应用场景。虽然这种方式限制了开发者的控制能力,但在许多情况下,简化的内存管理可以提高开发效率。

以下是Vulkan 中设备内存(VkDeviceMemory)的分配和资源对象(如 Buffer 和 Image)的绑定过程进行了详细的描述

Vulkan 中的设备内存管理

1. 设备内存对象的创建

在 Vulkan 中,设备内存是通过 vkAllocateMemory 函数进行分配的。这个过程涉及以下几个关键步骤:

  • 查询内存属性:

    • 在创建设备之前,开发者需要使用 vkGetPhysicalDeviceMemoryProperties 查询物理设备的内存属性。这将返回可用的内存类型和堆的信息,包括每种内存类型的特性(如是否可被主机访问、是否可被设备访问等)。
  • 分配内存:

    • 使用 vkAllocateMemory 函数分配内存。VkMemoryAllocateInfo 结构体中包含了所需的内存大小和内存类型索引(memoryTypeIndex),该索引对应于之前查询到的内存类型。
VkResult vkAllocateMemory(
    VkDevice                                    device,
    const VkMemoryAllocateInfo*                 pAllocateInfo,
    const VkAllocationCallbacks*                pAllocator,
    VkDeviceMemory*                             pMemory);
2. 资源对象的创建与内存绑定

一旦分配了设备内存,接下来需要创建资源对象(如 Buffer 和 Image)并将其与内存绑定:

  • 创建资源对象:

    • 使用 vkCreateBuffervkCreateImage 函数创建 Buffer 或 Image 对象。在创建这些对象之前,开发者需要知道它们所需的内存大小和格式,可以通过 vkGetBufferMemoryRequirementsvkGetImageMemoryRequirements 函数获取这些信息。
  • 绑定内存:

    • 通过 vkBindBufferMemoryvkBindImageMemory 函数将资源对象与设备内存绑定。绑定后,资源对象才能被使用。
VkResult vkBindBufferMemory(
    VkDevice                                    device,
    VkBuffer                                    buffer,
    VkDeviceMemory                              memory,
    VkDeviceSize                                memoryOffset);

VkResult vkBindImageMemory(
    VkDevice                                    device,
    VkImage                                     image,
    VkDeviceMemory                              memory,
    VkDeviceSize                                memoryOffset);
3. 资源与内存的关系
  • 资源与内存的分离:

    • Vulkan 的设计理念是将资源对象与内存对象分离。这种分离使得开发者可以灵活地管理内存,允许多个资源对象共享同一块内存,只要它们的内存访问方式兼容,并且能够正确处理使用冲突。
  • 不可重新绑定:

    • 一旦资源对象与内存绑定后,不能再重新绑定到其他内存对象,也不能解绑。这种设计确保了资源的生命周期与其绑定的内存的生命周期一致,避免了潜在的内存管理问题。
4. 专用内存分配
  • 专用内存分配:
    • Vulkan 还支持专用内存分配,可以在创建内存对象时自动将其绑定到特定的 Buffer 或 Image。通过在 VkMemoryAllocateInfo 中使用 pNext 指向 VkMemoryDedicatedAllocateInfo 结构体,可以实现这一功能。这在某些情况下可以提高性能,因为它允许驱动程序更好地优化内存使用。

总结

Vulkan 的内存管理机制提供了高度的灵活性和控制能力,允许开发者精确管理设备内存的分配和使用。通过将内存对象与资源对象分离,Vulkan 使得多个资源可以共享同一块内存,同时也要求开发者在管理内存时更加小心,以避免资源冲突和内存泄漏。

Metal 和 OpenGL ES 的内存管理比较

1. Metal 的内存管理

在 Metal 中,资源的创建和内存的分配是紧密结合的。通过以下 API,开发者可以直接创建 Buffer 和 Texture 对象,并在创建时指定内存的访问方式:

  • 创建 Buffer:

    - (id<MTLBuffer>)newBufferWithLength:(NSUInteger)length 
                                   options:(MTLResourceOptions)options;
    
  • 创建 Texture:

    - (id<MTLTexture>)newTextureWithDescriptor:(MTLTextureDescriptor *)descriptor;
    

在 Metal 中,MTLResourceOptions 允许开发者指定资源的内存访问模式(如 CPU 可读、GPU 可写等),这使得内存管理更加灵活和高效。

2. OpenGL ES 的内存管理

与 Metal 不同,OpenGL ES 的资源创建和内存分配是分开的。开发者首先创建一个对象,然后再为其分配内存:

  • 创建 Buffer:

    glBindBuffer(GL_ARRAY_BUFFER, buffer);
    glBufferData(GL_ARRAY_BUFFER, size, data, usage);
    
  • 创建 Texture:

    glBindTexture(GL_TEXTURE_2D, texture);
    glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, type, data);
    

这种设计使得 OpenGL ES 的内存管理相对简单,但在某些情况下可能会导致性能瓶颈,特别是在需要频繁更新资源时。

3. OpenGL ES 的额外内存对象

OpenGL ES 引入了一些额外的内存对象,以解决特定的需求和限制。这些对象包括:

RenderBuffer
  • 用途: RenderBuffer 是一种特殊的内存对象,可以作为 Framebuffer 的附件。它不用于采样,而是专门用于渲染目的。
  • 优势:
    • 支持多重采样(MSAA)渲染,允许将渲染结果输出到多重采样的 RenderBuffer,而不是 Texture。
    • 在某些情况下,RenderBuffer 的性能可能优于 Texture,尤其是在不需要进行后续采样的情况下。
BackBuffer
  • 用途: OpenGL ES 允许直接从 Framebuffer 读写内存内容,以访问系统的 backbuffer。这种设计使得开发者能够在渲染过程中处理 backbuffer 的内容。
  • 限制: 在 Metal 和 Vulkan 中,backbuffer 通常是可以作为 Image 对象访问的,这使得对 backbuffer 的操作更加灵活。
PixelBuffer (PBO)
  • 用途: Pixel Buffer Object 是一种特殊类型的 Buffer,主要用于在 CPU 和 GPU 之间传输纹理数据。
  • 优势:
    • PBO 允许开发者在 CPU 和 GPU 之间异步传输数据,减少 CPU 和 GPU 之间的同步问题。
    • 开发者可以先将数据写入 PBO,然后再将其传输到 Texture,这在性能上通常优于直接将数据写入 Texture。

总结

Metal 和 OpenGL ES 在内存管理和资源创建方面的设计理念各有不同。Metal 提供了更为紧密的资源创建和内存分配机制,使得开发者能够更灵活地管理内存。而 OpenGL ES 则采用了更为传统的分离式设计,虽然在某些情况下可能导致性能瓶颈,但其简单性使得它易于使用。

OpenGL ES 还引入了一些额外的内存对象(如 RenderBuffer、BackBuffer 和 Pixel Buffer),以解决特定的需求和限制。这些对象的引入虽然增加了 API 的复杂性,但也为开发者提供了更多的灵活性和控制能力。

在图形编程和计算中,设备(Device)内存的管理和访问是一个复杂而重要的主题。设备内存通常指的是 GPU 的内存,而主机(Host)内存则是 CPU 的内存。由于主机和设备之间的内存访问模式不同,理解这些访问模式及其管理机制对于优化性能和避免潜在问题至关重要。以下是对不同访问情况的详细分析:

1. Device(GPU)侧的访问

1.1 Device 侧的 Read
  • 描述: GPU 从其内存中读取数据。这通常发生在着色器执行期间,着色器需要访问纹理、缓冲区等资源。
  • 管理: GPU 直接访问其内存,通常不需要额外的同步机制,因为所有的读取操作都是在 GPU 内部进行的。
1.2 Device 侧的 Write
  • 描述: GPU 向其内存中写入数据。这可能是渲染结果、计算结果或更新的纹理数据。
  • 管理: 写入操作通常在 GPU 的渲染管线或计算管线中进行,GPU 内部会管理这些写入操作的顺序和一致性。
1.3 Device 侧的 Copy
  • 描述: GPU 在其内存中复制数据。这可能涉及从一个缓冲区到另一个缓冲区的复制,或从纹理到缓冲区的复制。
  • 管理: 复制操作通常由 GPU 的命令缓冲区管理,开发者可以通过 API 提交复制命令,GPU 会在适当的时机执行这些命令。

2. Host(CPU)侧的访问

2.1 Host 侧的 Read
  • 描述: CPU 从 GPU 的内存中读取数据。这通常涉及到从 GPU 读取渲染结果或计算结果。
  • 管理: 由于 CPU 和 GPU 之间的访问是异步的,CPU 读取 GPU 内存时需要确保 GPU 完成了相关的写入操作。这通常通过同步机制(如信号量、事件或查询)来实现。
2.2 Host 侧的 Write
  • 描述: CPU 向 GPU 的内存中写入数据。这可能是更新纹理、缓冲区或其他资源。
  • 管理: 在进行写入操作之前,CPU 需要确保 GPU 不再使用该内存区域。这通常通过使用映射(mapping)机制来实现,开发者可以将 GPU 内存映射到 CPU 地址空间,从而进行直接访问。

3. 复杂性与管理机制

由于设备内存可能同时被主机和设备访问,因此在管理这些访问时需要考虑以下几个方面:

  • 同步: 由于 CPU 和 GPU 的操作是异步的,必须确保在进行读写操作时,相关的内存区域处于一致状态。常用的同步机制包括:

    • 事件: 使用事件对象来通知 CPU 或 GPU 某个操作已完成。
    • 查询: 通过查询 GPU 的状态,确保某个操作已完成。
    • 屏障: 在 GPU 内部使用屏障来确保某些操作的顺序。
  • 映射与解映射: 在 CPU 访问 GPU 内存时,通常需要将 GPU 内存映射到 CPU 地址空间。这一过程可能涉及性能开销,因此应尽量减少映射和解映射的频率。

  • 性能优化: 在设计内存访问模式时,开发者应考虑数据的局部性和访问模式,以减少 CPU 和 GPU 之间的带宽竞争。例如,尽量减少 CPU 和 GPU 之间的数据传输,使用异步传输等技术。

总结

设备内存的访问涉及多种情况,包括 GPU 侧的读、写和复制,以及 CPU 侧的读和写。由于主机和设备之间的异步特性,管理这些访问需要复杂的同步机制和映射策略。理解这些访问模式及其管理机制对于优化性能和确保数据一致性至关重要。

在 GPU 编程中,资源的管理和访问是一个关键的概念,尤其是在图形渲染和计算任务中。GPU 的数据处理管线设计为高效处理图形和计算任务,但与 CPU 的内存访问方式不同,GPU 需要通过特定的绑定机制来访问资源。以下是对 GPU 资源访问的详细说明,特别是关于绑定后的访问。

绑定后的访问

资源绑定的必要性

在 GPU 的数据处理管线中,资源(如纹理、缓冲区等)必须在使用之前通过特定的 API 进行绑定。这是因为 GPU 的管线设计为硬件化的,不能像 CPU 那样通过寻址的方式直接访问内存。相反,GPU 通过预留的资源绑定槽位来管理和访问这些资源。

资源绑定槽位

在 GPU 管线中,存在多个资源绑定槽位,这些槽位用于存放不同类型的资源。常见的资源绑定槽位包括:

  • Texture Units: 用于绑定纹理资源,供着色器进行采样。
  • Uniform Slots: 用于绑定统一变量(Uniforms),这些变量在渲染过程中保持不变,通常用于传递变换矩阵、光照参数等。
  • Array Buffers: 用于绑定数组缓冲区,供顶点着色器等使用。

在使用这些资源之前,开发者需要通过 API 将资源绑定到相应的槽位。例如:

  • 在 OpenGL 中,使用 glBindTexture 将纹理绑定到当前上下文。
  • 在 Vulkan 中,使用 vkCmdBindDescriptorSets 绑定描述符集。
  • 在 Metal 中,使用 [MtlRenderCommandEncoder setFragmentTexture] 绑定纹理。

GPU 读取操作

读取的基本情况

GPU 读取操作是对设备内存的基本访问方式,主要用于获取纹理和缓冲区中的数据。以下是几种常见的读取方式:

  1. 纹理读取:

    • 纹理在创建后,必须通过绑定 API 绑定到管线的特定位置。之后,着色器可以使用采样器对绑定的纹理进行采样。
    • 例如,在 OpenGL 中,使用 glBindTexture 将纹理绑定到当前上下文中,然后在着色器中使用 texture() 函数进行采样。
  2. 缓冲区读取:

    • 类似于纹理,缓冲区也需要通过 glBindBuffer 等 API 进行绑定,之后可以在着色器中通过索引访问缓冲区中的数据。
读取的类型

GPU 上的读取操作可以分为以下几种类型:

  1. Attachment 读取:

    • 这种读取方式涉及到将图像作为帧缓冲的附件(Attachment)进行读取。图像的内容在渲染过程中被加载到当前像素中,通常用于像素着色器的初始值或后续的混合测试。
    • 这种读取操作直接操作图像的 tile 内存。
  2. Shader 读取:

    • 在着色器中,开发者可以显式地对纹理进行采样。这种读取操作通常涉及到从图像的主内存中获取数据。
  3. Framebuffer Fetch:

    • 这种特殊的读取方式允许在着色器中直接访问 tile 内存,通常用于实现某些特定的效果,如后处理效果。

GPU 写入操作

写入的基本情况

GPU 的写入操作通常涉及将数据写入到设备内存中的资源。写入操作的典型情况包括:

  1. 作为 Render Pass 的 Attachment:

    • 在图形渲染过程中,图像可以作为渲染通道的附件被写入。GPU 在渲染过程中会将结果写入到这些附件中。
  2. 在 Compute Shader 中写入:

    • 在计算着色器中,开发者可以直接对图像或缓冲区的内存进行写入。这种方式允许更灵活的内存操作,适用于复杂的计算任务。
  3. 着色器中的写入:

    • 在顶点着色器(VS)和像素着色器(PS)中,通常只能进行读取操作,而不能直接写入资源。写入操作一般在计算着色器中进行。
写入的类型

与读取类似,GPU 的写入操作也可以根据操作的内存领域进行分类:

  1. Attachment 写:

    • 这种写入方式涉及将图像作为帧缓冲的附件进行写入,通常在渲染过程中进行。
  2. Shader 写:

    • 在计算着色器中,开发者可以直接对图像或缓冲区进行写入,允许对内存进行更灵活的操作。

总结

在 GPU 编程中,资源的绑定是访问设备内存的前提。GPU 的读取和写入操作具有特定的模式和类型,开发者需要通过适当的 API 进行资源的绑定和访问。理解这些操作的细节对于优化性能和实现复杂的图形效果至关重要。通过合理的资源管理和访问模式,可以有效提高 GPU 的计算效率和渲染性能。

在 GPU 编程中,读取操作是对设备内存的基本访问方式,主要用于获取纹理和缓冲区中的数据。以下是对 GPU 读取操作的详细说明,包括不同的读取情形和相关的 API。

GPU 读取操作

1. 读取的基本概念

GPU 读取操作是指从设备内存中获取数据的过程。常见的读取操作包括:

  • 纹理读取:在创建纹理后,通过绑定 API 将其绑定到管线的特定位置,然后在着色器中进行采样。
  • 缓冲区读取:在创建缓冲区后,通过绑定 API 将其绑定到管线,之后可以在着色器中通过索引访问缓冲区中的数据。
2. 纹理读取

对于纹理的访问,开发者需要使用特定的 API 将纹理绑定到管线的某个索引位置。常见的 API 包括:

  • OpenGL: glBindTexture
  • Vulkan: vkCmdBindDescriptorSets
  • Metal: [MtlRenderCommandEncoder setFragmentTexture]

在绑定后,着色器可以使用相应的采样函数(如 texture())对绑定的纹理进行采样。

3. 读取的类型

在 GPU 上,纹理的读取操作可以分为以下几种情形:

  1. Attachment 读取:

    • 这种读取方式涉及将图像作为帧缓冲的附件(Attachment)进行读取。图像的内容在渲染过程中被加载到当前像素中,通常用于像素着色器的初始值或后续的混合测试。
    • Attachment 读取直接操作图像的 tile 内存,通常在渲染过程中进行。
  2. Shader 读取:

    • 在着色器中,开发者可以显式地对纹理进行采样。这种读取操作通常涉及从图像的主内存中获取数据。
    • 例如,在片段着色器中,可以使用 texture() 函数对绑定的纹理进行采样。
  3. Framebuffer Fetch:

    • 这种特殊的读取方式允许在着色器中直接访问 tile 内存,通常用于实现某些特定的效果,如后处理效果。
    • 在 Vulkan 中,这种方式被称为 Input Attachment,允许着色器直接读取帧缓冲中的数据。
4. 缓冲区读取

对于缓冲区的读取,开发者同样需要使用绑定 API 将缓冲区绑定到管线。常见的 API 包括:

  • OpenGL: glBindBuffer
  • Vulkan: vkCmdBindDescriptorSets
  • Metal: [MtlRenderCommandEncoder setFragmentBuffer]

在绑定后,着色器可以通过索引访问缓冲区中的数据。例如,在顶点着色器或片段着色器中,可以使用 gl_VertexID 或其他索引来访问缓冲区中的元素。

总结

GPU 的读取操作是对设备内存的基本访问方式,主要用于获取纹理和缓冲区中的数据。通过适当的 API 进行资源的绑定,开发者可以在着色器中对这些资源进行有效的读取。理解不同的读取情形(如 Attachment 读取、Shader 读取和 Framebuffer Fetch)对于优化性能和实现复杂的图形效果至关重要。通过合理的资源管理和访问模式,可以有效提高 GPU 的计算效率和渲染性能。

在 GPU 编程中,写入操作是对设备内存的基本访问方式,主要用于将数据写入到纹理和缓冲区中。以下是对 GPU 写入操作的详细说明,包括不同的写入情形和相关的 API。

GPU 写入操作

1. 写入的基本概念

GPU 的写入操作是指将数据写入到设备内存中的资源。常见的写入操作包括:

  • 作为 Render Pass 的 Attachment 写入:在图形渲染过程中,图像作为帧缓冲的附件被写入。
  • 在 Compute Shader 中写入:在计算着色器中,开发者可以直接对图像或缓冲区的内存进行写入。

在顶点着色器(VS)和片段着色器(PS)中,通常只能进行读取操作,而不能直接写入资源。

2. 写入的类型

GPU 的写入操作可以根据操作的内存领域分为以下两种类型:

  1. Attachment 写:

    • 在图形渲染过程中,图像作为帧缓冲的附件进行写入。开发者可以通过将图像绑定到渲染通道,并设置相应的加载和存储操作来实现。
    • 例如,在 OpenGL 中,可以使用 glFramebufferRenderbuffer 将颜色缓冲区作为附件绑定到帧缓冲对象(FBO)。在 Vulkan 中,可以通过设置渲染通道的描述符来实现。
  2. Shader 写:

    • 在计算着色器中,开发者可以直接对图像或缓冲区的内存进行写入。这种方式允许更灵活的内存操作,适用于复杂的计算任务。
    • 在着色器中,可以使用 imageStore() 函数将数据写入到绑定的图像中,或者使用 bufferStore() 将数据写入到绑定的缓冲区。

4.3.1.2 Clear 操作

对于设备资源,GPU 还会发生一种特殊的写入操作,称为 Clear。Clear 操作将一个缓冲区或图像的所有元素设置为某个特定的值。

Clear 操作的实现

在图形渲染中,Clear 操作通常用于初始化渲染目标(Render Target, RT)。具体实现步骤如下:

  1. 绑定 Render Pass:

    • 将渲染目标(如颜色缓冲区)绑定到当前的渲染通道。
  2. 设置 Load Action:

    • 在渲染通道的设置中,将 Load Action 设置为 CLEAR,这表示在开始渲染之前,清除该附件的内容。
  3. 设置 Store Action:

    • 将 Store Action 设置为 STORE,这表示在渲染完成后,将结果存储到附件中。
  4. 设置 Clear Color:

    • 设置当前的 Clear Color,这将用于填充清除后的颜色缓冲区。
  5. 启动 Render Pass:

    • 启动渲染通道,GPU 将自动执行 Clear 操作。

这种基于图形管线的图像清除操作在许多 API 中都有实现,且通常提供了更简单的 API 来执行清除操作。例如,在 OpenGL 中,可以使用 glClearColorglClear 函数来清除颜色缓冲区。

总结

GPU 的写入操作是对设备内存的基本访问方式,主要用于将数据写入到纹理和缓冲区中。通过适当的 API 进行资源的绑定,开发者可以在图形渲染和计算任务中有效地进行写入操作。理解不同的写入情形(如 Attachment 写和 Shader 写)以及 Clear 操作的实现,对于优化性能和实现复杂的图形效果至关重要。通过合理的资源管理和访问模式,可以有效提高 GPU 的计算效率和渲染性能。

GPU Copy 操作

在 GPU 编程中,Copy 操作是一种常见的内存读写操作,主要用于在设备内存中高效地传输数据。Copy 操作可以在不同类型的对象之间进行,例如从图像(Image)到缓冲区(Buffer),或在同一类型的对象之间进行数据传输。以下是对 GPU Copy 操作的详细分析,特别是在 Vulkan 和 Metal 中的实现。

1. Copy 操作的基本概念

Copy 操作允许开发者在 GPU 设备内存中高效地移动数据。它可以涉及以下几种形式:

  • Buffer 到 Buffer:在两个缓冲区之间复制数据。
  • Image 到 Image:在两个图像之间复制数据。
  • Buffer 到 Image:将缓冲区的数据复制到图像中。
  • Image 到 Buffer:将图像的数据复制到缓冲区中。
2. Vulkan 中的 Copy 操作

在 Vulkan 中,Copy 操作的 API 设计非常明确,提供了四大类直观的 API 来完成内存之间的拷贝。具体如下:

2.1 Buffer 到 Buffer
  • vkCmdCopyBuffer2: 将源缓冲区 A 的某个区域拷贝到目标缓冲区 B 的某个区域。这是一个通用的缓冲区拷贝操作。
  • vkCmdUpdateBuffer: 也称为 Inline Copy,它将待拷贝的数据直接写入到命令缓冲区中,然后快速复制到目标缓冲区。此操作适用于小数据量(通常小于 64KB)。
2.2 Image 到 Image
  • vkCmdCopyImage2: 将源图像 A 的某个 mip 级别的某个区域拷贝到目标图像 B。这是一个简单的内存数据传输操作。
  • vkCmdBlitImage: 允许在不同格式之间进行拷贝,并支持缩放和图像过滤(如点采样和线性采样)。此操作不仅仅是数据搬运,还会对图像数据进行重解析,但不支持多重采样(multi-sampled)图像。
  • vkCmdResolveImage: 用于将多重采样图像解析为单采样图像,适用于后处理阶段。
2.3 Buffer 到 Image
  • vkCmdCopyBufferToImage: 将缓冲区的数据复制到图像中,常用于将 CPU 端的数据上传到 GPU 端的图像资源。
2.4 Image 到 Buffer
  • vkCmdCopyImageToBuffer: 将图像的数据复制到缓冲区中,常用于从 GPU 端读取图像数据到 CPU 端。
3. 注意事项

在进行图像拷贝操作时,源图像和目标图像的布局必须提前设置为适当的状态:

  • 源图像应设置为 VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL
  • 目标图像应设置为 VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL

此外,所有这些设备侧的 Copy 指令必须在渲染通道(Render Pass)之外记录,即它们的渲染通道范围是“外部的”(Outside)。

4. 提交到队列

上述所有 API 都支持在图形(Graphics)、计算(Compute)和传输(Transfer)队列中提交。这意味着 GPU 上的数据 Copy 操作可以在单独的传输队列中执行,从而提高性能和灵活性。

总结

GPU Copy 操作是设备内存中高效数据传输的重要手段。通过 Vulkan 提供的明确 API,开发者可以方便地在不同类型的对象之间进行数据拷贝。理解这些 Copy 操作的使用场景和注意事项,对于优化 GPU 的性能和资源管理至关重要。

Pixel Format

在图形编程中,Pixel Format(像素格式)是描述图像内存组织和数据传输行为的重要概念。与简单的缓冲区(Buffer)内存不同,图像(Image)内存具有特定的数据组织格式,这对于 GPU 的内存操作和数据传输至关重要。以下是对 Pixel Format 的详细讨论,包括其组成部分和不同类型。

1. Pixel Format 的组成

Pixel Format 通常由两个主要属性组成:

  • Data Type(数据类型)
  • Format(格式)

这些属性共同定义了图像数据的存储方式和访问方式。

2. Data Type

Data Type 用于描述在数据传输时,单位数据被视为何种类型。它可以是基本类型或打包类型。

2.1 基本类型

基本类型是指单个数据元素的类型,常见的基本类型包括:

  • UNSIGNED_BYTE: 1 字节的无符号整数。
  • BYTE: 1 字节的有符号整数。
  • HALF_FLOAT: 2 字节的半精度浮点数。
  • FLOAT: 4 字节的单精度浮点数。
  • UNSIGNED_SHORT: 2 字节的无符号整数。
  • INT: 4 字节的有符号整数。

这些基本类型直接影响数据的存储和处理方式。

2.2 Packed 类型

Packed 类型是指逻辑上由多个通道数据组成,但在存储上被打包为一个单位数据的类型。常见的 Packed 类型包括:

  • RGB565: 将 RGB 颜色信息打包为一个 16 位的无符号整数,其中 5 位用于红色,6 位用于绿色,5 位用于蓝色。
  • RGB4444: 将 RGBA 颜色信息打包为一个 16 位的无符号整数,其中每个通道占 4 位。
  • BGR10A2: 将 BGR 和 Alpha 通道信息打包为一个 32 位的无符号整数,其中 B、G、R 各占 10 位,A 占 2 位。
  • Depth24Stencil8: 将深度和模板信息打包为一个 32 位的无符号整数,其中前 24 位用于深度,后 8 位用于模板。
  • RG11B10FLOAT: 将 RGB 颜色信息打包为一个 32 位的浮点数,其中 R 和 G 通道各占 11 位,B 通道占 10 位。
  • RGB9E5FLOAT: 将 RGB 颜色信息打包为一个 32 位的浮点数,其中每个通道解释为浮点数,尾数有 9 位,公用一个 5 位的指数。

在这些打包数据中,单个通道数据通常被解释为无符号整数,除非特别说明。例如,BGR10A2 的解析结果依然是无符号整数。

3. Pixel Format 的重要性

Pixel Format 对于图像的存储、传输和处理具有重要影响。它决定了:

  • 数据在内存中的布局。
  • 数据的读取和写入方式。
  • 不同图像格式之间的相互适配性,尤其在进行 GPU 拷贝操作时。

在不同的图形 API(如 Vulkan、OpenGL、DirectX 等)中,Pixel Format 的定义和使用可能会有所不同,但其核心概念是相似的。理解 Pixel Format 的细节对于高效地处理图像数据和优化图形性能至关重要。

总结

Pixel Format 是图像内存组织和数据传输行为的关键概念,由数据类型和格式组成。通过理解基本类型和打包类型,开发者可以更好地管理图像数据的存储和处理,从而提高图形应用的性能和效率。

Pixel Format 的详细解析

在图形编程中,Pixel Format(像素格式)不仅仅是描述图像的存储方式,它还涉及到如何解释和处理这些数据。Pixel Format 主要由两个部分组成:Format(格式)和 Data Type(数据类型)。下面将详细讨论这两个概念及其相关内容。

1. Format 的定义

Format 是用来描述一个像素在内存中占用情况的关键概念,主要包括以下几个方面:

  • 通道组成:一个像素由多少个通道(元素)组成。
  • 每个通道的内存大小:每个通道占用多少字节。

Format 和 Data Type 是不同的概念。以下是一些具体的例子来说明它们之间的关系:

1.1 示例
  • RGBA8

    • Format: RGBA8
    • Data Type: UNSIGNED_BYTE
    • 说明:每个像素由 4 个通道组成,每个通道占用 1 字节(8 位),因此读取一个像素需要跨越 4 个字节。
  • RGB10_A2UI

    • Format: RGB10_A2UI
    • Data Type: UNSIGNED_INT_10_10_10_2_REV
    • 说明:每个像素由 4 个通道(RGB 和 A)组成,数据存储为一个打包的 32 位整数,其中 RGB 各占 10 位,A 占 2 位。
2. Format 的分类

Pixel Format 可以基本分为两大类:

  • 非压缩格式:直接存储每个通道的数据,通常以字节为单位。
  • 压缩格式:通过特定算法压缩数据以节省内存空间,常见的压缩格式包括 ASTC(Adaptive Scalable Texture Compression)。
2.1 压缩格式的特点
  • Data Type:压缩格式的 Pixel Format 通常将所有数据视为单字节(unsigned byte)。
  • Block Width/Height:压缩格式还会包含额外的属性,表示一个压缩单元的像素尺寸。
  • API 规范:不同的图形 API 会定义一些基本需要支持的压缩类型 Pixel Format,但不同硬件实现可能支持更多的格式。
3. Pixel 数据的解释

Pixel 数据可以以多种基本形式存储(如 byte、float、int 等),但 GPU 如何解释这些数据是至关重要的。常见的解释方式包括:

  • 解读为浮点数:对于各种位数的浮点数,GPU 直接将其解读为浮点数。
  • 定点数转化为浮点数:大多数 byte 类型数据实际上是定点数,最终会被 GPU 解读为归一化的浮点数。通常,16 位和 24 位的深度数据也是定点数:
    • snorm:归一化到 -1 到 1 之间的定点数。
    • unorm:归一化到 0 到 1 之间的定点数,或没有特殊后缀的定点数。
  • 解读为整数:直接将数据视为整数(int/uint 类型)。
  • 解读为索引:一些以 byte 存储的数据(如模板数据)会被视为索引。
  • LUMINANCE 类型:在 OpenGL ES 中,LUMINANCE 类型的数据也使用 byte 存储。

总结

Pixel Format 是图形编程中一个重要的概念,它不仅描述了图像数据的存储方式,还影响了数据的解释和处理。通过理解 Format 和 Data Type 的关系,以及如何解释不同类型的 Pixel 数据,开发者可以更有效地管理图像数据,从而优化图形应用的性能和效率。

GPU 对 Pixel 数据的读取流程

在图形处理单元(GPU)中,像素数据的读取和解析是一个复杂的过程,尤其是当数据以不同的格式存储时。GPU 主要识别的通道包括 RGBA、深度(DEPTH)和模板(STENCIL)通道。以下是数据从输入到 GPU 解析为可用格式的四个主要步骤:

1. Unpack(解包)
  • 过程:如果输入的数据是打包格式(如 RGB10_A2UI),首先需要将其解包为单独的数值。这意味着将一个打包的整数(如 32 位)分解为其组成部分(如 10 位的 R、G、B 和 2 位的 A)。
  • 输出:此步骤的结果是一个或多个独立的数值,可能是无符号整数(uint)或浮点数(float)。
2. Convert to Float(转换为浮点数)
  • 过程:在这一阶段,所有数据(无论是浮点数、定点数、整数、索引或亮度)都需要被转换为 GPU 可识别的浮点数格式。对于定点数,GPU 会根据其位数将其转换为归一化的浮点数。
    • 浮点数:直接使用。
    • 定点数:根据其位数(如 8 位、16 位、24 位)转换为浮点数,通常转换为 floathalf 类型。
  • 输出:此步骤的结果是所有数据都被转换为浮点数格式。
3. Convert to RGB(转换为 RGB)
  • 过程:如果当前数据是 LUMINANCE 类型(亮度),则需要将其转换为 RGB 通道。通常,LUMINANCE 数据会被复制到 R、G 和 B 通道,而 A 通道则保持为 1(完全不透明)。
  • 输出:此步骤的结果是所有数据都被组织为 RGBADS 通道的浮点数、整数或索引格式。
4. Final to RGBA(最终转换为 RGBA)
  • 过程:在这一阶段,对于深度和模板通道(DEPTH 和 STENCIL),保持当前数值不变。对于 RGB 通道,缺失的通道用 0 填充,缺失的 A 通道用 1 填充。这样,所有数据最终都被转换为 RGBA 格式。
  • 输出:此步骤的结果是所有数据都以 RGBA 的浮点数、整数或索引格式存在,或者是深度和模板的浮点数或索引格式,确保 GPU 能够识别和处理。

总结

通过以上四个步骤,GPU 能够将不同格式的像素数据解析为其能够理解的 RGBA、DEPTH 和 STENCIL 通道格式。这一过程确保了数据在 GPU 中的有效处理和渲染。相应地,当 GPU 进行数据写入时,也会遵循类似的反向流程,将 RGBA 数据转换回原始格式并存储。

Pixel Format 的应用范围

在图形 API 中,像素格式(Pixel Format)是一个重要的概念,它定义了图像数据的存储方式和结构。不同的 API 对像素格式的支持和定义有所不同,以下是对可用作图像格式、帧缓冲格式以及不同平台 API 中像素格式定义的详细说明。

1. 可作为 Image 格式的 Pixel Format
  • 定义:可作为图像格式的像素格式是所有像素格式中的一个子集。这些格式可以用于图像的创建和数据传输相关的 API。
  • 注意事项:不同的图形 API 可能支持不同的像素格式,因此在使用时需要查阅具体的 API 文档。大多数现代图形 API 都支持常见的像素格式,如 RGBA、RGB、灰度等。
2. 可被用在 Framebuffer 上的 Pixel Format
  • 定义:可用于帧缓冲的像素格式是可作为图像格式的像素格式的一个更小的子集。这些格式必须满足特定的要求,以便在帧缓冲中使用。
  • 限制:并非所有的压缩格式都可以用作颜色渲染目标(Color Render Target)或深度模板渲染目标(Depth Stencil Render Target)。具体的支持情况需要查阅相关 API 文档。

各平台 API 中 Pixel Format 的定义

Vulkan
  • 定义:在 Vulkan 中,像素格式使用 VkFormat 来定义,命名规范为 VK_FORMAT_{component-format|compression-scheme}_{numeric-format}
  • 示例:例如,VK_FORMAT_R8G8B8A8_UNORM 表示一个包含红、绿、蓝和 alpha 通道的 8 位无符号整型格式。
Metal
  • 定义:在 Metal 中,像素格式使用枚举类型 MTLPixelFormat 来定义。它将像素格式分为多个部分,包括普通类型、打包类型、压缩类型和深度模板类型等。
  • 示例:例如,MTLPixelFormatRGBA8Unorm 表示一个 RGBA 格式的 8 位无符号整型。
OpenGL ES
  • 定义:在 OpenGL ES 中,像素格式的定义相对复杂,分为两部分:
    • Base Internal Format:表示每个像素的基本布局,不考虑每个像素的位数,包含通道数目和每个通道的意义。常见的格式包括:
      • RGB, RGBA, RG, RED
      • DEPTH_COMPONENT, DEPTH_STENCIL, STENCIL_INDEX
      • LUMINANCE_ALPHA, LUMINANCE, ALPHA
    • Sized Internal Format:基于 Base Internal Format 扩展的格式,包含每个通道的位数和类型信息。通常被称为纹理格式。
  • 注意事项:在 OpenGL ES 中,只有特定的类型、基本内部格式和大小内部格式的组合才是合理的。所有合理的像素格式组合需要参考规范中的表格(Valid combinations of format, type, and unsized internal format)。

总结

像素格式在图形 API 中扮演着重要角色,不同的 API 对其支持和定义有所不同。了解这些格式的应用范围和限制对于开发图形应用程序至关重要。在使用时,开发者应查阅相关文档,以确保所选格式的兼容性和有效性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

你一身傲骨怎能输

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

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

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

打赏作者

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

抵扣说明:

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

余额充值