Android-GPU压测工具

用 200 行 C++ 写一个 Android GPU 压测工具

无窗口、多线程、纯 ALU 负载------一个可以直接在 adb shell 下运行的 EGL/GLES3 离屏渲染压测器。

一、为什么需要它

给 GPU"烤机"的工具不少,但大多有一个前提:要有一个窗口。glmark2 这类基准要跑在 Activity 或 X11 窗口里,意味着你得先有 SurfaceFlinger 参与的图形栈链路,测试本身也被合成器、刷新率等因素干扰。

而在驱动调试、温控测试、GPU 稳定性验证这些场景里,我们其实想要一个更"纯"的东西:

  • 不依赖任何窗口系统------adb shell 一敲就能跑,设备哪怕没有 UI 也能压;
  • CPU 几乎不参与------负载真正落在 GPU 上,而不是主线程忙循环;
  • 压力大小可精确调节------一个参数就能从"轻负载"拧到"单帧算到天荒地老";
  • 能检测 GPU 挂死------驱动卡死时能报出来,而不是陪着一起卡住。

这个 demo 就是这么一个工具。整体只用了 200 来行 C++,没有任何第三方依赖。

二、总体设计

arduino 复制代码
                     ┌────────────────────────┐
   argv: threads /   │  main                  │
   seconds / iters / │  解析参数、注册信号、起线程 │
   width / height    └───────────┬────────────┘
                                 │ pthread_create × N + 1
        ┌────────────────────────┼──────────────────────┐
        ▼                        ▼                      ▼
 ┌─────────────┐         ┌─────────────┐        ┌──────────────┐
 │ stressThread│   ...   │ stressThread│        │ reportThread │
 │ EGL+GLES3   │         │ EGL+GLES3   │        │ 每5秒算fps   │
 │ 离屏渲染循环 │         │ 离屏渲染循环 │        └──────┬───────┘
 └──────┬──────┘         └──────┬──────┘               │
        │      写 g_frames(fetch_add)                  │ 读 g_frames
        └──────────────────┬───────────────────────────┘
                           ▼
              g_run (atomic<bool> 停止开关)
         写入方: SIGINT/SIGTERM 信号处理函数、主线程超时

命令行用法:

css 复制代码
gpu_stress_demo [threads] [seconds] [iters] [width] [height]
参数 含义 默认值
threads 压测线程数(上限 32) 3
seconds 运行时长,0 表示跑到 Ctrl+C 0
iters 片元着色器每像素循环次数(负载旋钮) 2000
width/height 离屏 pbuffer 分辨率 1080×1920

四条设计原则贯穿全文:

  1. 窗口系统完全不参与(pbuffer 离屏渲染);
  2. CPU 几乎不干活 (顶点用 gl_VertexID 生成,负载全在 GPU 侧);
  3. vsync 关掉(测极限吞吐,而不是测刷新率);
  4. swap 当探针(交换失败即疑似 GPU 挂死)。

下面逐个拆解核心点。

三、核心点详解

核心点 1:离屏渲染------pbuffer 让工具摆脱窗口系统

传统的 EGL 渲染需要 eglCreateWindowSurface,背后是 SurfaceFlinger 分配的窗口缓冲。而压测工具用的是 pbuffer(pixel buffer) :

scss 复制代码
const EGLint pbAttribs[] = {
    EGL_WIDTH,  p->width,     // 离屏缓冲的分辨率
    EGL_HEIGHT, p->height,
    EGL_NONE};
surf = eglCreatePbufferSurface(dpy, cfg, pbAttribs);

pbuffer 是一块纯 GPU 侧的离屏渲染目标,不挂任何窗口,也不需要合成器参与。渲染结果没人看------但这本来就是压测,画面内容无所谓。带来的好处是:

  • 不需要 Android UI、不需要 App、不需要任何权限,adb shell 直接运行;
  • 渲染管线里没有合成器这个变量,测出来的吞吐更接近 GPU 的裸性能;
  • eglSwapBuffers 在 pbuffer 上依然有效,仍然充当驱动提交/同步点(后面会用到)。

