「安卓高性能」使用HardwareBuffer上传YUV到OES纹理

1. 前言

看到该标题后,相信大家都会产生一个疑问,为什么要上传YUV到OES纹理(OES Texture)?

在解答这个疑问前,我们先了解一下MediaCodec。MediaCodec是Android提供的用于对音视频进行编解码的类,它通过访问底层的codec来实现编解码的功能。

MediaCodec编码器支持两种类型的输入数据,分别是ByteBuffer和Texuture(OES Texutre是Texture的一种)。

  • Texture:Texture又叫纹理,表示视频数据存储在显存中,在编码前将纹理绘制到编码的SurfaceTexture中从而触发编码,整个编码流程不需要占用过多的CPU资源,由GPU和VPU协作完成编码任务。

  • ByteBuffer:ByteBuffer将视频数据存储在内存中,编码前通过内存拷贝的方式传递给编码器。

两种方式各有优劣。使用 Texture 进行编码利用了GPU的图像处理能力,可以减少 CPU 的工作量,提高编码效率,而使用 ByteBuffer 进行编码则更加灵活,数据可以从文件中读取,也可从摄像头采集。

在安卓上,摄像头采集是由驱动和系统完成的,上层在获取视频数据时已经存储在Texture或者ByteBuffer中了,且摄像头采集帧率有限。如果要测试出极限的编码帧率,就要寻找一种方法,在应用层也能够将视频数据存储到Texture中传给MediaCodec编码。这里就要介绍今天的主角HardwareBuffer

Android HardwareBuffer 是 Android 操作系统提供的一种用于共享和传递图形和图像数据的高性能、低延迟的跨进程内存缓冲区。它在 Android 8.0(API 级别 26)中首次引入,并在后续版本中不断改进和扩展。安卓支持将HardwareBuffer与纹理绑定,通过GPU将纹理绘制到编码的Surface中,减少内存的拷贝。

本文介绍如何使用HardwareBuffer上传YUV到OES纹理,主要内容有:

  1. 介绍HardwareBuffer及其实现原理
  2. 使用OpenGL上传YUV数据
  3. 渲染纹理验证上传结果

2. HardwareBuffer

Android HardwareBuffer 主要用于优化图形渲染、视频编解码、相机捕捉等场景,提供了一种高效的内存传输机制,可以加速多个应用和系统组件之间的数据传输

以下是 Android HardwareBuffer 的一些关键特性和用途:

  1. 跨进程共享:Android HardwareBuffer 允许多个应用程序和系统组件之间共享图像和图形数据,而无需昂贵的数据复制操作。这对于多应用协作和优化性能非常有用。
  2. 性能优化:HardwareBuffer 提供了直接内存访问的机制,减少了数据复制和转换的开销,从而提高了性能并降低了延迟。这在图形渲染、视频编解码等性能敏感的应用中尤其有用。
  3. 多格式支持:HardwareBuffer 支持多种图像格式,包括 RGBA、YUV 等,使其适用于各种应用场景。
  4. 硬件加速:由于 HardwareBuffer 直接与硬件 GPU 和图像处理器集成,因此可以在硬件层面上进行优化,提供更高效的渲染和图像处理。
  5. 安全性:HardwareBuffer 具有内置的安全性,可确保数据传输和访问在 Android 系统中受到保护,防止恶意应用程序的滥用。
  6. API 支持:Android 提供了一组 API,开发者可以使用这些 API 创建、修改、共享和销毁 HardwareBuffer。一些与 OpenGL、Vulkan 等图形 API 集成的扩展也可用于更方便地与 HardwareBuffer 进行交互。

Android HardwareBuffer 是一个在 Android 平台上提供高性能、跨进程图像和图形数据传输的关键组件,对于提高多媒体应用程序的性能和效率非常有帮助。它可以用于图形渲染、相机捕捉、视频处理等各种应用场景,减少了数据复制和转换的开销,提供更流畅的用户体验。

2.1 实现原理

