前文
在序中我曾提及到如果不需要将渲染的结果进行输出, 这条数据链路的传输可以由
VPU -> CPU -> GPU -> CPU -> VPU
简化为 VPU -> GPU -> VPU
这是一个很自然而言的想法, 那么业界的大佬是否也有这种想法; 实际上早在 2011 年 Marek Szyprowski (Samsung)
就实现了
通过 V4L2
采集摄像头的画面, 然后直接用 DRM
显示到屏幕上, 全程没有 CPU
的干预.
更多技术细节可以看看何大神的文章 dma-buf 由浅入深(一) —— 最简单的 dma-buf 驱动程序
我不从事驱动开发, 没有能力也不想讲述这些内容; 故此我还是想站在应用层的角度去讲述这些事情.
有一个问题, 不止诸位有无想过?
在组装 PC 整机时, 我们会说 CPU XXX 型号, DDR XXX G, 显卡 XXX 型号显存 XXX G;
而在买手机时, 我们好像并不会讲显存有多少G;如果感到不和谐的话,请仔细回想一下.
手机上采用的大多数是 SOC
芯片, 全称 System On Chip
, 顾名思义其将 CPU
、GPU
等诸多硬件单元都集中到了一整块芯片上.
注意其是整理密不可分, 跟 PC
这种分离式的结构大有不同.
实际上 CPU
和 GPU
乃至 VPU
都是先接到 BUS
总线, 尔后再与 DDR
也就是内存相连.
所以某种程度上, SOC
上的显存和内存是同一会事情; 既然不同的硬件单元的内存实际上是连接在一起的.
那么是不是可以共享呢? 答案是肯定的.
DMA BUF
如果开发了比较久, 一般会接触到 mmap
这个函数, 我比较熟悉的常规操作有两种:
- 跨进程共享内存
- 映射文件
这固然解决了一些数据共享问题, 但更多是站在 CPU
这个角度的技术实现.
那么站在 BUS
总线这个角度, 这个角色是谁的呢? 答案是 DMA BUF
.
DMA BUF
到目前为止已经有三版实现了, 何大神文章里其实是第一版, 三版分别为:
- DRM : 最初主要针对于 V4L2 和 DRM
- ION : 主要应用到安卓系统上, 不过目前已经慢慢停止使用
- DMA HEAP : 目前的主流应用, Linux 和 安卓 都在逐步支持
对于应用层开发, 将其理解为一种 malloc
的方式即可, 只不过在 malloc
的过程中我们可以拿到一个 fd
, 这个 fd
能够帮助我们跨设备节点实现数据共享.
有兴趣具体如何操作的同学可以参考一下 DrmAllocateMethod 和 DmaHeapAllocateMethod
OES Texture
有了 DMA BUF
这项技术,之后需要各设备节点可以实现数据共享需要什么? 答案肯定是各端对 DMA-BUF
的支持啦.
VPU
是可以的, 这里我们不展开; 来讲讲 OpenGL ES
是如何支持的.
实际上 OpenGL ES
或者 OpenGL
是不支持通过使用 DMA BUF
导入纹理的, 因为这种方式不通用不可跨平台.
故实现此特性的实际上是在 EGL
, 若 EGL
支持 EGL_EXT_image_dma_buf_import
拓展, 即可通过 EGL
的拓展接口通过 DMA BUF FD
.
整个过程需要经历两个步骤, 创建 EGLImage
和绑定 EGLImage
至 OES Texture
.
创建 EGLImage
void* CreateEGLImage(const OpenGLFeature& openGLFeature, OpenGLRenderTexture::ptr tex, AbstractDeviceAllocateMethod::ptr allocate)
{
EGLImageKHR image = nullptr;
if (openGLFeature._eglCreateImageKHR)
{
switch (tex->format)
{
case DataFormat::R8G8B8A8_UNORM:
case DataFormat::R8G8B8A8_UINT:
{
EGLint attr[] =
{
EGL_LINUX_DRM_FOURCC_EXT, MMP_DRM_FORMAT_ABGR8888,
EGL_WIDTH, tex->horStride,
EGL_HEIGHT, tex->virStride,
EGL_DMA_BUF_PLANE0_FD_EXT, allocate->GetFd(),
EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0,
EGL_DMA_BUF_PLANE0_PITCH_EXT, tex->horStride * 4,
EGL_IMAGE_PRESERVED_KHR, EGL_TRUE,
EGL_NONE, EGL_NONE,
EGL_NONE, EGL_NONE,
EGL_NONE
};
if (allocate->GetFlags() & DmaHeapAllocateMethod::kArmAFBC)
{
attr[3] = tex->w; attr[5] = tex->h;
attr[14] = EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT; attr[15] = (MMP_AFBC_FORMAT_MOD_SPARSE | MMP_AFBC_FORMAT_MOD_BLOCK_SIZE_16x16) & 0xFFFFFFFF;
attr[16] = EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT; attr[17] = (0x08<<24u); // ARM 平台标志位
}
image = openGLFeature._eglCreateImageKHR(openGLFeature._eglDisplay, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, (EGLClientBuffer)nullptr, attr);
break;
}
case DataFormat::NV12_UINT:
{
if (!(allocate->GetFlags() & DmaHeapAllocateMethod::kArmAFBC))
{
EGLint attr[] =
{
EGL_LINUX_DRM_FOURCC_EXT, MMP_DRM_FORMAT_NV12,
EGL_WIDTH, tex->horStride,
EGL_HEIGHT, tex->virStride,
EGL_DMA_BUF_PLANE0_FD_EXT, allocate->GetFd(),
EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0,
EGL_DMA_BUF_PLANE0_PITCH_EXT, tex->horStride,
EGL_DMA_BUF_PLANE1_FD_EXT, allocate->GetFd(),
EGL_DMA_BUF_PLANE1_OFFSET_EXT, tex->horStride * tex->virStride,
EGL_DMA_BUF_PLANE1_PITCH_EXT, tex->horStride,
EGL_IMAGE_PRESERVED_KHR, EGL_TRUE,
EGL_NONE
};
image = openGLFeature._eglCreateImageKHR(openGLFeature._eglDisplay, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, (EGLClientBuffer)nullptr, attr);
}
else
{
// See aslo : https://github.com/rockchip-linux/mpp/issues/348
EGLint attr[] =
{
EGL_LINUX_DRM_FOURCC_EXT, MMP_DRM_FORMAT_YUV420_8BIT,
EGL_WIDTH, tex->w,
EGL_HEIGHT, tex->h,
EGL_DMA_BUF_PLANE0_FD_EXT, allocate->GetFd(),
EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0,
EGL_DMA_BUF_PLANE0_PITCH_EXT, tex->w,
EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, (MMP_AFBC_FORMAT_MOD_SPARSE | MMP_AFBC_FORMAT_MOD_BLOCK_SIZE_16x16) & 0xFFFFFFFF,
EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, (0x08<<24u), // ARM 平台标志位
EGL_IMAGE_PRESERVED_KHR, EGL_TRUE,
EGL_NONE
};
image = openGLFeature._eglCreateImageKHR(openGLFeature._eglDisplay, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, (EGLClientBuffer)nullptr, attr);
}
break;
}
default:
break;
}
}
if (image == 0 /* EGL_NO_IMAGE */)
{
EGLint error = openGLFeature._eglGetError();
GL_LOG_ERROR << "eglCreateImageKHR fail , egl error is: 0x" << std::hex << std::setw(2) << std::setfill('0') << std::uppercase << error;
assert(false);
}
return image;
}
绑定 EGLImage 至 OES Texture
static void InitExternalTexture(const OpenGLFeature& openGLFeature, OpenGLRenderTexture::ptr tex, AbstractAllocateMethod::ptr alloc, uint64_t size)
{
#if MMP_PLATFORM(LINUX)
assert(GL_TEXTURE_EXTERNAL_OES == tex->target);
{
static auto getDeviceAlloc = [](OpenGLRenderTexture::ptr tex, AbstractAllocateMethod::ptr alloc, uint64_t size) -> AbstractDeviceAllocateMethod::ptr
{
AbstractDeviceAllocateMethod::ptr deviceAlloc = alloc ? std::dynamic_pointer_cast<AbstractDeviceAllocateMethod>(alloc) : nullptr;
if (!deviceAlloc)
{
if (!tex->deviceAlloc)
{
tex->deviceAlloc = std::make_shared<DmaHeapAllocateMethod>();
tex->deviceAlloc->Malloc(size);
}
deviceAlloc = std::dynamic_pointer_cast<AbstractDeviceAllocateMethod>(tex->deviceAlloc);
if (deviceAlloc && alloc)
{
memcpy(deviceAlloc->GetAddress(0), alloc->GetAddress(0), size);
}
else
{
memset(deviceAlloc->GetAddress(0), 0xFF, size);
}
}
return deviceAlloc;
};
AbstractDeviceAllocateMethod::ptr deviceAlloc = getDeviceAlloc(tex, alloc, size);
if (deviceAlloc && openGLFeature._glEGLImageTargetTexture2DOES)
{
EGLImageKHR image = CreateEGLImage(openGLFeature, tex, deviceAlloc);
glBindTexture(tex->target, tex->texture);
openGLFeature._glEGLImageTargetTexture2DOES(tex->target, image);
tex->onTextureDeletes.push_back([openGLFeature, image]()
{
DestroyEGLImage(openGLFeature, image);
});
glTexParameteri(tex->target, GL_TEXTURE_WRAP_S, tex->wrapS);
glTexParameteri(tex->target, GL_TEXTURE_WRAP_T, tex->wrapT);
glTexParameteri(tex->target, GL_TEXTURE_MAG_FILTER, tex->magFilter);
glTexParameteri(tex->target, GL_TEXTURE_MIN_FILTER, tex->minFilter);
FORCE_GL_FENCE_SYNC();
}
else
{
assert(false);
}
}
CHECK_GL_ERROR_IF_DEBUG();
#endif
}
这里只讲概念不讲代码, 故我不过多解释; 不过完整的代码实现你可以查阅 OpenGLInitStep.
YUV OES Texture
OpenGL ES
的 Texture
可以是 YUV
吗? 答案是可以的, 只要 OpenGL
支持 GL_EXT_YUV_target
拓展, 或 EGL
支持 EGL_EXT_yuv_surface
拓展即可.
具体详细地说明可以参考 EGL
标准 EGL_EXT_yuv_surface.
这里主要讲述一下其使用的一些要点:
- 当将
YUV
作为Texture
使用时,Sampler
需要声明为__samplerExternal2DY2YEXT
, 这时采样得到的将会是YUV
值对应xyz
- 当将
YUV
作为FrameBuffer OBject
时,FrameBuffer
需要加上layout(yuv)
的限定说明, 输出的xyz
将会对应到YUV
- 当将
YUV
作为FrameBuffer OBject
时,glClear
的rgb
也将对应yuv
, 故需要特殊处理
YUV
纹理是降低开销, 提升性能的有效手段.