注意选 config 时要声明我们要 pbuffer:

arduino 复制代码
const EGLint cfgAttribs[] = {
    EGL_SURFACE_TYPE,    EGL_PBUFFER_BIT,   // 支持离屏 pbuffer
    EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,// GLES3
    EGL_NONE};

核心点 2:每个线程一套独立的 EGL 环境

OpenGL ES 的 context 有严格的线程绑定语义:一个 context 同一时刻只能被一个线程 makeCurrent。所以多线程压测不是"共享一个 GL 环境",而是每个线程从头到尾自己搭一套:

scss 复制代码
dpy  = eglGetDisplay(EGL_DEFAULT_DISPLAY);   // 同一个显示连接
eglInitialize(dpy, nullptr, nullptr);        // 每线程各自初始化
eglChooseConfig(...);                        // 各自选配置
surf = eglCreatePbufferSurface(...);         // 各自的离屏 surface
ctx  = eglCreateContext(...);                // 各自的 GLES3 context
eglMakeCurrent(dpy, surf, surf, ctx);        // 绑定到本线程

线程之间唯一的通信是两个原子变量(停止开关和帧计数),GL 世界完全隔离。这样每个线程就是一个独立的"客户端",N 个线程就模拟了 N 个应用同时向 GPU 提交负载------这恰好也是很多车机/多窗口场景下 GPU 的真实压力形态。

创建 context 时指定 GLES 3.0(片元着色器要用 gl_VertexID 和 layout 限定符,都是 ES 3.0 特性):

ini 复制代码
const EGLint ctxAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};

核心点 3:关闭 vsync------eglSwapInterval(dpy, 0)

这行代码只有一次调用,但决定了测出来的是什么:

scss 复制代码
eglSwapInterval(dpy, 0);   // 0 = 不同步到垂直刷新信号
  • 不关 vsync:每秒最多提交 60/90/120 帧(取决于屏幕刷新率),你测的是"能不能跑满刷新率",帧率到 60 就封顶了;
  • 关掉 vsync :GPU 画完一帧立刻画下一帧,吞吐可以到每秒几万帧,你测的是裸的渲染吞吐极限。

压测要的是后者。后面的输出示例里 fps=26561.6 这种"离谱"的数字,就是关了 vsync 之后多线程提交速率的正常水平------注意它和屏幕刷新没有任何关系。

核心点 4:不用 VBO 画满全屏------gl_VertexID 技巧

常规 GLES 教程画三角形要建 VBO、配 VAO、传顶点属性......而这里整个程序没有一个顶点缓冲 ,靠的是 ES 3.0 的内建顶点编号 gl_VertexID:

python 复制代码
static const char* kVS = R"(#version 300 es
void main() {
    vec2 p = vec2((gl_VertexID == 1 || gl_VertexID == 3) ? 1.0 : -1.0,
                  (gl_VertexID == 2 || gl_VertexID == 3) ? 1.0 : -1.0);
    gl_Position = vec4(p, 0.0, 1.0);
})";

glDrawArrays(GL_TRIANGLE_STRIP, 0, 4) 会依次发出 4 个顶点(编号 0~3),顶点着色器里根据编号直接算出裁剪空间坐标:

gl_VertexID x y 位置
0 -1 -1 左下
1 +1 -1 右下
2 -1 +1 左上
3 +1 +1 右上

TRIANGLE_STRIP 模式下这 4 个点正好拼成一个覆盖整个视口的四边形,于是每个像素都会执行一次片元着色器------这正是压测需要的。零 VBO 带来的好处:CPU 侧零准备、零内存拷贝,压测循环里 CPU 除了提交命令什么都不做,测到的退化全是 GPU 的。

