第10章 Compiler - 编译器测试

Compiler 测试套件验证 OpenCL 编译器的核心功能,包括在线编译、链接、编译选项、预处理器、错误处理等。这是确保 OpenCL 程序能够正确编译和执行的关键测试。

10.1 编译器测试概述

10.1.1 OpenCL 编译模型

OpenCL 支持两种编译模型:

1. 传统编译模型(OpenCL 1.x)

源代码 → clBuildProgram → 可执行程序 → clCreateKernel → 内核对象

2. 分离编译链接模型(OpenCL 2.0+)

源代码 → clCompileProgram → 编译对象 (.o)
                              ↓
多个编译对象 → clLinkProgram → 可执行程序 → clCreateKernel → 内核对象

10.1.2 测试分类

测试类别测试数量主要功能
程序加载测试7源代码加载、多字符串源、程序信息查询
编译选项测试5优化选项、宏定义、包含目录、非规格化数缓存
预处理器测试4#define、#include、#line、#pragma
版本与扩展测试3OpenCL C 版本、扩展宏、特性宏
分离编译链接测试24独立编译、静态链接、嵌入式头文件、多文件链接
编译器卸载测试7编译器资源卸载、多线程卸载

10.1.3 核心测试列表

test_definition test_list[] = {
    // 程序加载
    ADD_TEST(load_program_source),
    ADD_TEST(load_multistring_source),
    ADD_TEST(load_two_kernel_source),
    ADD_TEST(load_null_terminated_source),
    ADD_TEST(get_program_source),
    ADD_TEST(get_program_build_info),
    ADD_TEST(get_program_info),
    
    // 编译与构建
    ADD_TEST(large_compile),
    ADD_TEST(async_build),
    
    // 编译选项
    ADD_TEST(options_build_optimizations),
    ADD_TEST(options_build_macro),
    ADD_TEST(options_build_macro_existence),
    ADD_TEST(options_include_directory),
    ADD_TEST(options_denorm_cache),
    
    // 预处理器
    ADD_TEST(preprocessor_define_udef),
    ADD_TEST(preprocessor_include),
    ADD_TEST(preprocessor_line_error),
    ADD_TEST(preprocessor_pragma),
    
    // 版本与扩展
    ADD_TEST(opencl_c_versions),
    ADD_TEST(compiler_defines_for_extensions),
    ADD_TEST(image_macro),
    
    // 分离编译与链接
    ADD_TEST(simple_compile_only),
    ADD_TEST(simple_link_only),
    ADD_TEST(execute_after_simple_compile_and_link),
    ADD_TEST(two_file_link),
    ADD_TEST(multiple_files),
    ADD_TEST(multiple_libraries),
    
    // 编译器卸载
    ADD_TEST(unload_valid),
    ADD_TEST(unload_repeated),
    ADD_TEST(unload_build_threaded),
};

10.2 在线编译测试

10.2.1 基本程序编译

OpenCL 程序从源代码编译的基本流程:

// 1. 创建程序对象
cl_program program = clCreateProgramWithSource(
    context,
    1,                    // 源字符串数量
    &kernel_source,       // 源代码
    NULL,                 // 自动检测长度
    &error);

// 2. 构建程序
error = clBuildProgram(
    program,
    1,                    // 设备数量
    &device,              // 目标设备
    NULL,                 // 编译选项
    NULL,                 // 回调函数
    NULL);                // 回调数据

// 3. 检查构建状态
if (error != CL_SUCCESS) {
    // 获取构建日志
    size_t log_size;
    clGetProgramBuildInfo(program, device,
                         CL_PROGRAM_BUILD_LOG,
                         0, NULL, &log_size);
    
    char *build_log = (char*)malloc(log_size);
    clGetProgramBuildInfo(program, device,
                         CL_PROGRAM_BUILD_LOG,
                         log_size, build_log, NULL);
    
    log_error("Build failed:\n%s\n", build_log);
    free(build_log);
}

// 4. 创建内核
cl_kernel kernel = clCreateKernel(program, "kernel_name", &error);

10.2.2 多字符串源代码

OpenCL 允许程序由多个字符串组成:

