Opengl ES之PBO

news2025/4/19 14:03:35

叨叨一句几句

关于Opengl的系列已经有较长的一段时间没有更新了,然而这个系列还远没有到完毕地步,后续至少还有关于Opengl矩阵变换、YUV与RGB互转、Opengl水印贴图、Opengl转场动画等主题文章。

断更的主要原因如果给自己找个借口的话可以说是工作比价忙,如果说的比较现实一点就是自己懒且没啥动力,毕竟写技术博客文章是一件时间成本投入很大,而收益产出极小的一件事情…

进入正题…

了解过Opengl的童鞋们都知道,在Opengl中存在这个各种O,例如VAO、VBO、FBO等,而出现各种各样的O一般都是因为考虑到性能的原因。

今天我们要介绍的主角PBO,它和之前我们介绍VBO很像,了解完PBO之后童鞋们可以对比一下PBO与VBO的差异点。

下面从两个方面介绍PBO,什么是PBO以及如何使用PBO。

更多关于音视频、FFmpeg、Opengl、C++的原创文章请关注微信公众号:思想觉悟

什么是PBO

PBO(Pixel Buffer Object像素缓冲对象)。在了解什么是PBO之前,我们先来了解一下为什么会出现PBO这么一个东西?

所谓存在即合理,用发展的眼光看问题,PBO的出现肯定是为了替代某种东西,或者是为了解决某个问题。

在使用Opengl的时候经常需要在GPU和CPU之间传递数据,例如在使用Opengl将YUV数据转换成RGB数据时就需要先将YUV数据上传到GPU,一般使用函数glTexImage2D,处理完毕后再将RGB结果数据读取到CPU,
这时使用函数glReadPixels即可将数据取回。但是这两个函数都是比较缓慢的,特别是在数据量比较大的时候。PBO就是为了解决这个访问慢的问题而产生的。

使用PBO交换数据图

PBO可以让我们通过一个内存指针,直接访问显存(GPU)的数据,我们将这块内存指针称作缓冲区。我们可以通过函数glMapBuffer得到它的内存指针,然后就对这块缓冲区的数据可以为所欲为了。

例如使用函数glReadPixels原本是要传一个内存指针进去的,但是有了缓冲区,它就可以把数据直接复制到缓冲区中去,而不是复制到内存条中去,这样就大大提高了数据的传递效率。

PBO的主要优点是通过直接内存访问的方式与显存进行快速像素数据传输,而无需占用CPU周期。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RtbEyIH4-1669172615917)(https://flyer-blog.oss-cn-shenzhen.aliyuncs.com/PBO%E6%95%B0%E6%8D%AE%E4%BA%A4%E6%8D%A2.png)]

可能看到这张图你没什么感觉,但是对比看看下面这张CPU与GPU直接传递数据的图你就会有所发现了:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KKZ6RDsF-1669172615917)(https://flyer-blog.oss-cn-shenzhen.aliyuncs.com/gpu%E5%B8%B8%E8%A7%84%E6%95%B0%E6%8D%AE%E4%BA%A4%E6%8D%A2.png)]

注意:PBO是OpenGL ES 3.0开始提供的一种方式,主要应用于从内存快速复制纹理到显存,或从显存复制像素数据到内存。

PBO的使用方式

既然PBO这么有效率,那么我们在什么情况下可能会用到PBO呢?有个常见的例子,例如我们在安卓上开发Camera应用录制视频时,如果需要用到x264进行软编码的话可能就会用到PBO,
首先我们将相机纹理图像送到Surface渲染显示,然后将Surface数据使用PBO的方式读取处理送到X264编码器中进行编码,当然在安卓上你也可以使用ImageReader…

下面我们来介绍下PBO的使用方式。

PBO的创建和初始化类似于VBO,但是在使用的时候需要用到GL_PIXEL_UNPACK_BUFFER GL_PIXEL_PACK_BUFFER 这两个令牌,其中GL_PIXEL_UNPACK_BUFFER绑定表示该PBO用于将像素数据从程序(CPU)传送到OpenGL中;绑定为GL_PIXEL_PACK_BUFFER表示该PBO用于从OpenGL中读回像素数据。

  1. Pbo创建

先上代码,跟着注释看

void PBOOpengl::initPbo() {

    int imgByteSize = imageWidth * imageHeight * 4; // RGBA

    glGenBuffers(1, &uploadPboId);
    // 绑定pbo
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, uploadPboId);
    // 设置pbo内存大小
    // 这一步十分重要,第2个参数指定了这个缓冲区的大小,单位是字节,一定要注意
    //  然后第3个参数是初始化用的数据,如果你传个内存指针进去,这个函数就会把你的
    //  数据复制到缓冲区里,我们这里一开始并不需要什么数据,所以传个nullptr就行了
    glBufferData(GL_PIXEL_UNPACK_BUFFER, imgByteSize, nullptr, GL_STREAM_DRAW);
    // 解除绑定
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);

    glGenBuffers(1, &downloadPboId);
    glBindBuffer(GL_PIXEL_PACK_BUFFER, downloadPboId);
    glBufferData(GL_PIXEL_PACK_BUFFER, imgByteSize, nullptr, GL_STREAM_DRAW);
    // 解除绑定
    glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
    LOGD("uploadPboId:%d---downloadPboId:%d---imgByteSize:%d", uploadPboId, downloadPboId,
         imgByteSize);
}

上面的代码创建了两个PBO,其中uploadPboId用于纹理上传,downloadPboId用于纹理下载。创建好PBO之后然后使用两个PBO专用的令牌进行绑定,之后就调用glBufferData给PBO分配缓冲区,当然,你也可以在使用的时候先进行绑定,然后重新调用glBufferData分配新的缓冲区。

  1. Pbo上传纹理

所谓上传纹理是值将纹理数据从CPU传递到OpenGL,使用Pbo上传纹理时需要先使用令牌GL_PIXEL_UNPACK_BUFFER绑定对应的PBO,然后才行使用PBO的缓冲区:

// 单个PBO测试
void PBOOpengl::setPixel(void *data, int width, int height, int length) {
    LOGD("texture setPixel");
    imageWidth = width;
    imageHeight = height;
    // Pbo初始化
    initPbo();

    glGenTextures(1, &imageTextureId);

    // 激活纹理,注意以下这个两句是搭配的,glActiveTexture激活的是那个纹理,就设置的sampler2D是那个
    // 默认是0,如果不是0的话,需要在onDraw的时候重新激活一下?
//    glActiveTexture(GL_TEXTURE0);
//    glUniform1i(textureSampler, 0);

// 例如,一样的
    glActiveTexture(GL_TEXTURE2);
    glUniform1i(textureSampler, 2);
    
    // 本文首发于微信公总号号:思想觉悟
    // 更多关于音视频、FFmpeg、Opengl、C++的原创文章请关注微信公众号:思想觉悟

    // 绑定纹理
    glBindTexture(GL_TEXTURE_2D, imageTextureId);
    // 为当前绑定的纹理对象设置环绕、过滤方式
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    // pixels参数传递空,后面会通过pbo更新纹理数据
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);

    // 生成mip贴图
    glGenerateMipmap(GL_TEXTURE_2D);

    int dataSize = width * height * 4;
    // 使用Pbo
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, uploadPboId);
    // 将纹理数据拷贝进入缓冲区
    GLubyte *bufPtr = (GLubyte *) glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0,
                                                   dataSize,
                                                   GL_MAP_WRITE_BIT);
    if (bufPtr) {
        memcpy(bufPtr, data, static_cast<size_t>(dataSize));
        glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
    }
    // 将pbo缓冲区中的数据拷贝到纹理,调用 glTexSubImage2D 后立即返回,不影响 CPU 时钟周期
    // 这个函数会判断 GL_PIXEL_UNPACK_BUFFER 这个地方有没有绑定一个缓冲区
    //   如果有,就从这个缓冲区读取数据,而不是data参数指定的那个内存
    // 这样glTexSubImage2D就会从我们的缓冲区中读取数据了
    // 这里为什么要用glTexSubImage2D呢,因为如果用glTexImage2D,glTexImage2D会销毁纹理内存重新申请,glTexSubImage2D就仅仅只是更新纹理中的数据,这就提高了速度,并且优化了显存的利用率
    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
    // Pbo解除
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
    // 解绑定
    glBindTexture(GL_TEXTURE_2D, 0);
}

注释已经很详细了,就不多解析了,还是看不懂的私聊交流呗…

  1. Pbo下载纹理

所谓上传纹理是值将纹理数据从OpenGL中读取回CPU,与上传纹理一样,下载纹理也是需要先使用令牌绑定PBO才能使用,下载纹理使用的令牌是GL_PIXEL_PACK_BUFFER
下面的代码作用是将Opengl的渲染结果使用PBO读取出来:

// 单PBO读取测试
void PBOOpengl::readPixel(uint8_t **data,int *width,int *height) {

    *width = eglHelper->viewWidth;
    *height = eglHelper->viewHeight;
    int dataSize = eglHelper->viewWidth * eglHelper->viewHeight * 4;
    *data = new uint8_t[dataSize];
    // 方法一 正常的glReadPixels读取 最简单
//    glReadPixels(0, 0, eglHelper->viewWidth, eglHelper->viewHeight,
//                 GL_RGBA, GL_UNSIGNED_BYTE, *data);

    // 方法二 pbo读取
    // 首先我们要把缓冲区绑定到 GL_PIXEL_PACK_BUFFER 这个地方
    glBindBuffer(GL_PIXEL_PACK_BUFFER, downloadPboId);
    // 重新分配一下空间 如果必要,这里就懒得判断了
    glBufferData(GL_PIXEL_PACK_BUFFER, dataSize, nullptr, GL_STREAM_DRAW);
    // 这个函数会判断 GL_PIXEL_PACK_BUFFER 这个地方有没有绑定一个缓冲区,如果有,那就把数据写入到这个缓冲区里
    // 前4个参数就是要读取的屏幕区域,不多解释
    //  格式是RGB,类型是BYTE,每个像素1字节
    // 如果GL_PIXEL_PACK_BUFFER有绑定缓冲区,最后一个参数就作为偏移值来使用,传nullptr就行
    glReadPixels(0, 0, *width, *height, GL_RGBA, GL_UNSIGNED_BYTE, 0);
    // 好了我们已经成功把屏幕的像素数据复制到了缓冲区里

    // 这时候,你可以用 glMapBuffer 得到缓冲区的内存指针,来读取里面的像素数据,保存到图片文件
    // 注意glMapBuffer的第1个参数不一定要是GL_PIXEL_PACK_BUFFER,你可以把缓冲区绑定到比如上面init函数的GL_ARRAY_BUFFER
    //  然后这里也传GL_ARRAY_BUFFER,由于懒得再绑定一次,就接着用上面绑定的GL_PIXEL_PACK_BUFFER吧
    GLubyte *mapPtr = static_cast<GLubyte *>(glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, dataSize,
                                                              GL_MAP_READ_BIT));
    if (nullptr != mapPtr)
    {
        LOGD("readPixel 数据拷贝");
        // 拷贝数据
        memcpy(*data,mapPtr,dataSize);
        // 解除Map
        glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
    } else{
        LOGD("readPixel glMapBufferRange null");
    }
    // 完事了把GL_PIXEL_PACK_BUFFER这个地方的缓冲区解绑掉,以免别的函数误操作
    glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
}

其实无论有没有使用PBO都是使用的函数glReadPixels进行读取,但是他们的参数却是不一样的,你发现细节了吗?如果是在PBO模式下使用glReadPixels,则会自动将结果发送到PBO的缓冲区,
并不会像单独使用glReadPixels那样需要阻塞进行等待返回,因此效率就高了…

双PBO

