OpenGL ES 像素缓冲对象 (PBO) 总结

概述

OpenGL ES 中的像素缓冲对象 (PBO) 是一种专为像素操作设计的缓冲区,用于优化 CPU 和 GPU 之间的数据传输。它针对大数据量像素移动的性能瓶颈,提供高效解决方案。

传统方法如使用 glTexImage2D 上传数据到 GPU,或 glReadPixels 下载数据回 CPU,通常涉及同步操作,导致管道阻塞。PBO 通过直接内存访问 (DMA) 实现异步传输,减少 CPU 负载,提高整体效率。

主要优势

  • 加速像素上传(如从 CPU 到 GPU 的纹理传输)。
  • 高效像素下载(如从 GPU 渲染帧到 CPU)。
  • 从 OpenGL ES 3.0 开始支持,适用于移动应用,如 Android 视频处理。

PBO 的应用场景

PBO 适用于需要高速像素处理的场合,例如:

  • 相机应用中的实时视频编码,将 GPU 处理的帧读取到 CPU 进行编码。
  • 虽然 Android 有 ImageReader 等替代方案,但 PBO 在 GPU 密集型任务中表现更优。

PBO 的初始化

PBO 的创建类似于其他 OpenGL 缓冲区。使用 GL_PIXEL_UNPACK_BUFFER 处理上传 (CPU 到 GPU),GL_PIXEL_PACK_BUFFER 处理下载 (GPU 到 CPU)。

以下是为 RGBA 图像初始化两个 PBO 的示例:

void initializePBOs() {
    int bufferSize = imgWidth * imgHeight * 4;  // RGBA 格式的字节大小

    // 上传 PBO
    glGenBuffers(1, &uploadPBO);
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, uploadPBO);
    glBufferData(GL_PIXEL_UNPACK_BUFFER, bufferSize, nullptr, GL_STREAM_DRAW);
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);

    // 下载 PBO
    glGenBuffers(1, &downloadPBO);
    glBindBuffer(GL_PIXEL_PACK_BUFFER, downloadPBO);
    glBufferData(GL_PIXEL_PACK_BUFFER, bufferSize, nullptr, GL_STREAM_DRAW);
    glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);

    LOGD("Initialized PBOs: upload=%d, download=%d, size=%d", uploadPBO, downloadPBO, bufferSize);
}
  • GL_STREAM_DRAW 用于提示数据频繁更新。
  • 如需调整大小,后续可重新调用 glBufferData

使用 PBO 上传纹理

上传纹理是将像素数据从 CPU 传输到 OpenGL 纹理。步骤包括:

  1. 绑定 PBO 到 GL_PIXEL_UNPACK_BUFFER
  2. 映射缓冲区并拷贝数据。
  3. 使用 glTexSubImage2D 更新纹理(比 glTexImage2D 更高效,避免内存重分配)。

单 PBO 上传示例:

void uploadTexture(void* pixelData, int width, int height) {
    imgWidth = width;
    imgHeight = height;
    initializePBOs();  // 确保 PBO 已初始化

    if (textureID == 0) {
        glGenTextures(1, &textureID);
        glActiveTexture(GL_TEXTURE0);
        glUniform1i(samplerLocation, 0);
        glBindTexture(GL_TEXTURE_2D, textureID);

        // 设置纹理参数
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

        // 初始化空纹理
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
        glGenerateMipmap(GL_TEXTURE_2D);
    }

    glBindTexture(GL_TEXTURE_2D, textureID);

    int dataSize = width * height * 4;
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, uploadPBO);

    // 映射并拷贝数据
    GLubyte* mappedBuffer = (GLubyte*)glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, dataSize, GL_MAP_WRITE_BIT);
    if (mappedBuffer) {
        memcpy(mappedBuffer, pixelData, dataSize);
        glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
    }

    // 从 PBO 更新纹理
    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);

    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
    glBindTexture(GL_TEXTURE_2D, 0);
}

此方法确保上传过程非阻塞。

使用 PBO 下载像素

下载渲染像素的步骤:

  1. 绑定 PBO 到 GL_PIXEL_PACK_BUFFER
  2. 使用 glReadPixels 异步传输数据到 PBO。
  3. 映射缓冲区以访问 CPU 数据。

单 PBO 下载示例:

void downloadPixels(uint8_t** outputData, int* outWidth, int* outHeight) {
    *outWidth = viewportWidth;
    *outHeight = viewportHeight;
    int dataSize = viewportWidth * viewportHeight * 4;
    *outputData = new uint8_t[dataSize];

    glBindBuffer(GL_PIXEL_PACK_BUFFER, downloadPBO);
    glBufferData(GL_PIXEL_PACK_BUFFER, dataSize, nullptr, GL_STREAM_DRAW);  // 如需调整大小

    glReadPixels(0, 0, *outWidth, *outHeight, GL_RGBA, GL_UNSIGNED_BYTE, 0);

    GLubyte* mappedBuffer = (GLubyte*)glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, dataSize, GL_MAP_READ_BIT);
    if (mappedBuffer) {
        memcpy(*outputData, mappedBuffer, dataSize);
        glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
    } else {
		LOGD("Mapping failed");
    }

    glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
}

注意:使用 PBO 时,glReadPixels 的最后一个参数作为偏移,实现异步操作。

双 PBO 优化

为进一步提升性能,可使用双 PBO(每个方向两个),交替操作以利用 DMA 的异步特性,减少等待时间。

双 PBO 初始化:

void initializeDualPBOs() {
    int bufferSize = imgWidth * imgHeight * 4;
    uploadPBOs = new GLuint[2];
    glGenBuffers(2, uploadPBOs);
    for (int i = 0; i < 2; ++i) {
        glBindBuffer(GL_PIXEL_UNPACK_BUFFER, uploadPBOs[i]);
        glBufferData(GL_PIXEL_UNPACK_BUFFER, bufferSize, nullptr, GL_STREAM_DRAW);
        glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
    }

    // 下载 PBO 类似初始化
}

双 PBO 上传:

void uploadTextureDual(void* pixelData, int width, int height) {
    // ... (类似于单 PBO,但绑定 uploadPBOs[uploadIndex % 2])
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, uploadPBOs[uploadIndex % 2]);
    // 映射和拷贝...
    // 更新后:
    uploadIndex++;
}

下载操作类似。

示例实现

以下是一个完整类,用于处理基于 PBO 的纹理上传、渲染和下载,使用双 PBO 支持动态更新。

头文件 (PBOHandler.h)

#ifndef PBO_HANDLER_H
#define PBO_HANDLER_H

#include <GLES3/gl3.h>  // OpenGL ES 3.0 头文件
#include <EGL/egl.h>    // EGL 头文件,用于上下文管理
#include <stdlib.h>     // 用于 new/delete
#include <string.h>     // 用于 memcpy
#include <android/log.h>// Android 日志(可选,如果非 Android 可替换为 std::cout)

#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "PBO", __VA_ARGS__)

const int PBO_COUNT = 2;

class PBOHandler {
public:
    PBOHandler();
    ~PBOHandler();
    void drawFrame();
    void updateTexture(void* data, int width, int height, int len);
    void fetchPixels(uint8_t** data, int* width, int* height);

private:
    void initProgram(const char* vsSource, const char* fsSource);  // 初始化着色器程序
    void setupPBOArray();  // 初始化双 PBO 数组
    void swapBuffers();    // 模拟交换缓冲(实际需调用 eglSwapBuffers)

    GLuint prog = 0;       // 着色器程序 ID
    GLint posAttr = -1;    // 位置属性位置
    GLint texAttr = -1;    // 纹理坐标属性位置
    GLuint vertexBuffer = 0;  // 顶点缓冲对象
    GLuint vertexArray = 0;   // 顶点数组对象
    GLuint indexBuffer = 0;   // 索引缓冲对象
    GLuint texID = 0;         // 纹理 ID
    GLint texUniform = -1;    // 纹理 uniform 位置
    int texWidth = 0;         // 纹理宽度
    int texHeight = 0;        // 纹理高度
    GLuint* uploadBuffers = nullptr;   // 上传 PBO 数组
    GLuint* downloadBuffers = nullptr; // 下载 PBO 数组
    int uploadIdx = 0;        // 上传 PBO 索引
    int downloadIdx = 0;      // 下载 PBO 索引
    int viewWidth = 1920;     // 视口宽度(默认值,可外部设置)
    int viewHeight = 1080;    // 视口高度(默认值,可外部设置)
};

#endif // PBO_HANDLER_H

源文件 (PBOHandler.cpp)

#include "PBOHandler.h"

// 顶点着色器源码
static const char* vsSource = "#version 300 es\n"
                              "in vec4 pos;\n"
                              "in vec2 uv;\n"
                              "out vec2 fragUV;\n"
                              "void main() {\n"
                              "    fragUV = uv;\n"
                              "    gl_Position = pos;\n"
                              "}";

// 片元着色器源码
static const char* fsSource = "#version 300 es\n"
                              "precision mediump float;\n"
                              "out vec4 color;\n"
                              "in vec2 fragUV;\n"
                              "uniform sampler2D tex;\n"
                              "void main() {\n"
                              "    color = texture(tex, fragUV);\n"
                              "}";