const char *source_strings[] = {
    "// 第一部分:常量定义\n"
    "#define ARRAY_SIZE 1024\n"
    "\n",
    
    "// 第二部分:辅助函数\n"
    "float helper_function(float x) {\n"
    "    return x * 2.0f;\n"
    "}\n"
    "\n",
    
    "// 第三部分:内核函数\n"
    "__kernel void process(__global float *data) {\n"
    "    int id = get_global_id(0);\n"
    "    data[id] = helper_function(data[id]);\n"
    "}\n"
};

// 使用多个字符串创建程序
cl_program program = clCreateProgramWithSource(
    context,
    3,                    // 三个源字符串
    source_strings,
    NULL,
    &error);

测试要点

  • 验证多字符串源代码能够正确拼接
  • 确保跨字符串的函数调用正常工作
  • 检查预处理器指令在多字符串中的行为

10.2.3 异步编译

OpenCL 支持异步编译,允许在编译过程中执行其他操作:

// 异步编译回调函数
void CL_CALLBACK build_callback(cl_program program, void *user_data) {
    int *build_complete = (int*)user_data;
    *build_complete = 1;
    log_info("Build completed asynchronously\n");
}

// 启动异步编译
int build_complete = 0;
error = clBuildProgram(
    program,
    1, &device,
    NULL,
    build_callback,      // 回调函数
    &build_complete);    // 回调数据

// 等待编译完成
while (!build_complete) {
    // 可以执行其他操作
    usleep(1000);
}

// 或使用事件等待
clWaitForEvents(1, &build_event);

10.2.4 大规模程序编译

测试编译器处理大型程序的能力:

// 生成包含大量代码行的程序
char large_kernel_source[MAX_SIZE];
char *ptr = large_kernel_source;

ptr += sprintf(ptr, "__kernel void large_test(__global float *data) {\n");
ptr += sprintf(ptr, "    int id = get_global_id(0);\n");
ptr += sprintf(ptr, "    float temp = data[id];\n");

// 生成 1000 行计算语句
for (int i = 0; i < 1000; i++) {
    ptr += sprintf(ptr, "    temp = temp * 1.001f + 0.001f;\n");
}

ptr += sprintf(ptr, "    data[id] = temp;\n");
ptr += sprintf(ptr, "}\n");

// 编译大型程序
cl_program program = clCreateProgramWithSource(context, 1,
                                              (const char**)&large_kernel_source,
                                              NULL, &error);
error = clBuildProgram(program, 1, &device, NULL, NULL, NULL);

测试验证

  • 编译器能够处理大型程序
  • 编译时间在合理范围内
  • 编译后的程序能够正确执行

10.3 编译选项测试

10.3.1 优化选项

OpenCL 提供多种编译优化选项:

选项说明
-cl-opt-disable禁用所有优化
-cl-mad-enable允许 a*b+c 融合为 mad 指令
-cl-no-signed-zeros允许忽略有符号零
-cl-unsafe-math-optimizations允许不安全的数学优化
-cl-finite-math-only假设参数和结果不是 NaN 或 Inf
-cl-fast-relaxed-math快速宽松数学模式
-cl-single-precision-constant单精度常量
-cl-denorms-are-zero非规格化数视为零

测试示例

const char *optimization_options[] = {
    "-cl-opt-disable",
    "-cl-mad-enable",
    "-cl-fast-relaxed-math",
    NULL
};

for (int i = 0; optimization_options[i] != NULL; i++) {
    log_info("Testing option: %s\n", optimization_options[i]);
    
    error = clBuildProgram(program, 1, &device,
                          optimization_options[i],
                          NULL, NULL);
    
    if (error != CL_SUCCESS) {
        log_error("Failed to build with option %s\n",
                 optimization_options[i]);
        return -1;
    }
}

10.3.2 宏定义选项

通过编译选项定义宏:

// 内核源代码使用宏
const char *kernel_with_macro =
"__kernel void test(__global int *output) {\n"
"    output[0] = VALUE;\n"
"}\n";

// 方式1:单个宏定义
error = clBuildProgram(program, 1, &device,
                      "-DVALUE=42",
                      NULL, NULL);

// 方式2:多个宏定义
error = clBuildProgram(program, 1, &device,
                      "-DWIDTH=1024 -DHEIGHT=768",
                      NULL, NULL);