核心点 5:片元着色器才是真正的压力源

整个工具的"发动机"是这段片元着色器:

scss 复制代码
static const char* kFS = R"(#version 300 es
precision highp float;
layout(location = 0) out vec4 oColor;
uniform int uIter;
void main() {
    float s = 0.0;
    float x = 0.1;
    for (int i = 0; i < 100000; i++) {
        if (i >= uIter) break;                    // 负载旋钮在这里
        x = x * 1.0001 + 0.001;
        s += fract(sin(x * 78.233) * 43758.5453);
    }
    oColor = vec4(s * 1e-6, 0.0, 0.0, 1.0);      // 输出无所谓
})";

几个关键设计:

  • 纯 ALU 负载 :不采样纹理、不访问 buffer,每轮循环就是乘加 + sin + fract------专门打 GPU 的浮点运算单元。想测带宽就把循环换成纹理采样,负载类型由着色器决定。

  • uIter 是负载旋钮 :循环上限硬编码 100000,实际跑多少圈由 uniform 控制。CPU 侧一行 glUniform1i 就能在线调节负载强度,不用重编着色器:

    less 复制代码
    glUniform1i(glGetUniformLocation(prog, "uIter"), p->iters);  // 循环外设一次
  • highp 必须显式声明 :ES 的片元着色器里 float 默认精度是 mediump(很多驱动上是 16 位半精度),压 ALU 必须写 precision highp float; 强制单精度。

  • 结果输出接近零 :s * 1e-6 让画面基本是黑色------防止有人误把测试输出当 UI。同时保留了对 s 的依赖,避免激进驱动把"无副作用"的计算整个优化掉。

总计算量的公式:

arduino 复制代码
每秒 GPU 运算量 ≈ fps × width × height × iters

四个变量里 iters 和分辨率是输入侧旋钮,fps 是输出侧仪表。

核心点 6:主循环与 GPU 挂死检测

scss 复制代码
while (g_run.load(std::memory_order_acquire)) {
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);           // 提交一帧
    if (eglSwapBuffers(dpy, surf) != EGL_TRUE) {     // 同步点
        LOGE("thread %d: eglSwapBuffers failed (%#x), GPU may be hung",
             p->idx, eglGetError());
        break;
    }
    g_frames.fetch_add(1, std::memory_order_release);
    frames++;
}

这里有一个理解 GL 的关键点:glDrawArrays 是异步的 。它只是把命令塞进驱动的命令流就返回了,CPU 不会等 GPU 画完。真正产生"背压"的是 eglSwapBuffers------它要求这帧画完,驱动要么等 GPU 追上来,要么阻塞/报错。

于是 swap 天然承担了两个角色:

  1. 节流阀:CPU 提交太快时,swap 会让它慢下来,保证命令队列不无限膨胀;
  2. 挂死探测器 :如果 GPU 真的挂了(驱动超时、GPU reset、TDR),swap 会返回失败而不是永远卡死------线程打印 GPU may be hung 后自己退出,不会陪葬。这也是把 swap 的返回值单独判断而不是靠 glGetError 兜底的原因。

核心点 7:线程协调------两个原子量解决所有问题

c 复制代码
static std::atomic<bool> g_run{true};                     // 停止开关
static std::atomic<unsigned long long> g_frames{0};       // 总帧数
  • g_run 是唯一的停止开关,有两个写入方 :信号处理函数(Ctrl+C / kill)和主线程的定时逻辑。所有压测线程和汇报线程都在循环条件里读它。用 acquire/release 内存序保证标志位和帧计数的变化不会被乱序可见性问题吞掉。
  • g_frames 由每个线程在每帧完成后 fetch_add(1) 原子累加,reportThread 每 5 秒 load 一次------所有线程的真实总和,无锁、无争抢热点(原子加的开销相对一帧的渲染时间可以忽略)。

信号处理保持了最小化------异步信号安全的前提下只做一件事:

c 复制代码
static void sigHandler(int) { g_run.store(false, std::memory_order_release); }

主线程的收尾逻辑则把"定时停止"和"信号停止"统一到同一个开关上:

scss 复制代码
if (seconds > 0) { sleep(seconds); g_run.store(false, ...); }
for (int i = 0; i < threads; i++) pthread_join(tids[i], nullptr);
pthread_join(reporter, nullptr);

顺带一提:Android 的 bionic libc 里 pthread 是内建的,链接时既不需要 -pthread 也不需要 libpthread。

核心点 8:FPS 统计与解读

reportThread 的实现只有几行:

c 复制代码
static void* reportThread(void*) {
    unsigned long long last = 0;
    while (g_run.load(std::memory_order_acquire)) {
        sleep(5);
        unsigned long long total = g_frames.load(std::memory_order_acquire);
        LOGI("fps(last 5s)=%.1f total=%llu", (total - last) / 5.0, total);
        last = total;
    }
    return nullptr;
}

看到这样的输出时------

css 复制代码
I/gpu_stress: fps(last 5s)=26561.6 total=132808

含义是:

  • total=132808:从启动到现在,所有线程累加完成的总帧数;
  • fps(last 5s)=26561.6 :最近 5 秒窗口的平均速率 (total_now − total_last) / 5。上面这条 132808 ÷ 5 = 26561.6 严丝合缝,说明它是第一次汇报(基准还是 0)。

解读要点:

  1. 这里的 fps 是 swap 完成次数/秒,不是屏幕刷新率------离屏渲染 + vsync 关闭 + 多线程求和,几万是正常值;
  2. 它是衡量 GPU 饱和度的仪表 :iters 从小往大调,fps 会一路下降;fps 掉到个位数、不再增长、或直接吐出 GPU may be hung,就是 GPU 的极限/稳定性边界;
  3. 典型测试方法------固定线程数和分辨率,iters 按 100 → 1k → 5k → 20k → 100k 梯度跑,记录每档 fps,就能画出这台设备的"负载-吞吐"曲线。

四、完整代码