const GLfloat vertexData[] = {
    0.5f, -0.5f, 1.0f, 1.0f,   // 右下
    0.5f, 0.5f, 1.0f, 0.0f,    // 右上
    -0.5f, -0.5f, 0.0f, 1.0f,  // 左下
    -0.5f, 0.5f, 0.0f, 0.0f    // 左上
};

const uint8_t indices[] = {0, 1, 2, 1, 2, 3};

PBOHandler::PBOHandler() : uploadBuffers(nullptr), downloadBuffers(nullptr) {
    // 初始化着色器程序
    initProgram(vsSource, fsSource);
    posAttr = glGetAttribLocation(prog, "pos");
    texAttr = glGetAttribLocation(prog, "uv");
    texUniform = glGetUniformLocation(prog, "tex");

    glGenVertexArrays(1, &vertexArray);
    glBindVertexArray(vertexArray);

    glGenBuffers(1, &vertexBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData), vertexData, GL_STATIC_DRAW);

    glVertexAttribPointer(posAttr, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (void*)0);
    glEnableVertexAttribArray(posAttr);

    glVertexAttribPointer(texAttr, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (void*)(2 * sizeof(GLfloat)));
    glEnableVertexAttribArray(texAttr);

    glGenBuffers(1, &indexBuffer);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    glBindVertexArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}

PBOHandler::~PBOHandler() {
    glDeleteBuffers(1, &indexBuffer);
    glDeleteBuffers(1, &vertexBuffer);
    glDeleteVertexArrays(1, &vertexArray);
    glDeleteTextures(1, &texID);  // 删除纹理
    if (uploadBuffers) {
        glDeleteBuffers(PBO_COUNT, uploadBuffers);
        delete[] uploadBuffers;
    }
    if (downloadBuffers) {
        glDeleteBuffers(PBO_COUNT, downloadBuffers);
        delete[] downloadBuffers;
    }
    if (prog) {
        glDeleteProgram(prog);  // 删除程序
    }
}

// 关键部分:初始化着色器程序(编译顶点和片元着色器,并链接程序)
void PBOHandler::initProgram(const char* vsSource, const char* fsSource) {
    GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vsSource, nullptr);
    glCompileShader(vertexShader);
    // 检查编译错误(实际生产中应添加日志检查)

    GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fsSource, nullptr);
    glCompileShader(fragmentShader);
    // 检查编译错误

    prog = glCreateProgram();
    glAttachShader(prog, vertexShader);
    glAttachShader(prog, fragmentShader);
    glLinkProgram(prog);
    // 检查链接错误

    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);
}

// 关键部分:初始化双 PBO 数组(用于异步上传和下载)
// 一个 PBO 让 GPU 在后台 DMA 传输
// 另一个 PBO 让 CPU 写入下一帧数据
void PBOHandler::setupPBOArray() {
    int bufferSize = texWidth * texHeight * 4;  // 计算缓冲区大小 (RGBA)
    if (!uploadBuffers) {
        uploadBuffers = new GLuint[PBO_COUNT];
        glGenBuffers(PBO_COUNT, uploadBuffers);
        for (int i = 0; i < PBO_COUNT; ++i) {
            glBindBuffer(GL_PIXEL_UNPACK_BUFFER, uploadBuffers[i]);
            glBufferData(GL_PIXEL_UNPACK_BUFFER, bufferSize, nullptr, GL_STREAM_DRAW);  // 流式绘制提示
            glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
        }
    }
    if (!downloadBuffers) {
        downloadBuffers = new GLuint[PBO_COUNT];
        glGenBuffers(PBO_COUNT, downloadBuffers);
        for (int i = 0; i < PBO_COUNT; ++i) {
            glBindBuffer(GL_PIXEL_PACK_BUFFER, downloadBuffers[i]);
            glBufferData(GL_PIXEL_PACK_BUFFER, bufferSize, nullptr, GL_STREAM_DRAW);
            glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
        }
    }
}