// 方式3:宏展开为表达式
error = clBuildProgram(program, 1, &device,
                      "-DARRAY_SIZE=(1024*1024)",
                      NULL, NULL);

// 验证宏是否生效
cl_int result;
// ... 执行内核并读取 result
if (result != 42) {
    log_error("Macro definition failed\n");
}

宏存在性测试

const char *existence_test =
"__kernel void test(__global int *output) {\n"
"#ifdef TEST_MACRO\n"
"    output[0] = 1;  // 宏已定义\n"
"#else\n"
"    output[0] = 0;  // 宏未定义\n"
"#endif\n"
"}\n";

// 测试:定义宏
clBuildProgram(program1, 1, &device, "-DTEST_MACRO", NULL, NULL);
// 执行后 output[0] 应该为 1

// 测试:不定义宏
clBuildProgram(program2, 1, &device, NULL, NULL, NULL);
// 执行后 output[0] 应该为 0

10.3.3 包含目录选项

使用 -I 选项指定头文件搜索路径:

// 创建头文件:./include/common.h
const char *header_content =
"#ifndef COMMON_H\n"
"#define COMMON_H\n"
"#define MAGIC_NUMBER 42\n"
"#endif\n";

// 内核源代码包含头文件
const char *kernel_with_include =
"#include \"common.h\"\n"
"__kernel void test(__global int *output) {\n"
"    output[0] = MAGIC_NUMBER;\n"
"}\n";

// 使用 -I 指定包含目录
char build_options[256];
sprintf(build_options, "-I%s/include", test_directory);

error = clBuildProgram(program, 1, &device,
                      build_options,
                      NULL, NULL);

多级目录测试

// 目录结构:
// ./include/
//   ├── level1.h
//   └── subdir/
//       └── level2.h

// level1.h 包含 level2.h
"#include \"subdir/level2.h\"\n"

// 编译选项
"-I./include"

10.3.4 警告与错误控制

// -w:抑制所有警告
clBuildProgram(program, 1, &device, "-w", NULL, NULL);

// -Werror:将警告视为错误
clBuildProgram(program, 1, &device, "-Werror", NULL, NULL);

// 组合使用
clBuildProgram(program, 1, &device, "-Werror -cl-opt-disable", NULL, NULL);

10.4 预处理器测试

10.4.1 #define 和 #undef

const char *define_test =
"#define VALUE 100\n"
"__kernel void test1(__global int *out) {\n"
"    out[0] = VALUE;\n"  // 应该是 100
"}\n"
"#undef VALUE\n"
"#define VALUE 200\n"
"__kernel void test2(__global int *out) {\n"
"    out[0] = VALUE;\n"  // 应该是 200
"}\n";

测试要点

  • #define 正确定义宏
  • #undef 正确取消宏定义
  • 宏重定义行为
  • 宏展开的递归处理

10.4.2 #include 指令

// 测试相对路径包含
"#include \"header.h\"\n"

// 测试绝对路径包含
"#include \"/path/to/header.h\"\n"

// 测试嵌套包含
// file1.h 包含 file2.h
// file2.h 包含 file3.h

// 测试循环包含保护
// header.h:
"#ifndef HEADER_H\n"
"#define HEADER_H\n"
"// 内容\n"
"#endif\n"

10.4.3 #pragma 指令

// 测试 #pragma OPENCL EXTENSION
const char *extension_pragma =
"#pragma OPENCL EXTENSION cl_khr_fp64 : enable\n"
"__kernel void test(__global double *data) {\n"
"    data[0] = 3.14159;\n"
"}\n";

// 测试 #pragma unroll (OpenCL 2.0+)
const char *unroll_pragma =
"__kernel void test(__global int *data) {\n"
"    #pragma unroll 4\n"
"    for (int i = 0; i < 16; i++) {\n"
"        data[i] = i * 2;\n"
"    }\n"
"}\n";

// 测试未知 pragma(应该被忽略)
"#pragma unknown_directive\n"

10.4.4 #line 和 #error

// 测试 #line 指令
const char *line_test =
"#line 100 \"virtual_file.cl\"\n"
"__kernel void test() {\n"
"    int x = UNDEFINED;  // 错误应该报告在 virtual_file.cl:101\n"
"}\n";