scss 复制代码
// gpu_stress_demo.cpp --- 极简 Android GPU 压测工具
// (离屏 pbuffer + 每线程独立 GLES3 环境 + 片元 ALU 热循环)
//
// 用法: gpu_stress_demo [threads] [seconds] [iters] [width] [height]
//   默认: threads=3 seconds=0(无限, Ctrl+C 停止) iters=2000 width=1080 height=1920
//   线程数上限 32
​
#include <EGL/egl.h>
#include <GLES3/gl3.h>
#include <atomic>
#include <pthread.h>
#include <signal.h>
#include <unistd.h>
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
#include <cstring>
​
#define LOG_TAG "gpu_stress"
​
// 日志直接打到控制台(stdout), adb shell 下即可看到; 无需 logcat
static void conlog(char prio, const char* fmt, ...) {
    va_list ap;
    va_start(ap, fmt);
    std::printf("%c/%s: ", prio, LOG_TAG);
    std::vprintf(fmt, ap);
    std::printf("\n");
    va_end(ap);
    std::fflush(stdout);  // 管道/重定向下也立即上屏
}
​
#define LOGI(...) conlog('I', __VA_ARGS__)
#define LOGE(...) conlog('E', __VA_ARGS__)
​
// 唯一的停止开关: 信号处理函数 / 主线程超时都会把它置 false
static std::atomic<bool> g_run{true};
// 所有线程 fetch_add 累加的总帧数, reportThread 每 5 秒读一次
static std::atomic<unsigned long long> g_frames{0};
​
struct ThreadParams {              // 传给每个压测线程的参数
    int idx;                       // 线程编号(只用于日志)
    int iters;                     // 片元着色器每像素循环次数
    int width;                     // pbuffer 分辨率
    int height;
};
​
// ---------- 顶点着色器: 用 gl_VertexID 生成全屏四边形, 不需要任何 VBO/VAO ----------
static const char* kVS = R"(#version 300 es
void main() {
    vec2 p = vec2((gl_VertexID == 1 || gl_VertexID == 3) ? 1.0 : -1.0,
                  (gl_VertexID == 2 || gl_VertexID == 3) ? 1.0 : -1.0);
    gl_Position = vec4(p, 0.0, 1.0);
})";
​
// ---------- 片元着色器: ALU 压力核心, 循环上限 100000, 由 uniform uIter 控制实际次数 ----------
static const char* kFS = R"(#version 300 es
precision highp float;
layout(location = 0) out vec4 oColor;
uniform int uIter;
void main() {
    float s = 0.0;
    float x = 0.1;
    for (int i = 0; i < 100000; i++) {
        if (i >= uIter) break;
        x = x * 1.0001 + 0.001;
        s += fract(sin(x * 78.233) * 43758.5453);
    }
    oColor = vec4(s * 1e-6, 0.0, 0.0, 1.0);
})";
​
static GLuint compileShader(GLenum type, const char* src) {
    GLuint shader = glCreateShader(type);
    glShaderSource(shader, 1, &src, nullptr);
    glCompileShader(shader);
    GLint ok = 0;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
    if (!ok) {
        char log[512] = {0};
        glGetShaderInfoLog(shader, sizeof(log), nullptr, log);
        LOGE("shader compile failed: %s", log);
        return 0;
    }
    return shader;
}
​
// 每线程独立的 EGL/GLES 环境 + 渲染循环
static void* stressThread(void* arg) {
    ThreadParams* p = (ThreadParams*)arg;
    unsigned long long frames = 0;
    EGLDisplay dpy = EGL_NO_DISPLAY;
    EGLSurface surf = EGL_NO_SURFACE;
    EGLContext ctx = EGL_NO_CONTEXT;
​
    do {
        // ---- EGL 初始化: 离屏 pbuffer, 不依赖窗口/SurfaceFlinger ----
        dpy = eglGetDisplay(EGL_DEFAULT_DISPLAY);
        if (dpy == EGL_NO_DISPLAY || !eglInitialize(dpy, nullptr, nullptr)) break;
​
        const EGLint cfgAttribs[] = {
            EGL_SURFACE_TYPE,    EGL_PBUFFER_BIT,
            EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
            EGL_NONE};
        EGLConfig cfg = nullptr; EGLint n = 0;
        if (!eglChooseConfig(dpy, cfgAttribs, &cfg, 1, &n) || n < 1) {
            LOGE("thread %d: no EGL config", p->idx); break;
        }
​
        const EGLint pbAttribs[] = {
            EGL_WIDTH,  p->width,
            EGL_HEIGHT, p->height,
            EGL_NONE};
        surf = eglCreatePbufferSurface(dpy, cfg, pbAttribs);
        if (surf == EGL_NO_SURFACE) break;
​
        const EGLint ctxAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
        ctx = eglCreateContext(dpy, cfg, EGL_NO_CONTEXT, ctxAttribs);
        if (ctx == EGL_NO_CONTEXT || !eglMakeCurrent(dpy, surf, surf, ctx)) {
            LOGE("thread %d: EGL setup failed (%#x)", p->idx, eglGetError()); break;
        }
        eglSwapInterval(dpy, 0);               // 关闭 vsync, 拉满提交速率
​
        // ---- 着色器管线 ----
        GLuint prog = glCreateProgram();
        GLuint vs = compileShader(GL_VERTEX_SHADER, kVS);
        GLuint fs = compileShader(GL_FRAGMENT_SHADER, kFS);
        glAttachShader(prog, vs);
        glAttachShader(prog, fs);
        glLinkProgram(prog);
        GLint linked = 0;
        glGetProgramiv(prog, GL_LINK_STATUS, &linked);
        if (!linked) { LOGE("thread %d: program link failed", p->idx); break; }
​
        glUseProgram(prog);
        glUniform1i(glGetUniformLocation(prog, "uIter"), p->iters);  // 设一次负载旋钮
        glViewport(0, 0, p->width, p->height);
        LOGI("thread %d: start (%dx%d, iters=%d)", p->idx, p->width, p->height, p->iters);
​
        // ---- 压测主循环 ----
        while (g_run.load(std::memory_order_acquire)) {
            glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);           // 全屏四边形, 无 VBO
            if (eglSwapBuffers(dpy, surf) != EGL_TRUE) {     // 同步点 + 挂死探测
                LOGE("thread %d: eglSwapBuffers failed (%#x), GPU may be hung",
                     p->idx, eglGetError());
                break;
            }
            g_frames.fetch_add(1, std::memory_order_release);
            frames++;
        }
        LOGI("thread %d: done, %llu frames", p->idx, frames);
    } while (0);