HardwareBuffer实质上是一块内存,该内存如何在GPU和CPU之间传递是提升编码效率的关键。为了避免概念混淆,本文将内存分为系统内存显示内存

  1. 系统内存和显示内存的定义:系统内存是指操作系统和应用程序可以直接访问的主机内存,而显示内存是指显卡(GPU)专用的内存,用于存储图形数据和执行图形计算。系统内存通常由CPU管理,而显示内存由GPU管理。
  2. 共享系统内存:在某些情况下,GPU和CPU可以共享一部分内存,以实现更高效的数据传输和共享数据。这种共享内存通常称为共享系统内存。共享系统内存可以通过特定的技术(如共享缓冲区或映射内存)实现,以便GPU和CPU可以直接访问相同的内存区域,从而避免数据复制和传输的开销。
  3. 物理存储器上的位置:系统内存和显示内存可以位于相同的物理存储器上,例如在集成显卡中,系统内存和显示内存可能是统一的物理内存。然而,在独立显卡中,显示内存通常是独立的显存,与系统内存分开。

那么HardwareBuffer存储在哪里呢?为什么使用HardwareBuffer可以提高编码效率?

前面说到输入Texture经由GPU绘制到编码Texture,MediaCodec编码器从纹理中读取数据进行编码,为了GPU能够访问到输入Texture的视频数据,意味着要将视频数据存放在显示内存中,同时为了提升效率,不让CPU参与数据的拷贝,有如下方法:

  • 内存映射:将显示内存映射到程序的地址空间,程序往该地址写入视频数据(内存映射后,不需要CPU再把系统内存中的视频数据拷贝到显示内存中)。

  • DMA控制器:由DMA控制器将系统内存中的视频数据拷贝到显示内存中,让CPU处理其它事情,拷贝完成后再重新获取CPU调度。

早期,所有设备之间的通信都需要经过CPU,结果严重影响了整个系统的性能。为了解决这个问题,有些设备加入了直接内存访问(DMA)的能力。DMA允许设备在北桥的帮助下,无需CPU的干涉,直接读写RAM。到了今天,所有高性能的设备都可以使用DMA。虽然DMA大大降低了CPU的负担,却占用了北桥的带宽,与CPU形成了争用。

在安卓手机中,图形处理单元(GPU)通常与主处理器(CPU)集成在同一芯片上,共享系统的内存(RAM)。这意味着显存并没有单独的物理存储器,而是使用系统内存来存储图形数据。

所以安卓HardwareBuffer通常采用的是内存映射的方式。HardwareBuffer是NDK提供的API,其底层由GraphicBuffer实现。

安卓手机通过使用帧缓冲区(Frame Buffer)来存储图形数据。帧缓冲区是一块显示内存区域,用于存储每个像素的颜色和其他相关信息。当图形数据需要在屏幕上显示时,GPU将从帧缓冲区中读取数据,并将其发送到显示屏上。
用户空间中的应用程序可以申请分配一块图形缓冲区(Graphic Buffer),并且将这块图形缓冲区映射到应用程序的地址空间来,以便可以向里面写入要绘制的画面的内容。
图形缓冲区(Graphic Buffer)是Android的术语,帧缓冲区(Frame Buffer)是Linux Kernel的术语。帧缓冲区是对显示内存的抽象,图形缓冲区可以在帧缓冲区上分配,即显示内存上分配,也可以在系统内存上分配,或者其它地方分配,这完全是由厂商提供的gralloc驱动决定。

Q: MediaCodec为什么要创建自己的Texture,而不直接使用输入Texture?

A: 类似于双缓冲技术,当编码器在访问Texture时,如果外部正在往里面写数据,就会导致画面异常。使用两个Texture,先把视频数据绑定到输入Texture,GPU将输入Texture绘制到帧缓冲区后,再交换输入Texture和编码Texture的缓冲区指针。

3. 上传

上传指将YUV数据和纹理进行绑定,需要使用EGL扩展。

EGL扩展:EGL扩展是对EGL标准的扩展,用于提供额外的功能或特性。这些扩展可以由硬件厂商、操作系统或图形库开发者提供,以满足特定的需求或支持特定的硬件功能。