// 测试 #error 指令
const char *error_test =
"#ifndef REQUIRED_MACRO\n"
"#error \"REQUIRED_MACRO must be defined\"\n"
"#endif\n";

// 编译应该失败,并输出错误消息

10.5 版本与扩展测试

10.5.1 OpenCL C 版本宏

OpenCL C 预定义版本宏:

宏名称OpenCL 版本宏值
__OPENCL_VERSION__所有版本版本号(如 120, 200, 300)
__OPENCL_C_VERSION__OpenCL 1.1+C 语言版本号
CL_VERSION_1_01.0+100
CL_VERSION_1_11.1+110
CL_VERSION_1_21.2+120
CL_VERSION_2_02.0+200
CL_VERSION_3_03.0+300

测试示例

const char *version_test =
"__kernel void test(__global int *output) {\n"
"#ifdef __OPENCL_VERSION__\n"
"    output[0] = __OPENCL_VERSION__;\n"
"#else\n"
"    output[0] = 0;\n"
"#endif\n"
"#ifdef CL_VERSION_2_0\n"
"    output[1] = 1;  // 支持 OpenCL 2.0\n"
"#else\n"
"    output[1] = 0;  // 不支持\n"
"#endif\n"
"}\n";

10.5.2 扩展宏定义

设备支持的扩展会自动定义相应的宏:

// 查询设备支持的扩展
char extensions[4096];
clGetDeviceInfo(device, CL_DEVICE_EXTENSIONS,
               sizeof(extensions), extensions, NULL);

// 测试扩展宏
const char *extension_macro_test =
"__kernel void test(__global int *output) {\n"
"    int idx = 0;\n"
"#ifdef cl_khr_fp64\n"
"    output[idx++] = 1;  // 支持 双精度\n"
"#endif\n"
"#ifdef cl_khr_3d_image_writes\n"
"    output[idx++] = 2;  // 支持 3D 图像写入\n"
"#endif\n"
"#ifdef cl_khr_gl_sharing\n"
"    output[idx++] = 3;  // 支持 OpenGL 共享\n"
"#endif\n"
"}\n";

10.5.3 图像支持宏

const char *image_macro_test =
"__kernel void test(__global int *output) {\n"
"#ifdef __IMAGE_SUPPORT__\n"
"    output[0] = 1;  // 设备支持图像\n"
"#else\n"
"    output[0] = 0;  // 不支持图像\n"
"#endif\n"
"}\n";

// 验证宏与设备能力一致
cl_bool image_support;
clGetDeviceInfo(device, CL_DEVICE_IMAGE_SUPPORT,
               sizeof(cl_bool), &image_support, NULL);

// 执行内核并检查结果
// output[0] 应该与 image_support 一致

10.5.4 特性宏(OpenCL 3.0+)

OpenCL 3.0 引入可选特性宏:

// __opencl_c_3d_image_writes
// __opencl_c_atomic_order_acq_rel
// __opencl_c_atomic_scope_device
// __opencl_c_generic_address_space
// __opencl_c_pipes
// __opencl_c_work_group_collective_functions

const char *feature_macro_test =
"__kernel void test(__global int *output) {\n"
"#ifdef __opencl_c_atomic_order_acq_rel\n"
"    output[0] = 1;\n"
"#endif\n"
"#ifdef __opencl_c_generic_address_space\n"
"    output[1] = 1;\n"
"#endif\n"
"}\n";

10.6 分离编译与链接

10.6.1 基本编译与链接流程

// 步骤1:编译源代码到目标文件
cl_program compiled_program = clCompileProgram(
    program,
    1, &device,
    NULL,                    // 编译选项
    0, NULL, NULL,           // 头文件
    NULL, NULL);             // 回调

// 步骤2:链接目标文件到可执行程序
cl_program linked_program = clLinkProgram(
    context,
    1, &device,
    NULL,                    // 链接选项
    1, &compiled_program,    // 输入程序
    NULL, NULL,              // 回调
    &error);

// 步骤3:创建内核
cl_kernel kernel = clCreateKernel(linked_program, "kernel_name", &error);

10.6.2 多文件编译

// 文件1:辅助函数
const char *helper_source =
"float helper(float x) {\n"
"    return x * 2.0f;\n"
"}\n";