​
    // ---- 统一清理: do{}while(0) 里任何一步失败都会落到这里 ----
    if (dpy != EGL_NO_DISPLAY) {
        eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
        if (ctx   != EGL_NO_CONTEXT) eglDestroyContext(dpy, ctx);
        if (surf  != EGL_NO_SURFACE) eglDestroySurface(dpy, surf);
        eglTerminate(dpy);
    }
    return nullptr;
}
​
// 每 5 秒打印一次吞吐统计
static void* reportThread(void*) {
    unsigned long long last = 0;
    while (g_run.load(std::memory_order_acquire)) {
        sleep(5);
        unsigned long long total = g_frames.load(std::memory_order_acquire);
        LOGI("fps(last 5s)=%.1f total=%llu", (total - last) / 5.0, total);
        last = total;
    }
    return nullptr;
}
​
static void sigHandler(int) { g_run.store(false, std::memory_order_release); }
​
int main(int argc, char* argv[]) {
    int threads = argc > 1 ? atoi(argv[1]) : 3;      // 默认 3 线程
    int seconds = argc > 2 ? atoi(argv[2]) : 0;      // 0 = 一直跑到 Ctrl+C
    int iters   = argc > 3 ? atoi(argv[3]) : 2000;   // 每像素循环次数
    int width   = argc > 4 ? atoi(argv[4]) : 1080;
    int height  = argc > 5 ? atoi(argv[5]) : 1920;
    if (threads > 32) threads = 32;
​
    signal(SIGINT, sigHandler);
    signal(SIGTERM, sigHandler);
    LOGI("gpu_stress: threads=%d seconds=%d iters=%d size=%dx%d",
         threads, seconds, iters, width, height);
​
    pthread_t tids[32] = {0};
    ThreadParams params[32];
    for (int i = 0; i < threads; i++) {
        params[i] = {i, iters, width, height};
        pthread_create(&tids[i], nullptr, stressThread, &params[i]);
    }
    pthread_t reporter = 0;
    pthread_create(&reporter, nullptr, reportThread, nullptr);
​
    if (seconds > 0) { sleep(seconds); g_run.store(false, std::memory_order_release); }
​
    for (int i = 0; i < threads; i++) pthread_join(tids[i], nullptr);
    pthread_join(reporter, nullptr);
    LOGI("gpu_stress: exit");
    return 0;
}

五、构建与运行

方式一:NDK 命令行(推荐,最快)

bash 复制代码
$NDK/toolchains/llvm/prebuilt/<host>/bin/aarch64-linux-android33-clang++ \
    gpu_stress_demo.cpp -o gpu_stress_demo -O2
​
adb push gpu_stress_demo /data/local/tmp/
adb shell "chmod +x /data/local/tmp/gpu_stress_demo"
adb shell "/data/local/tmp/gpu_stress_demo 4 30 2000 1080 1920"