// 更新纹理(使用 PBO 异步上传像素数据)
// CPU → PBO(memcpy) → GPU 异步 DMA → 纹理
void PBOHandler::updateTexture(void* data, int width, int height, int len) {
    texWidth = width;
    texHeight = height;
    setupPBOArray();  // 确保 PBO 已初始化

    if (texID == 0) {
        glGenTextures(1, &texID);
        glActiveTexture(GL_TEXTURE0);
        glUniform1i(texUniform, 0);
        glBindTexture(GL_TEXTURE_2D, texID);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        // 初始化空纹理(只分配纹理的 GPU 显存空间)
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
        glGenerateMipmap(GL_TEXTURE_2D);
    }
	// 绑定纹理,准备写入
    glBindTexture(GL_TEXTURE_2D, texID);
    int dataSize = width * height * 4;
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, uploadBuffers[uploadIdx % PBO_COUNT]);  // 绑定当前 PBO
	// 映射缓冲区,把这个 GPU PBO 的显存映射成一个 CPU 可以写的指针。
    GLubyte* buf = (GLubyte*)glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, dataSize, GL_MAP_WRITE_BIT);
    if (buf) {
        memcpy(buf, data, dataSize);  // 拷贝数据到 PBO
        glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);  // 解除映射
    }
    // 从 PBO 更新纹理(异步)
    // 最后一个参数是nullptr,表示glTexSubImage2D 不再从 "data 指针" 读数据,而是从当前绑定的 PBO 读数据。
    // 小结:PBO到纹理这个过程完全走的 GPU的通过,CPU无法干预
    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
    glBindTexture(GL_TEXTURE_2D, 0);
    uploadIdx++;  // 切换下一个 PBO
}

// 关键部分:获取像素(使用 PBO 异步下载渲染结果)
void PBOHandler::fetchPixels(uint8_t** data, int* width, int* height) {
    *width = viewWidth;
    *height = viewHeight;
    int dataSize = *width * *height * 4;
    *data = new uint8_t[dataSize];

    glBindBuffer(GL_PIXEL_PACK_BUFFER, downloadBuffers[downloadIdx % PBO_COUNT]);  // 绑定当前 PBO
    glBufferData(GL_PIXEL_PACK_BUFFER, dataSize, nullptr, GL_STREAM_DRAW);  // 重新分配缓冲(如果大小变化)

    glReadPixels(0, 0, *width, *height, GL_RGBA, GL_UNSIGNED_BYTE, 0);  // 异步读取到 PBO

    GLubyte* buf = (GLubyte*)glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, dataSize, GL_MAP_READ_BIT);  // 映射缓冲区
    if (buf) {
        memcpy(*data, buf, dataSize);  // 拷贝数据到 CPU
        glUnmapBuffer(GL_PIXEL_PACK_BUFFER);  // 解除映射
    } else {
        LOGD("Pixel acquisition: Mapping failed.");
    }

    glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
    downloadIdx++;  // 切换下一个 PBO
}

// 绘制帧(渲染纹理到屏幕)
void PBOHandler::drawFrame() {
    glViewport(0, 0, viewWidth, viewHeight);  // 设置视口
    glClearColor(0.0f, 0.0f, 1.0f, 1.0f);     // 清屏颜色(蓝色)
    glClear(GL_COLOR_BUFFER_BIT);             // 清屏
    glUseProgram(prog);                       // 使用程序

    // 把纹理 texID 按照 vertexArray 指定的矩形绘制到屏幕上
    glActiveTexture(GL_TEXTURE0);
    glUniform1i(texUniform, 0);
    glBindTexture(GL_TEXTURE_2D, texID);      // 绑定纹理

    glBindVertexArray(vertexArray);           // 绑定 VAO
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, 0);  // 绘制元素

    glUseProgram(0);
    glBindVertexArray(0);
    glDisableVertexAttribArray(posAttr);      // 禁用位置属性
    glDisableVertexAttribArray(texAttr);      // 禁用纹理属性
    glBindTexture(GL_TEXTURE_2D, 0);

    swapBuffers();  // 交换缓冲
}

// 模拟交换缓冲(实际环境中调用 eglSwapBuffers(eglDisplay, eglSurface))
void PBOHandler::swapBuffers() {
    // 在完整 EGL 环境中实现:
    // eglSwapBuffers(eglDisplay, eglSurface);
    LOGD("Simulated swap buffer.");
}

小结:流程图:

CPU data
   ↓ memcpy
PBO[n] (显存)
   ↓ GPU DMA(异步)
glTexSubImage2D
   ↓
纹理 texID 更新
   ↓
drawFrame() 显示

运行演示

实际应用中,通过 updateTexture 触发上传、fetchPixels 触发下载。双 PBO 确保循环操作顺畅。

使用步骤总结

  1. 初始化 PBO:使用 glGenBuffersglBufferData 创建并分配缓冲区,指定合适的使用提示。
  2. 上传操作:绑定到 GL_PIXEL_UNPACK_BUFFER,映射/拷贝数据,然后用 glTexSubImage2D 传输到纹理。
  3. 下载操作:绑定到 GL_PIXEL_PACK_BUFFER,调用 glReadPixels 以偏移方式传输,然后映射/拷贝缓冲区数据。
  4. 双 PBO 优化:使用两个缓冲区交替操作,避免阻塞。
  5. 资源清理:在析构函数中删除缓冲区释放资源。
  6. 性能测试:在目标硬件上基准测试,如必要回退到非 PBO 方法。

vx公众号ID:gh_c297cf398802

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值