// 文件2:主内核(使用辅助函数)
const char *kernel_source =
"extern float helper(float x);\n"
"__kernel void main_kernel(__global float *data) {\n"
"    int id = get_global_id(0);\n"
"    data[id] = helper(data[id]);\n"
"}\n";

// 分别编译
cl_program helper_program = /* 创建并编译 helper_source */;
cl_program kernel_program = /* 创建并编译 kernel_source */;

// 链接到一起
cl_program programs[] = { helper_program, kernel_program };
cl_program linked = clLinkProgram(context, 1, &device, NULL,
                                 2, programs,
                                 NULL, NULL, &error);

10.6.3 静态库

// 创建静态库
cl_program library = clLinkProgram(
    context,
    1, &device,
    "-create-library",       // 创建库而不是可执行程序
    1, &compiled_program,
    NULL, NULL,
    &error);

// 使用库链接程序
cl_program programs[] = { library, main_program };
cl_program executable = clLinkProgram(context, 1, &device, NULL,
                                     2, programs,
                                     NULL, NULL, &error);

10.6.4 嵌入式头文件

// 创建嵌入式头文件
const char *header_name = "embedded_header.h";
const char *header_source =
"#define CONSTANT_VALUE 42\n"
"typedef struct { int x, y; } Point;\n";

cl_program header_program = clCreateProgramWithSource(
    context, 1, &header_source, NULL, &error);

// 编译时包含嵌入式头文件
error = clCompileProgram(
    main_program,
    1, &device,
    NULL,
    1,                      // 头文件数量
    &header_program,        // 头文件程序
    &header_name,           // 头文件名称
    NULL, NULL);

10.7 编译器卸载测试

10.7.1 基本卸载测试

// 编译并使用程序
cl_program program = /* 创建和构建程序 */;
cl_kernel kernel = clCreateKernel(program, "test", &error);

// 执行内核
/* ... */

// 清理
clReleaseKernel(kernel);
clReleaseProgram(program);

// 卸载编译器(释放编译器资源)
error = clUnloadPlatformCompiler(platform);
test_error(error, "Failed to unload compiler");

10.7.2 多次卸载测试

// 测试重复卸载
for (int i = 0; i < 10; i++) {
    error = clUnloadPlatformCompiler(platform);
    if (error != CL_SUCCESS) {
        log_error("Unload failed at iteration %d\n", i);
        return -1;
    }
}

10.7.3 卸载后重新编译

// 1. 编译程序
cl_program program1 = /* 构建程序 */;

// 2. 卸载编译器
clUnloadPlatformCompiler(platform);

// 3. 尝试重新编译(应该成功)
cl_program program2 = clCreateProgramWithSource(context, 1, &source, NULL, &error);
error = clBuildProgram(program2, 1, &device, NULL, NULL, NULL);

// 编译器应该能够重新初始化并正常工作

10.7.4 多线程卸载测试

验证在多线程环境下卸载编译器的安全性。


10.8 编译错误处理

10.8.1 获取构建日志

int get_build_log(cl_program program, cl_device_id device)
{
    size_t log_size;
    cl_int error;
    
    // 获取日志大小
    error = clGetProgramBuildInfo(program, device,
                                 CL_PROGRAM_BUILD_LOG,
                                 0, NULL, &log_size);
    if (error != CL_SUCCESS)
        return error;
    
    // 分配缓冲区
    char *build_log = (char*)malloc(log_size + 1);
    if (!build_log)
        return CL_OUT_OF_HOST_MEMORY;
    
    // 获取日志内容
    error = clGetProgramBuildInfo(program, device,
                                 CL_PROGRAM_BUILD_LOG,
                                 log_size, build_log, NULL);
    if (error != CL_SUCCESS) {
        free(build_log);
        return error;
    }
    
    build_log[log_size] = '\0';
    log_info("Build log:\n%s\n", build_log);
    
    free(build_log);
    return CL_SUCCESS;
}

10.8.2 构建状态查询

// 查询构建状态
cl_build_status build_status;
clGetProgramBuildInfo(program, device,
                     CL_PROGRAM_BUILD_STATUS,
                     sizeof(cl_build_status),
                     &build_status, NULL);

