简介:最近研究了一些开源的项目,同时也意识到以前写了好多文章从来没有写过对某一个开源项目或者引擎的源码剖析与自己对它的理解,所以这里专门写了一篇也算是我对这一部分的处女作

资料介绍

文档: ntroduction | GPUPixelI
移动端安卓IOS的集成: 集成 | GPUPixel
滤镜列表: 滤镜列表 | GPUPixel
sunshey/GPUBeauty: 在gpupixel基础上完善GPUPixelSourceImage美颜功能

CV/AI部分:

使用的欢聚的VNN, VNN/doc/face_landmark_detection.md at master
VNN开放使用但是没有源代码!只有doc、lib、ai的model(体积非常小)、有使用的demo,展示VNN是怎么样调用 VNN/demos/Windows
在这里插入图片描述

迪士尼人脸风格化
mask的分割
对于AI/CV这一块最新版本作者使用了 MNN(Mobile Neural Network)框架的高性能人脸检测和面部特征点检测,具体详细文档在这里: 人脸关键点 | GPUPixel ,至于为什么不在使用VNN,主要是VNN不开源,如果想搞成Wasm的形式就无法进行。

gpupixe架构:

gpupixel是参考的gpuimage框架设计,然后再根据 GPUImage-x C++版本的代码大量的搬迁过来;
GPUimage
AwemeLike:特效相机 ZZZZou/AwemeLike: 算法借鉴仿照抖音的特效相机,实现美颜、2D贴图、分屏、转场等;
在这里插入图片描述

渲染线程

gpuimage与gpupixel同样采用单线程主渲染线程架构(Main Render Thread Architecture),
在这里插入图片描述
由于CPU端没有做复杂的运算,在实时相机或视频处理方面表现良好,如果需要在CPU端做复杂的运算就不行了。
useAsCurrent使用将当前 OpenGL 上下文设置为活动上下文。它确保在调用 OpenGL 函数之前,正确的上下文已经被绑定到当前线程。这对于在多线程环境中使用 OpenGL 非常重要,因为 OpenGL 上下文是线程特定的。
在这里插入图片描述
opengl同步操作:LocalDispatchQueue 是一个任务队列确保是同步的并且是有顺序、 原子性的执行gl资源的删除,任务在useAsCurrent当前绑定的窗口线程上执行。所有任务必须通过调用 processOne() 或 processAll() 方法在将要执行这些任务的线程上处理。LocalDispatchQueue 同步操作是在主线程下(渲染线程与主线程是一致)
在这里插入图片描述

对于opengl的同步操作除了gpupixel的对gl资源删除需要有顺序、 原子性额执行,资源的创建(纹理、framebuffer、缓冲区)也最好需要这样中,这样对于一个SDK或引擎来说会对你有很大的帮助减少未来大量的bug。
除此之外对于同一进程下共享上下文、共享纹理,异步的数据传输PBO、DMA都需要使用lock、同步机制,这里gpupixel对于视频、相机类的异步PBO都需要runSyn操作,防止视频/相机出现帧撕裂等情况其中跟GPUimage的 contextQueue是一样的作用,
在这里插入图片描述
contextQueue 是一个 全局唯一的串行队列),所有 OpenGL 操作均通过 dispatch_sync 或 dispatch_async 提交到此队列。这确保了 OpenGL 命令的 顺序执行,避免了多线程竞争导致的上下文状态混乱
PBO:
在这里插入图片描述
以下是共享上下文、共享纹理和PBO视频异步读取的伪代码示例,并使用队列(queue)和互斥锁(mutex)进行同步操作。

1. 共享上下文
// 初始化
Create OpenGL context
Create shared OpenGL context
Create mutex


// 线程1:渲染线程
Thread1 {
    Lock mutex
    Make shared context current
    Render frame
    Unlock mutex
}


// 线程2:更新数据线程
Thread2 {
    Lock mutex
    Make shared context current
    Update data
    Unlock mutex
}2. 共享纹理
// 初始化
Create OpenGL context
Create shared OpenGL context
Create texture
Create mutex