PBO的另一个优势是它还具备异步DMA(Direct Memory Access)传输,也正因为这个特性,使得在使用单个PBO的情况下,在一些机型上可能性能提升并不明显,所以通常需要两个PBO配合使用。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-88h9w1Pm-1669172615917)(https://flyer-blog.oss-cn-shenzhen.aliyuncs.com/%E5%8F%8CPBO%E4%B8%8A%E4%BC%A0%E7%BA%B9%E7%90%86%E6%95%B0%E6%8D%AE.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9r3meRB2-1669172615917)(https://flyer-blog.oss-cn-shenzhen.aliyuncs.com/%E5%8F%8CPBO%E8%8E%B7%E5%8F%96%E7%BA%B9%E7%90%86%E6%95%B0%E6%8D%AE.png)]

通过上面这两张图我们可以看到利用交替使用双PBO的方式可以将PBO的快速特性更进一步。

下面的代码展示了如何通过双PBO上传纹理和下载渲染结果:

使用双PBO上传纹理数据

// 双PBO测试
void PBOOpengl::setPixel(void *data, int width, int height, int length) {
    LOGD("texture setPixel  uploadPboIndex:%d",uploadPboIndex);
    imageWidth = width;
    imageHeight = height;
    // Pbo初始化
    initPboArray();

    if(imageTextureId == 0){

        glGenTextures(1, &imageTextureId);

        // 激活纹理,注意以下这个两句是搭配的,glActiveTexture激活的是那个纹理,就设置的sampler2D是那个
        // 默认是0,如果不是0的话,需要在onDraw的时候重新激活一下?
//    glActiveTexture(GL_TEXTURE0);
//    glUniform1i(textureSampler, 0);

// 例如,一样的
        glActiveTexture(GL_TEXTURE2);
        glUniform1i(textureSampler, 2);

        // 绑定纹理
        glBindTexture(GL_TEXTURE_2D, imageTextureId);
        // 为当前绑定的纹理对象设置环绕、过滤方式
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        // pixels参数传递空,后面会通过pbo更新纹理数据
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);

        // 生成mip贴图
        glGenerateMipmap(GL_TEXTURE_2D);
    }

    // 绑定纹理
    glBindTexture(GL_TEXTURE_2D, imageTextureId);

    int dataSize = width * height * 4;
    // 使用Pbo
    // 指针运算你要会一点呀
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, *(uploadPboIds + uploadPboIndex % NUM_PBO ));
    // 将纹理数据拷贝进入缓冲区
    GLubyte *bufPtr = (GLubyte *) glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0,
                                                   dataSize,
                                                   GL_MAP_WRITE_BIT);
    if (bufPtr) {
        memcpy(bufPtr, data, static_cast<size_t>(dataSize));
        glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
    }
    // 将pbo缓冲区中的数据拷贝到纹理,调用 glTexSubImage2D 后立即返回,不影响 CPU 时钟周期
    // 这个函数会判断 GL_PIXEL_UNPACK_BUFFER 这个地方有没有绑定一个缓冲区
    //   如果有,就从这个缓冲区读取数据,而不是data参数指定的那个内存
    // 这样glTexSubImage2D就会从我们的缓冲区中读取数据了
    // 这里为什么要用glTexSubImage2D呢,因为如果用glTexImage2D,glTexImage2D会销毁纹理内存重新申请,glTexSubImage2D就仅仅只是更新纹理中的数据,这就提高了速度,并且优化了显存的利用率
    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
    // Pbo解除
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
    // 解绑定
    glBindTexture(GL_TEXTURE_2D, 0);
    // 索引自加
    uploadPboIndex++;
}

使用双PBO下载渲染结果:


// 双PBO读取测试
void PBOOpengl::readPixel(uint8_t **data,int *width,int *height) {

    *width = eglHelper->viewWidth;
    *height = eglHelper->viewHeight;
    int dataSize = eglHelper->viewWidth * eglHelper->viewHeight * 4;
    *data = new uint8_t[dataSize];

    // 首先我们要把缓冲区绑定到 GL_PIXEL_PACK_BUFFER 这个地方
    // 指针运算你要会一点呀
    glBindBuffer(GL_PIXEL_PACK_BUFFER, *(downloadPboIds + downloadPboIndex % NUM_PBO));
    // 重新分配一下空间
    glBufferData(GL_PIXEL_PACK_BUFFER, dataSize, nullptr, GL_STREAM_DRAW);
    // 这个函数会判断 GL_PIXEL_PACK_BUFFER 这个地方有没有绑定一个缓冲区,如果有,那就把数据写入到这个缓冲区里
    // 前4个参数就是要读取的屏幕区域,不多解释
    //  格式是RGB,类型是BYTE,每个像素1字节
    // 如果GL_PIXEL_PACK_BUFFER有绑定缓冲区,最后一个参数就作为偏移值来使用,传nullptr就行
    glReadPixels(0, 0, *width, *height, GL_RGBA, GL_UNSIGNED_BYTE, 0);
    // 好了我们已经成功把屏幕的像素数据复制到了缓冲区里

    // 这时候,你可以用 glMapBuffer 得到缓冲区的内存指针,来读取里面的像素数据,保存到图片文件
    // 注意glMapBuffer的第1个参数不一定要是GL_PIXEL_PACK_BUFFER,你可以把缓冲区绑定到比如上面init函数的GL_ARRAY_BUFFER
    //  然后这里也传GL_ARRAY_BUFFER,由于懒得再绑定一次,就接着用上面绑定的GL_PIXEL_PACK_BUFFER吧
    GLubyte *mapPtr = static_cast<GLubyte *>(glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, dataSize,
                                                              GL_MAP_READ_BIT));
    if (nullptr != mapPtr)
    {
        LOGD("readPixel 数据拷贝");
        // 拷贝数据
        memcpy(*data,mapPtr,dataSize);
        // 解除Map
        glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
    } else{
        LOGD("readPixel glMapBufferRange null");
    }
    // 完事了把GL_PIXEL_PACK_BUFFER这个地方的缓冲区解绑掉,以免别的函数误操作
    glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
    // Pbo索引自加
    downloadPboIndex++;
}

下面我们使用双PBO上传纹理和双PBO下载实现一个纹理动态切换和下载渲染结果的小demo,完整代码如下:

PBOOpengl.h