方式二:集成进 AOSP 源码树(Soong / Android.bp)

在源码目录放一个 Android.bp:

ruby 复制代码
cc_binary {
    name: "gpu_stress_demo",
    srcs: ["gpu_stress_demo.cpp"],
    shared_libs: [
        "libEGL",
        "libGLESv2",
    ],
    stl: "libc++_static",       // 用到 std::atomic
    compile_multilib: "64",     // 按目标架构选择
}

然后 m gpu_stress_demo 单编。

六、跑起来的样子与调参建议

bash 复制代码
$ gpu_stress_demo 2 10 5000 1080 1920
I/gpu_stress: gpu_stress: threads=2 seconds=10 iters=5000 size=1080x1920
I/gpu_stress: thread 0: start (1080x1920, iters=5000)
I/gpu_stress: thread 1: start (1080x1920, iters=5000)
I/gpu_stress: fps(last 5s)=42.0 total=210
I/gpu_stress: fps(last 5s)=38.4 total=402
I/gpu_stress: thread 0: done, 201 frames
I/gpu_stress: thread 1: done, 201 frames
I/gpu_stress: gpu_stress: exit

实操建议:

  • 找吞吐曲线 :固定线程数/分辨率,iters 按 100 → 1k → 5k → 20k → 100k 梯度跑,记录每档 fps,就能得到该设备的"负载-吞吐"关系;
  • 烤机/温控测试 :选一个 fps 掉到几十的 iters 档位长时间跑(seconds=0),配合温度监控观察降频行为;
  • 稳定性验证 :多线程 + 大 iters 持续跑,盯 eglSwapBuffers failed ... GPU may be hung------车机、盒子这类长时运行设备的 GPU 压力回归测试可以直接拿它做负载源;
  • 注意 iters=0 时片元循环空转,fps 会非常高,属于无效测试点,从 100 起步比较有意义。

七、总结

这个工具的"轻"和"狠"都来自同一种思路------把一切不影响 GPU 的东西都砍掉:

常规做法 本工具 收益
窗口 surface 离屏 pbuffer 摆脱窗口系统,adb shell 直接跑
VBO/VAO 顶点数据 gl_VertexID 程序化生成 CPU 零准备、零拷贝
开着 vsync eglSwapInterval(dpy, 0) 测极限吞吐而非刷新率
忽略 swap 返回值 swap 失败即报"GPU may be hung" 自带挂死探测
复杂的线程管理 两个原子量 200 行以内搞定

可扩展的方向也很自然:把片元循环换成纹理采样就是带宽压测;加 compute shader 版本就是 GPGPU 压测;把 uIter 改成运行中动态调节,就是一个最简 GPU 负载发生器。核心骨架不变,换的只是那一段着色器。

相关推荐
SL_staff2 小时前
10人团队如何用轻量私有化文档平台实现知识复用提效?技术视角拆解B/S架构下的协作闭环
java·开源·全栈
释厄6232 小时前
同性线套娃律——圆面积必然=1+2+...+n 的求和·圆心不与 π 相关
java·人工智能·算法·1024程序员节
猫猫.1532 小时前
IO、存储、硬盘、文件系统详细总结
java·java-ee
SL_staff3 小时前
设备离线时业务不中断:JVS-IOT高可用架构解析
java·物联网·产品
郭小李3 小时前
mybatis 动态 sql
java·sql·mybatis
瑞码空间3 小时前
JSP与Servlet核心开发与CRUD实战笔记
java·笔记·servlet·jsp
xcLeigh3 小时前
KingbaseES数据库教程:初识数据库,从安装到跑通第一条SQL
android·数据库·sql·安装·数据库教程·kes
知守观3 小时前
技术考古:iBATIS 2.3.4 时代的持久层,是如何用 Map 玩转 307 个存储过程的?
java·后端·mybatis
captain3764 小时前
MyBatis Plus(1)
java·服务器·mybatis