// 线程1:渲染线程
Thread1 {
    Lock mutex
    Make shared context current
    Bind texture
    Render frame using the texture
    Unlock mutex
}


// 线程2:更新纹理线程
Thread2 {
    Lock mutex
    Make shared context current
    Bind texture
    Update texture data
    Unlock mutex
}3. PBO视频异步读取
// 初始化
Create OpenGL context
Create PBO
Create queue
Create mutex


// 线程1:渲染线程
Thread1 {
    Lock mutex
    Make context current
    Bind PBO
    Map PBO to client memory
    if (queue is not empty) {
        data = queue.pop()
        Copy data to PBO
    }
    Unmap PBO
    Render frame using PBO
    Unlock mutex
}


// 线程2:视频读取线程
Thread2 {
    while (video frames available) {
        Read frame from video
        Lock mutex
        queue.push(frame data)
        Unlock mutex
    }
}

以上伪代码展示了如何使用队列和互斥锁进行同步操作,以实现共享上下文、共享纹理和PBO视频异步读取的功能。

dispatch_queue部分的解析:

dispatch_queue包含了两个类分别是LocalDispatchQueue 与 DispatchQueue,它两是非常著名的多线程同步的库,DispatchQueue没有使用(因为gpupixel 结构上还没有很好的支持opengl的多线程)。
再者上面也讨论到了gpuimage与gpupixel 都是基于单线程的,只使用了LocalDispatchQueue。但是作者这里有一个挺大的问题:gpupixel是单线程架构,并不需要加mutex锁,直接使用queue即可;

在这里插入图片描述

对于:DispatchQueue
Serial 队列表示任务是串行执行的。(还可以适用于opengl)
行为:任务会按照添加到队列的顺序逐个执行。只有一个线程处理任务,当前任务完成后才会开始下一个任务。
适用场景:
需要严格按照顺序执行的任务。免任务之间的并发冲突(如共享资源的操作)。

Concurrent队列表示任务是并发执行的。(不适用于opengl)
行为:
多个任务可以同时执行,具体的并发度取决于系统的硬件能力。使用多个线程来处理任务。
适用场景:
任务之间没有依赖关系,可以独立并行执行,需要提高任务的执行效率,充分利用多核 CPU。

opengl的多线程实现:

OpenGL 本身不是多线程安全的,至于OpenGL 单线程渲染的原因:OpenGL 开始于20世纪90年代到200X年的设计初衷并非是多线程的(当时大量的程序也是单线程的),opengl状态机模型使得多个线程同时访问和修改 OpenGL 状态容易引发数据竞争和不一致问题!要实现opengl的多线程,只能使用两种方式:
1、线程同步: 使用锁机制来确保同一时间只有一个线程访问 OpenGL 上下文;
2、多上下文管理: 为每个线程创建独立的 OpenGL 上下文,并在它们之间共享资源(如纹理、缓冲区等) 使用任务队列来管理 OpenGL 操作,确保所有 OpenGL 操作在同一个线程中顺序执行。可以参考 DispatchQueue 类的实现。
其实还有另外一种方式:在大多数的渲染引擎中确实建议只使用一个渲染线程,现代的渲染引擎架构最好是渲染线程是单独的线程(使用LocalDispatchQueue 来实现):

在这里插入图片描述
在主线程中,不再直接调用OpenGL API进行绘制,而是将绘制代码放在渲染线程中。在主线程中需要渲染时,向渲染线程发送命令,并附带上所需的参数。

gpupixe 数据流:

双 PBO(Pixel Buffer Object)用于CPU与GPU通信中异步读取像素数据,以提高性能。
在这里插入图片描述如果 PBO 成功映射到 CPU 地址空间,则可以访问其数据并进行处理。处理完成后,调用 glUnmapBuffer 函数取消映射 PBO,_readPixelData 用于存储从帧缓冲区读取的原始 RGBA 像素数据,而 _yuvFrameBuffer 用于存储转换后的 YUV 像素数据。在这里插入图片描述

gpupixe使用libyuv来实现色域的转换,但是这样是基于cpu的即使使用了SIMD加速也不够快,可以基于GPU shader来实现色域的转换。