#ifndef NDK_OPENGLES_LEARN_PBOOPENGL_H
#define NDK_OPENGLES_LEARN_PBOOPENGL_H

#include "BaseOpengl.h"

static const int NUM_PBO = 2;

class PBOOpengl: public BaseOpengl{

public:
    PBOOpengl();
    virtual ~PBOOpengl();
    // override要么就都写,要么就都不写,不要一个虚函数写override,而另外一个虚函数不写override,不然可能编译不过
    virtual void onDraw() override;
    virtual void setPixel(void *data, int width, int height, int length) override;
    virtual void readPixel(uint8_t **data,int *width,int *height) override;
private:
    void initPbo();
    void initPboArray();
    GLint positionHandle{-1};
    GLint textureHandle{-1};
    GLuint vbo{0};
    GLuint vao{0};
    GLuint ebo{0};
    // 本身图像纹理id
    GLuint imageTextureId{0};
    GLint textureSampler{-1};
    int imageWidth{0};
    int imageHeight{0};
    // 上传纹理的pbo
    GLuint uploadPboId{0};
    // cpu下载纹理的pbo
    GLuint downloadPboId{0};

    // 上传纹理的pbo
    GLuint *uploadPboIds{nullptr};
    // cpu下载纹理的pbo
    GLuint *downloadPboIds{nullptr};

    // Pbo 的索引,用于双PBO时
    int uploadPboIndex{0};
    int downloadPboIndex{0};

  // 本文首发于微信公总号号:思想觉悟
    // 更多关于音视频、FFmpeg、Opengl、C++的原创文章请关注微信公众号:思想觉悟
};


#endif //NDK_OPENGLES_LEARN_PBOOPENGL_H

PBOOpengl.cpp:

#include "PBOOpengl.h"

#include "../utils/Log.h"

// 顶点着色器
static const char *ver = "#version 300 es\n"
                         "in vec4 aPosition;\n"
                         "in vec2 aTexCoord;\n"
                         "out vec2 TexCoord;\n"
                         "void main() {\n"
                         "  TexCoord = aTexCoord;\n"
                         "  gl_Position = aPosition;\n"
                         "}";

// 片元着色器
static const char *fragment = "#version 300 es\n"
                              "precision mediump float;\n"
                              "out vec4 FragColor;\n"
                              "in vec2 TexCoord;\n"
                              "uniform sampler2D ourTexture;\n"
                              "void main()\n"
                              "{\n"
                              "    FragColor = texture(ourTexture, TexCoord);\n"
                              "}";

const static GLfloat VERTICES_AND_TEXTURE[] = {
        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 static GLfloat FBO_VERTICES_AND_TEXTURE[] = {
        1.0f, -1.0f, // 右下
        // 纹理坐标
        1.0f, 0.0f,
        1.0f, 1.0f, // 右上
        // 纹理坐标
        1.0f, 1.0f,
        -1.0f, -1.0f, // 左下
        // 纹理坐标
        0.0f, 0.0f,
        -1.0f, 1.0f, // 左上
        // 纹理坐标
        0.0f, 1.0f
};

// 使用byte类型比使用short或者int类型节约内存
const static uint8_t indices[] = {
        // 注意索引从0开始!
        // 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
        // 这样可以由下标代表顶点组合成矩形
        0, 1, 2, // 第一个三角形
        1, 2, 3  // 第二个三角形
};

PBOOpengl::PBOOpengl():uploadPboIds(nullptr),downloadPboIds(nullptr) {
    initGlProgram(ver, fragment);
    positionHandle = glGetAttribLocation(program, "aPosition");
    textureHandle = glGetAttribLocation(program, "aTexCoord");
    textureSampler = glGetUniformLocation(program, "ourTexture");
    LOGD("program:%d", program);
    LOGD("positionHandle:%d", positionHandle);
    LOGD("textureHandle:%d", textureHandle);
    LOGD("textureSample:%d", textureSampler);
    // VAO
    glGenVertexArrays(1, &vao);
    glBindVertexArray(vao);

    // vbo
    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(VERTICES_AND_TEXTURE), VERTICES_AND_TEXTURE,
                 GL_STATIC_DRAW);

    // stride 步长 每个顶点坐标之间相隔4个数据点,数据类型是float
    glVertexAttribPointer(positionHandle, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *) 0);
    // 启用顶点数据
    glEnableVertexAttribArray(positionHandle);
    // stride 步长 每个颜色坐标之间相隔4个数据点,数据类型是float,颜色坐标索引从2开始
    glVertexAttribPointer(textureHandle, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float),
                          (void *) (2 * sizeof(float)));
    // 启用纹理坐标数组
    glEnableVertexAttribArray(textureHandle);

    // EBO
    glGenBuffers(1, &ebo);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    // 这个顺序不能乱啊,先解除vao,再解除其他的,不然在绘制的时候可能会不起作用,需要重新glBindBuffer才生效
    // vao解除
    glBindVertexArray(0);
    // 解除绑定
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    // 解除绑定
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

    LOGD("program:%d", program);
    LOGD("positionHandle:%d", positionHandle);
    LOGD("colorHandle:%d", textureHandle);
}

void PBOOpengl::initPbo() {

    int imgByteSize = imageWidth * imageHeight * 4; // RGBA

    glGenBuffers(1, &uploadPboId);
    // 绑定pbo
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, uploadPboId);
    // 设置pbo内存大小
    // 这一步十分重要,第2个参数指定了这个缓冲区的大小,单位是字节,一定要注意
    //  然后第3个参数是初始化用的数据,如果你传个内存指针进去,这个函数就会把你的
    //  数据复制到缓冲区里,我们这里一开始并不需要什么数据,所以传个nullptr就行了
    glBufferData(GL_PIXEL_UNPACK_BUFFER, imgByteSize, nullptr, GL_STREAM_DRAW);
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
  // 本文首发于微信公总号号:思想觉悟
    // 更多关于音视频、FFmpeg、Opengl、C++的原创文章请关注微信公众号:思想觉悟
    glGenBuffers(1, &downloadPboId);
    glBindBuffer(GL_PIXEL_PACK_BUFFER, downloadPboId);
    glBufferData(GL_PIXEL_PACK_BUFFER, imgByteSize, nullptr, GL_STREAM_DRAW);
    // 解除绑定
    glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
    LOGD("uploadPboId:%d---downloadPboId:%d---imgByteSize:%d", uploadPboId, downloadPboId,
         imgByteSize);
}