int WrapYuvToTexture(EGLDisplay eglDisplay, int& textureId, const PlaneBuffer& planeBuffer,
                     uint32_t width, uint32_t height) {
    EGLDisplay native_egl_display = eglDisplay;
    AHardwareBuffer hardwareBuffer;
    AHardwareBuffer_Desc desc = {};
    desc.width = width;
    desc.height = height;
    desc.stride = width;
    desc.layers = 1;
    desc.usage = AHARDWAREBUFFER_USAGE_CPU_READ_NEVER | AHARDWAREBUFFER_USAGE_CPU_WRITE_NEVER |
                 AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT;
    desc.format = AHARDWAREBUFFER_FORMAT_Y8Cb8Cr8_420;
    int result = AHardwareBuffer_allocate(&desc, &hardwareBuffer);
    if (result != 0) {
        return -1;
    }

    AHardwareBuffer_Planes planes_info;
    result = AHardwareBuffer_lockPlanes(hardwareBuffer, AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY, -1,
                                        nullptr, &planes_info);

    if (result != 0) {
        AHardwareBuffer_release(hardwareBuffer);
        return -1;
    }
    if (planes_info.planeCount != 3) {
        AHardwareBuffer_release(hardwareBuffer);
        return -1;
    }

    memcpy(planes_info.planes[0].data, planeBuffer.dataY, planes_info.planes[0].rowStride * height);

    for (int i = 0; i < planes_info.planes[1].rowStride * height / 2; i++) {
        uint8_t* uBufferPtr = reinterpret_cast<uint8_t*>(planes_info.planes[1].data) + i;
        *uBufferPtr = *(planeBuffer.dataU + i / 2);
    }

    int32_t fence = -1;
    result = AHardwareBuffer_unlock(hardwareBuffer, &fence);

    if (result != 0) {
        AHardwareBuffer_release(hardwareBuffer);
        return -1;
    }

    EGLClientBuffer clientBuffer = g_eglGetNativeClientBufferANDROID(hardwareBuffer);

    EGLImageKHR img = g_eglCreateImageKHR(native_egl_display, EGL_NO_CONTEXT,
                                          EGL_NATIVE_BUFFER_ANDROID,
                                          clientBuffer, 0);

    if (img == EGL_NO_IMAGE_KHR) {
        AHardwareBuffer_release(hardwareBuffer);
        return -1;
    }
    glGenTextures(1, reinterpret_cast<GLuint*>(&textureId));
    glBindTexture(GL_TEXTURE_EXTERNAL_OES, textureId);
    g_glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, (GLeglImageOES) img);
    return 0;
}

3.1 输入

  • eglDisplay,显示设备句柄,使用默认的即可
  • textureId,用于保存纹理ID
  • planeBuffer,生成的YUV数据,可以自定义数据结构,能访问到三个分量即可
  • width,宽
  • height,高

3.2 流程

3.3 注意事项

  • 申请失败或不再使用HardwareBuffer请使用AHardwareBuffer_release释放资源
  • Y8Cb8Cr8格式存储I420,UV数据分别占用一个字节,所以是间隔一个字节存储一个分量

4. 渲染

将YUV数据与OES纹理进行绑定后,需要将OES纹理渲染到屏幕上来验证是否绑定成功。

可以自己搭建OpenGL ES的环境进行测试,也可以基于已有的项目进行验证。

参考GitHub - githubhaohao/NDK_OpenGLES_3_0: Android OpenGL ES 3.0 从入门到精通系统性学习教程,将YUV上传到OES纹理请参考上传一节。

注意OpenGL ES的上下文在多个线程间共享的问题,请搜索相关资料进行了解。

5. 参考文档

Android帧缓冲区(Frame Buffer)硬件抽象层(HAL)模块Gralloc的实现原理分析:Android帧缓冲区(Frame Buffer)硬件抽象层(HAL)模块Gralloc的实现原理分析_gralloc 能指定 stride吗-CSDN博客

GPU存储体系:https://www.cnblogs.com/hellobb/p/11023873.html

每个程序员都应该了解的内存知识:https://lrita.github.io/2018/06/10/programmer-should-know-about-memory-0/

HardwareBuffer单元测试:https://android.googlesource.com/platform/cts/+/master/tests/tests/nativehardware/jni/AHardwareBufferGLTest.cpp

  • 14
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值