数据流图:
在这里插入图片描述
源码对应的目录如下:

core:
android:安卓jni调用接口  (todo:单独放到胶水层去!)
face_detect:跟AI的接口,这里是脸部使用的vnn;(todo:以后作为)
filter:滤镜类,所有的图层、功能算法都放在此处;(todo:需要更细分,分解)
resources:资源类    (todo:参考美图素材包机制,sdk中只放最基础最核心的资源)
utils:基础公共方法、函数等 log、数学math_toolbox等;任务调度类:单线程渲染LocalDispatchQueue、多线程渲染DispatchQueue(现在gpup使用LocalDispatchQueue 未来可以考虑使用DispatchQueue,而且放到core里)
source:对应框架图的输入
target:对应框架图的输出,输出位视频流: TargetRawDataOutput class下的_readPixelData 与 _yuvFrameBuffer
third_party:第三方库;(位置以后可以放出去)

胶水层:
在这里插入图片描述

调用方式:

在 GPUPixel 也是参考的gpuimage的,Filter 类实现了责任链模式(Chain of Responsibility),其中每个 Filter 对象既是一个数据源(Source),也是一个数据目标(Target)。这种设计允许多个 Filter 对象串联在一起,形成一个处理链,每个 Filter 对象处理输入数据并将结果传递给下一个 Filter 对象。

在 GPUPixel 库中,Filter 类实现了责任链模式(Chain of Responsibility),其中每个 Filter 对象既是一个数据源(Source),也是一个数据目标(Target)。这种设计允许多个 Filter 对象串联在一起,形成一个处理链,每个 Filter 对象处理输入数据并将结果传递给下一个 Filter 对象。
在这里插入图片描述

FilterGroup 类的作用:
FilterGroup 类的主要作用是将多个滤镜组合在一起,形成一个滤镜组。它提供了管理子滤镜的方法,并确保子滤镜按顺序处理输入数据。通过 FilterGroup,可以实现复杂的滤镜效果,而不需要手动管理每个子滤镜的连接和处理顺序。

继承 FilterGroup 与直接继承 Filter 的区别
继承 FilterGroup:
适用于需要组合多个滤镜的场景。提供了管理子滤镜的方法,可以方便地添加、移除和管理子滤镜。子滤镜按顺序处理输入数据,形成一个处理链。
直接继承 Filter:
适用于单一滤镜的场景。只处理单一滤镜的输入和输出,不涉及子滤镜的管理。适合实现简单的滤镜效果。

opengl上下文创建与管理:

GPUPixel的examples在win、linux是这样创建上下文并且绑定窗口
在这里插入图片描述
GPUPixel的SDK直接使用宏来区别不同的平台的上下文,统一在gpupixel_context这种做法不好!最好上下文在胶水层实现然后管理,根据不同的平台不同的窗口管理程序不一样:比如
在这里插入图片描述
在这里插入图片描述

第三方库:
std:主要用来纹理贴图的解析;
math_toolbox:数学库,自己定义。(感觉直接使用glm才是更标准)

gpupixel FQA:

图片有问题放进去的有一些图片:VNN被移除了?
使用windows自带的测试,换了一个demo.png就闪退了。是图片有什么特别的要求吗
VNN要求4通道,如果放进去3通道的图片会导致失败;
任何一张其他的图片都会在这里报错,是图片格式问题吗,图片美颜功能在iOS上无法使用
对msvc的支持:gpupixel不支持MSVC,主要是VNN没有源代码只提供了lib库,是使用clang等编译出来的,msvc引进来无法使用
人脸点: VNN人脸关键点数据问题
在这里插入图片描述

Logo

昇腾计算产业是基于昇腾系列(HUAWEI Ascend)处理器和基础软件构建的全栈 AI计算基础设施、行业应用及服务,https://devpress.csdn.net/organization/setting/general/146749包括昇腾系列处理器、系列硬件、CANN、AI计算框架、应用使能、开发工具链、管理运维工具、行业应用及服务等全产业链

更多推荐