void PBOOpengl::initPboArray() {
    int imgByteSize = imageWidth * imageHeight * 4; // RGBA
    if(nullptr == uploadPboIds){
        uploadPboIds = new GLuint[NUM_PBO];
        glGenBuffers(NUM_PBO, uploadPboIds);
        for (int i = 0; i < NUM_PBO; ++i) {
            // 绑定pbo
            glBindBuffer(GL_PIXEL_UNPACK_BUFFER, *(uploadPboIds + i));
            // 设置pbo内存大小
            // 这一步十分重要,第2个参数指定了这个缓冲区的大小,单位是字节,一定要注意
            //  然后第3个参数是初始化用的数据,如果你传个内存指针进去,这个函数就会把你的
            //  数据复制到缓冲区里,我们这里一开始并不需要什么数据,所以传个nullptr就行了
            glBufferData(GL_PIXEL_UNPACK_BUFFER, imgByteSize, nullptr, GL_STREAM_DRAW);
            glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
        }
    }

    if(nullptr == downloadPboIds){
        downloadPboIds = new GLuint[NUM_PBO];
        glGenBuffers(NUM_PBO, downloadPboIds);
        for (int i = 0; i < NUM_PBO; ++i) {
            // 绑定pbo
            glBindBuffer(GL_PIXEL_PACK_BUFFER, *(downloadPboIds + i));
            glBufferData(GL_PIXEL_PACK_BUFFER, imgByteSize, nullptr, GL_STREAM_DRAW);
            glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
        }
    }

}

PBOOpengl::~PBOOpengl() {
    glDeleteBuffers(1, &ebo);
    glDeleteBuffers(1, &vbo);
    glDeleteVertexArrays(1, &vao);
    if(nullptr != uploadPboIds){
        glDeleteBuffers(NUM_PBO,uploadPboIds);
        delete [] uploadPboIds;
        uploadPboIds = nullptr;
    }
  // 本文首发于微信公总号号:思想觉悟
    // 更多关于音视频、FFmpeg、Opengl、C++的原创文章请关注微信公众号:思想觉悟
    if(nullptr != downloadPboIds){
        glDeleteBuffers(NUM_PBO,downloadPboIds);
        delete [] downloadPboIds;
        downloadPboIds = nullptr;
    }
}

void PBOOpengl::onDraw() {

    // 绘制屏幕宽高
    glViewport(0, 0, eglHelper->viewWidth, eglHelper->viewHeight);

    // 绘制到屏幕
    // 清屏
    glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    glUseProgram(program);

    // 激活纹理
    glActiveTexture(GL_TEXTURE2);
    glUniform1i(textureSampler, 2);

    // 绑定纹理
    glBindTexture(GL_TEXTURE_2D, imageTextureId);

    // VBO与VAO配合绘制
    // 使用vao
    glBindVertexArray(vao);
    // 使用EBO
// 使用byte类型节省内存
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, (void *) 0);
    glUseProgram(0);
    // vao解除绑定
    glBindVertexArray(0);

    // 禁用顶点
    glDisableVertexAttribArray(positionHandle);
    if (nullptr != eglHelper) {
        eglHelper->swapBuffers();
    }

    glBindTexture(GL_TEXTURE_2D, 0);
}
//
 单个PBO测试
//void PBOOpengl::setPixel(void *data, int width, int height, int length) {
//    LOGD("texture setPixel");
//    imageWidth = width;
//    imageHeight = height;
//    // Pbo初始化
//    initPbo();
//
//    glGenTextures(1, &imageTextureId);
//
//    // 激活纹理,注意以下这个两句是搭配的,glActiveTexture激活的是那个纹理,就设置的sampler2D是那个
//    // 默认是0,如果不是0的话,需要在onDraw的时候重新激活一下?
    glActiveTexture(GL_TEXTURE0);
    glUniform1i(textureSampler, 0);
//
 例如,一样的
//    glActiveTexture(GL_TEXTURE2);
//    glUniform1i(textureSampler, 2);
//
//    // 绑定纹理
//    glBindTexture(GL_TEXTURE_2D, imageTextureId);
//    // 为当前绑定的纹理对象设置环绕、过滤方式
//    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
//    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
//    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
//    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//    // pixels参数传递空,后面会通过pbo更新纹理数据
//    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
//
//    // 生成mip贴图
//    glGenerateMipmap(GL_TEXTURE_2D);
//
//    int dataSize = width * height * 4;
//    // 使用Pbo
//    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, uploadPboId);
//    // 将纹理数据拷贝进入缓冲区
//    GLubyte *bufPtr = (GLubyte *) glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0,
//                                                   dataSize,
//                                                   GL_MAP_WRITE_BIT);
//    if (bufPtr) {
//        memcpy(bufPtr, data, static_cast<size_t>(dataSize));
//        glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
//    }
//    // 将pbo缓冲区中的数据拷贝到纹理,调用 glTexSubImage2D 后立即返回,不影响 CPU 时钟周期
//    // 这个函数会判断 GL_PIXEL_UNPACK_BUFFER 这个地方有没有绑定一个缓冲区
//    //   如果有,就从这个缓冲区读取数据,而不是data参数指定的那个内存
//    // 这样glTexSubImage2D就会从我们的缓冲区中读取数据了
//    // 这里为什么要用glTexSubImage2D呢,因为如果用glTexImage2D,glTexImage2D会销毁纹理内存重新申请,glTexSubImage2D就仅仅只是更新纹理中的数据,这就提高了速度,并且优化了显存的利用率
//    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
//    // Pbo解除
//    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
//    // 解绑定
//    glBindTexture(GL_TEXTURE_2D, 0);
//}