switch (build_status) {
    case CL_BUILD_SUCCESS:
        log_info("Build succeeded\n");
        break;
    case CL_BUILD_ERROR:
        log_error("Build failed\n");
        get_build_log(program, device);
        break;
    case CL_BUILD_IN_PROGRESS:
        log_info("Build in progress\n");
        break;
    case CL_BUILD_NONE:
        log_info("Build not started\n");
        break;
}

10.8.3 常见编译错误

错误类型示例解决方法
语法错误缺少分号检查语法
未定义标识符使用未声明的变量声明变量或包含头文件
类型不匹配float x = 1;使用 1.0f
扩展未启用使用 double 但未启用 cl_khr_fp64添加 #pragma OPENCL EXTENSION
包含文件未找到#include "missing.h"使用 -I 指定路径

10.9 编译器测试最佳实践

10.9.1 测试策略

  1. 渐进式测试:从简单程序开始,逐步增加复杂度
  2. 覆盖所有选项:测试所有编译选项组合
  3. 错误路径测试:验证错误处理是否正确
  4. 性能测试:测量编译时间和编译后性能
  5. 跨平台测试:在不同设备和平台上测试

10.9.2 调试技巧

// 1. 启用详细输出
"-cl-nv-verbose"         // NVIDIA
"-cl-amd-verbose"        // AMD

// 2. 保存中间文件
"-save-temps"            // 保存临时文件

// 3. 生成汇编代码
"-cl-nv-asm"            // NVIDIA PTX

10.9.3 注意事项

  • 编译选项顺序:某些选项可能相互影响
  • 设备差异:不同设备可能支持不同的特性
  • 版本兼容性:确保代码兼容目标 OpenCL 版本
  • 内存限制:大型程序可能超出编译器限制
  • 线程安全:多线程编译时需要注意同步

本章小结

第10章详细介绍了 Compiler 测试套件的核心内容:

  • 在线编译:程序创建、构建、异步编译、大规模程序处理
  • 编译选项:优化选项、宏定义、包含目录、警告控制
  • 预处理器:#define、#include、#pragma、#line、#error 指令
  • 版本与扩展:版本宏、扩展宏、图像宏、特性宏
  • 分离编译链接:独立编译、多文件链接、静态库、嵌入式头文件
  • 编译器卸载:资源释放、重复卸载、多线程安全
  • 错误处理:构建日志、状态查询、常见错误

编译器测试确保 OpenCL 编译器能够正确处理各种源代码,支持标准规定的所有特性,并提供清晰的错误诊断信息。

代码下载地址: https://pan.quark.cn/s/35e46f7e83fb 关于 Build Status Lines of code 这是一个参考 PotPlayer 的界面使用 Java 以及图形界面框架 JavaFX 使用 MCV 图形界面与业务逻辑分离的开发模式, 所开发的个人视频播放器项目, 开发这个项目旨在于学习图形界面框架 JavaFX 实现了具有和 PotPlayer相同 的简洁界面和流畅的操作逻辑。 Note: PotPlayer 是 KMPlayer 的原制作者姜龙喜先生(韩国)进入 Daum 公司后的 新一代网络播放器, PotPlayer的优势在于强大的内置解码器以及支持各类的 视频格式, 而且是免费下载提供使用的。 目前版本: 2020/10/28 v1.0.0 [x] 支持打开文件自动播放 [x] 支持查看播放记录 [x] 支持屏幕边沿窗口自动吸附 [x] 支持双击视频来播放和暂停 [x] 支持左键点击窗口任意位置来拖到窗口 [x] 支持左键双击播放窗口打开文件 [x] 支持根据视频尺寸自动调整窗口大小 [x] 支持根据播放文件类型调整窗口模式 [x] 支持根据视频尺寸自动调整窗口显示位置防止超出屏幕 [x] 支持记录上一次访问的文件路径 [x] 支持播放记录文件读写 已实现样式 未播放效果: 播放效果: 运行环境 本项目使用 NetBeans 配合 JDK 开发, NetBeans8.0 以及 JDK8.0 以上版本的均可以运行。 亦可使用其他集成开发环境, 例如 Eclipse, IntelliJ IDEA 配合使用 JDK8.0 以上版本均可构建此项目。 NetBeans download Eclipse downlo...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

DeeplyMind

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

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

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

打赏作者

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

抵扣说明:

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

余额充值