概述
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 纹理。步骤包括:
- 绑定 PBO 到
GL_PIXEL_UNPACK_BUFFER。 - 映射缓冲区并拷贝数据。
- 使用
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 下载像素
下载渲染像素的步骤:
- 绑定 PBO 到
GL_PIXEL_PACK_BUFFER。 - 使用
glReadPixels异步传输数据到 PBO。 - 映射缓冲区以访问 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 确保循环操作顺畅。
使用步骤总结
- 初始化 PBO:使用
glGenBuffers和glBufferData创建并分配缓冲区,指定合适的使用提示。 - 上传操作:绑定到
GL_PIXEL_UNPACK_BUFFER,映射/拷贝数据,然后用glTexSubImage2D传输到纹理。 - 下载操作:绑定到
GL_PIXEL_PACK_BUFFER,调用glReadPixels以偏移方式传输,然后映射/拷贝缓冲区数据。 - 双 PBO 优化:使用两个缓冲区交替操作,避免阻塞。
- 资源清理:在析构函数中删除缓冲区释放资源。
- 性能测试:在目标硬件上基准测试,如必要回退到非 PBO 方法。
vx公众号ID:gh_c297cf398802


4万+

被折叠的 条评论
为什么被折叠?