// 双PBO测试
void PBOOpengl::setPixel(void *data, int width, int height, int length) {
    LOGD("texture setPixel  uploadPboIndex:%d",uploadPboIndex);
    imageWidth = width;
    imageHeight = height;
    // Pbo初始化
    initPboArray();

    if(imageTextureId == 0){

        glGenTextures(1, &imageTextureId);

        // 激活纹理,注意以下这个两句是搭配的,glActiveTexture激活的是那个纹理,就设置的sampler2D是那个
        // 默认是0,如果不是0的话,需要在onDraw的时候重新激活一下?
//    glActiveTexture(GL_TEXTURE0);
//    glUniform1i(textureSampler, 0);

// 例如,一样的
        glActiveTexture(GL_TEXTURE2);
        glUniform1i(textureSampler, 2);

        // 绑定纹理
        glBindTexture(GL_TEXTURE_2D, imageTextureId);
        // 为当前绑定的纹理对象设置环绕、过滤方式
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        // pixels参数传递空,后面会通过pbo更新纹理数据
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);

        // 生成mip贴图
        glGenerateMipmap(GL_TEXTURE_2D);
    }

    // 绑定纹理
    glBindTexture(GL_TEXTURE_2D, imageTextureId);

    int dataSize = width * height * 4;
    // 使用Pbo
    // 指针运算你要会一点呀
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, *(uploadPboIds + uploadPboIndex % NUM_PBO ));
    // 将纹理数据拷贝进入缓冲区
    GLubyte *bufPtr = (GLubyte *) glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0,
                                                   dataSize,
                                                   GL_MAP_WRITE_BIT);
    if (bufPtr) {
        memcpy(bufPtr, data, static_cast<size_t>(dataSize));
        glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
    }
    // 将pbo缓冲区中的数据拷贝到纹理,调用 glTexSubImage2D 后立即返回,不影响 CPU 时钟周期
    // 这个函数会判断 GL_PIXEL_UNPACK_BUFFER 这个地方有没有绑定一个缓冲区
    //   如果有,就从这个缓冲区读取数据,而不是data参数指定的那个内存
    // 这样glTexSubImage2D就会从我们的缓冲区中读取数据了
    // 这里为什么要用glTexSubImage2D呢,因为如果用glTexImage2D,glTexImage2D会销毁纹理内存重新申请,glTexSubImage2D就仅仅只是更新纹理中的数据,这就提高了速度,并且优化了显存的利用率
    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
    // Pbo解除
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
    // 解绑定
    glBindTexture(GL_TEXTURE_2D, 0);
    // 索引自加
    uploadPboIndex++;
}

 单PBO读取测试
//void PBOOpengl::readPixel(uint8_t **data,int *width,int *height) {
//
//    *width = eglHelper->viewWidth;
//    *height = eglHelper->viewHeight;
//    int dataSize = eglHelper->viewWidth * eglHelper->viewHeight * 4;
//    *data = new uint8_t[dataSize];
//    // 方法一 正常的glReadPixels读取 最简单
    glReadPixels(0, 0, eglHelper->viewWidth, eglHelper->viewHeight,
                 GL_RGBA, GL_UNSIGNED_BYTE, *data);
//
//    // 方法二 pbo读取
//    // 首先我们要把缓冲区绑定到 GL_PIXEL_PACK_BUFFER 这个地方
//    glBindBuffer(GL_PIXEL_PACK_BUFFER, downloadPboId);
//    // 重新分配一下空间
//    glBufferData(GL_PIXEL_PACK_BUFFER, dataSize, nullptr, GL_STREAM_DRAW);
//    // 这个函数会判断 GL_PIXEL_PACK_BUFFER 这个地方有没有绑定一个缓冲区,如果有,那就把数据写入到这个缓冲区里
//    // 前4个参数就是要读取的屏幕区域,不多解释
//    //  格式是RGB,类型是BYTE,每个像素1字节
//    // 如果GL_PIXEL_PACK_BUFFER有绑定缓冲区,最后一个参数就作为偏移值来使用,传nullptr就行
//    glReadPixels(0, 0, *width, *height, GL_RGBA, GL_UNSIGNED_BYTE, 0);
//    // 好了我们已经成功把屏幕的像素数据复制到了缓冲区里
//
//    // 这时候,你可以用 glMapBuffer 得到缓冲区的内存指针,来读取里面的像素数据,保存到图片文件
//    // 注意glMapBuffer的第1个参数不一定要是GL_PIXEL_PACK_BUFFER,你可以把缓冲区绑定到比如上面init函数的GL_ARRAY_BUFFER
//    //  然后这里也传GL_ARRAY_BUFFER,由于懒得再绑定一次,就接着用上面绑定的GL_PIXEL_PACK_BUFFER吧
//    GLubyte *mapPtr = static_cast<GLubyte *>(glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, dataSize,
//                                                              GL_MAP_READ_BIT));
//    if (nullptr != mapPtr)
//    {
//        LOGD("readPixel 数据拷贝");
//        // 拷贝数据
//        memcpy(*data,mapPtr,dataSize);
//        // 解除Map
//        glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
//    } else{
//        LOGD("readPixel glMapBufferRange null");
//    }
//    // 完事了把GL_PIXEL_PACK_BUFFER这个地方的缓冲区解绑掉,以免别的函数误操作
//    glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
//}

// 双PBO读取测试
void PBOOpengl::readPixel(uint8_t **data,int *width,int *height) {

    *width = eglHelper->viewWidth;
    *height = eglHelper->viewHeight;
    int dataSize = eglHelper->viewWidth * eglHelper->viewHeight * 4;
    *data = new uint8_t[dataSize];

    // 首先我们要把缓冲区绑定到 GL_PIXEL_PACK_BUFFER 这个地方
    // 指针运算你要会一点呀
    glBindBuffer(GL_PIXEL_PACK_BUFFER, *(downloadPboIds + downloadPboIndex % NUM_PBO));
    // 重新分配一下空间
    glBufferData(GL_PIXEL_PACK_BUFFER, dataSize, nullptr, GL_STREAM_DRAW);
    // 这个函数会判断 GL_PIXEL_PACK_BUFFER 这个地方有没有绑定一个缓冲区,如果有,那就把数据写入到这个缓冲区里
    // 前4个参数就是要读取的屏幕区域,不多解释
    //  格式是RGB,类型是BYTE,每个像素1字节
    // 如果GL_PIXEL_PACK_BUFFER有绑定缓冲区,最后一个参数就作为偏移值来使用,传nullptr就行
    glReadPixels(0, 0, *width, *height, GL_RGBA, GL_UNSIGNED_BYTE, 0);
    // 好了我们已经成功把屏幕的像素数据复制到了缓冲区里

    // 这时候,你可以用 glMapBuffer 得到缓冲区的内存指针,来读取里面的像素数据,保存到图片文件
    // 注意glMapBuffer的第1个参数不一定要是GL_PIXEL_PACK_BUFFER,你可以把缓冲区绑定到比如上面init函数的GL_ARRAY_BUFFER
    //  然后这里也传GL_ARRAY_BUFFER,由于懒得再绑定一次,就接着用上面绑定的GL_PIXEL_PACK_BUFFER吧
    GLubyte *mapPtr = static_cast<GLubyte *>(glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, dataSize,
                                                              GL_MAP_READ_BIT));
    if (nullptr != mapPtr)
    {
        LOGD("readPixel 数据拷贝");
        // 拷贝数据
        memcpy(*data,mapPtr,dataSize);
        // 解除Map
        glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
    } else{
        LOGD("readPixel glMapBufferRange null");
    }
    // 完事了把GL_PIXEL_PACK_BUFFER这个地方的缓冲区解绑掉,以免别的函数误操作
    glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
    // Pbo索引自加
    downloadPboIndex++;
}

运行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GH7I1AsB-1669172615918)(https://flyer-blog.oss-cn-shenzhen.aliyuncs.com/%E5%8F%8Cpbo%E8%BF%90%E8%A1%8C%E7%BB%93%E6%9E%9C.gif)]

通过点击按钮可以实现使用双PBO的方式下载纹理数据。

关于PBO的坑

以下是笔者在网上看到某博主总结的关于PBO的坑,由于设备有限,笔者也没有进行过严格的对比测试,这里引用原文仅供记录参考:

然而PBO还有一个非常坑的地方,经测试表明,在部分硬件上glMapBufferRange映射出来的Buffer拷贝极为耗时,可以高达30+ms,这对于音视频处理显然是不能接受的。通常,映射出来的是一个DirectByteBuffer,也是一个堆外内存(C内存),这部分内存本身只能通过Buffer.get(byte[])拷贝来拿到数据,但正常情况下只需要2-3ms。出现这种问题估计是硬件上留下的坑。
所以,在Android上使用PBO是有比较多的兼容性问题的,包括上面说的。正确使用PBO的方式是,首先判断是否支持PBO,如果支持,则还是先使用glReadPixels进行读取测试,记录平均耗时,然后再使用PBO进行读取测试,记录平均耗时,最后对比两个方式的耗时,选择最快的一个。这样动态处理是比较复杂的,然而在这种情况下你不得不这样做。那么有没有一种既简单又高效的方式呢?

参考

http://www.songho.ca/opengl/gl_pbo.html

往期笔记

Opengl ES之EGL环境搭建
Opengl ES之着色器
Opengl ES之三角形绘制
Opengl ES之四边形绘制
Opengl ES之纹理贴图
Opengl ES之VBO和VAO
Opengl ES之EBO
Opengl ES之FBO

关注我,一起进步,人生不止coding!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/28967.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

1100w播放、45w涨粉!黑马UP在B站20天逆袭登顶!

在B站生活区里&#xff0c;“搞笑区”的流量独占鳌头&#xff0c;创作者也是络绎不绝。 为此&#xff0c;B站官方对搞笑内容创作布局了长期的扶持计划&#xff0c;比较热门的两个活动“搞笑新星训练营”、“搞笑研究所”是搞笑内容投稿最多的&#xff0c;至今两个活动已经分别…

动手实践丨基于ModelAtrs使用A2C算法制作登月器着陆小游戏

摘要&#xff1a;在本案例中&#xff0c;我们将展示如何基于A2C算法&#xff0c;训练一个LunarLander小游戏。本文分享自华为云社区《使用A2C算法控制登月器着陆》&#xff0c;作者&#xff1a;HWCloudAI 。 LunarLander是一款控制类的小游戏&#xff0c;也是强化学习中常用的…

C# dll代码混淆加密

目录 一、需求 二、用法 1.新建C#项目 2.开始加密 3.常见的错误 4.添加加密规则 5.导出加密dll 6.调用加密dll 结束 一、需求 C# 项目生成 dll&#xff0c;在反编译工具下&#xff0c;好比皇帝的新装&#xff0c;dll 内部的代码看的一清二楚&#xff0c;在这里推荐一…

关于Cy5.5 alkyne,Cyanine7 alkyne和1628790-37-3,1998119-13-3两者的区别

外观以及性质&#xff1a; Cy5.5 alkyne和Cy7alkyne 都含有荧光基团&#xff0c;其中氰基7的炔烃衍生物&#xff0c;近红外荧光团&#xff0c;Cy7的类似物&#xff0c;Cyanine5.5 alkyne用于点击化学标记的远红外/近红外染料炔烃&#xff0c;炔烃可以通过铜催化的点击化学与多种…

Ubuntu 22.04 一次及其繁琐的 允许 Traceroute 探测漏洞修复之旅

前言&#xff1a;允许 Traceroute 探测是绿盟漏洞扫描器报出来的一个漏洞&#xff0c;如下图&#xff1a; 我的系统是ubuntu 22.04&#xff0c;但由于是用户提供的虚拟机&#xff0c;会有些定制的部分&#xff0c;具体定制了哪部分就不知道了&#xff0c;直接描述问题。 解决问…

【服务器数据恢复】LINUX误删除、误格式化的数据恢复

Linux误删除及误格式化的数据恢复方案针对的文件系统&#xff1a; 1 、基于EXT2/EXT3/EXT4文件系统 &#xff1b; 2 、基于Reiserfs文件系统&#xff1b; 3 、基于Xfs文件系统。 Linux误删除及误格式化的数据恢复解决方案&#xff1a; 一、故障检测&#xff1a; 1、检测是否存在…

java学习day56(Spring Boot)Spring Boot

主要内容&#xff1a; 1.Spring Boot基本应用 2.Spring Boot原理深入及源码剖析 3.Spring Boot数据访问 4.Spring Boot视图技术 5.Spring Boot实战演练 6.Spring Boot项目部署 1. SpringBoot基本应用 1.1 约定优于配置Build Anything with Spring Boot&#xff1a;Spring Boot …

【数据结构】堆的实现及排序

目录一、树的相关概念及其特殊二叉树1、数的相关概念2、特殊二叉树二、堆1、堆的实现1.1、堆向下调整算法和向上调整算法的时间复杂度1.2、堆的构建1.3、堆的插入1.4、堆的删除1.5、取堆顶的数据、堆的个数及堆的判空2、堆的排序一、树的相关概念及其特殊二叉树 讲堆之前&#…

透视虎牙斗鱼三季报:游戏直播在各自“求变”中见分晓

游戏直播行业&#xff0c;依然硝烟弥漫。 经历千播大战、熊猫出局的洗礼后&#xff0c;虎牙和斗鱼双方缠斗升级&#xff0c;另有B站和抖音、快手等视频平台来势汹汹&#xff0c;抢夺仅有的市场蛋糕。 而在游戏行业遇冷、政策趋严等因素多重考验下&#xff0c;这场争夺战无疑将…

fsync

由于目前操作系统都具有延迟写(delayed write)功能&#xff0c; fwrite/write并不会直接把数据写到磁盘上&#xff0c; 而是在buffer满时才开始写入到磁盘。 要想把数据写到磁盘上&#xff0c;需要调用fsync函数 open(fd) write(fd) fsync(fd) close(fd) 或者 fopen(fp…

金仓数据库KingbaseES查询计划剖析

目录 1、KingbaseES数据库中的查询生命周期 2、数据设置 3、KingbaseES解释一个查询 4、什么是数据库中的缓冲区和缓存&#xff1f; 5、VERBOSE 命令参数 6、KingbaseES中的 FORMAT 解释 7、总结EXPLAIN使用方式 8、执行计划查看 了解KingbaseES查询计划对于开发人员和…

HarmonyOS应用API手势方法-绑定手势方法

述&#xff1a;为组件绑定不同类型的手势事件&#xff0c;并设置事件的响应方法。 Api&#xff1a;从API Version 7开始支持 一、绑定手势识别&#xff1a; 通过如下属性给组件绑定手势识别&#xff0c;手势识别成功后可以通过事件回调通知组件。 名称参数类型默认值描述ge…

银河麒麟V10+达梦数据库8保姆级安装教程

银河麒麟V10达梦数据库8保姆级安装教程 一、系统和数据库的下载 银河麒麟V10版本&#xff1a; 首页 → 桌面操作系统 → 银河麒麟桌面操作系统V10 → 试用&#xff08;填写信息&#xff09; → 点击地址进行下载&#xff08;X86centos7&#xff09; #如果不想进行上面的操作,…

Casbin——Java版本(笔记)

文章目录一、Casbin 是什么&#xff1f;二、快速开始2.1 载入配置2.2 如何判断权限2.3 model.conf2.3.1 基本格式2.3.2 SpringBoot下的使用2.3.3 匹配的函数内置函数自定义函数2.3.4 基于角色的访问控制角色的层次区分用户和角色隐式角色权限域内RBAC角色与函数2.3.5 优先级模型…

react事件系统(老版本)

带着问题阅读探索 React 为什么有自己的事件系统&#xff1f;什么是事件合成 &#xff1f;如何实现的批量更新&#xff1f;事件系统如何模拟冒泡和捕获阶段&#xff1f;如何通过 dom 元素找到与之匹配的fiber&#xff1f;为什么不能用 return false 来阻止事件的默认行为&…

python【PyQt5】的环境搭建和使用(全网最全)其一

什么是pyQT pyqt是一个用于创建GUI应用程序的跨平台工具包&#xff0c;它将python与qt库融为一体。也就是说&#xff0c;pyqt允许使用python语言调用qt库中的API。这样做的最大好处就是在保存了qt高运行效率的同时&#xff0c;大大提高开发效率。因为&#xff0c;使用python语言…

城市路边停车收费系统/停车收费管理系统

摘 要 近年来&#xff0c;随着社会的进步和发展&#xff0c;车辆也在迅速增加&#xff0c;城市交通的瓶颈不仅体现在道路交通的拥挤上&#xff0c;也体现在传统停车场管理效率和安全性大大滞后于社会的需要&#xff0c;给人们的生活带来了极大的不便。尤其&#xff0c;随着汽车…

二、MongoDB简介及基本操作

mongodb是一个基于文档的强大、灵活、易于扩展的通用型数据库。是基于分布式文件存储的数据库。其由 C 语言编写。旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。 mongodb也是一个介于关系数据库和非关系数据库之间的产品&#xff0c;是非关系数据库当中功能最丰富&…

运动健身买什么耳机好用、最优秀的健身耳机推荐分享

冬天绝对是个减肥的好季节&#xff0c;因为这个季节天气比较冷&#xff0c;我们在运动过程中消耗的热量也就会更多&#xff0c;因此选择一款不错的运动耳机来用坚持就显得尤为重要了。这款运动耳机要能稳定在耳朵上&#xff0c;还要具备防水功能&#xff0c;同时音质上也要有保…

闲人闲谈PS之三十四——项目成本费用控制阈值

**惯例闲话&#xff1a;**最近有小伙伴问闲人有没有PS顾问资源&#xff0c;闲人问了一圈&#xff0c;结果发现都没有档期&#xff0c;不免让闲人有些失落&#xff0c;好心答应帮忙&#xff0c;结果帮不上…但是隐隐约约觉得在几年前说的话被应验了&#xff0c;PS模块一定是个热…