edge264
极简软件解码器,为H.264/AVC视频格式提供最新性能表现。请注意这是一个正在进行中的项目,在制作GStreamer/VLC插件后将准备就绪。
功能特性
- 支持渐进式High和MVC 3D配置,最高级别6.2
- 最高8K UHD的任何分辨率
- 8位4:2:0平面YUV输出
- 切片和任意切片顺序
- 切片和帧多线程
- 每切片参考图片列表
- 内存管理控制操作
- 长期参考帧
支持平台
- Windows: x86, x64
- Linux: x86, x64, ARM64
- Mac OS: x64
编译和测试
edge264完全使用C语言开发,采用128位向量扩展和向量内部函数,可以使用GNU GCC或LLVM Clang编译。SDL2运行时库可用于(可选)通过edge264_test启用显示。
以下是调整编译库文件的make选项:
CC
- 用于将源文件转换为目标文件的C编译器(默认cc)CFLAGS
- 传递给CC和TARGETCC的额外编译标志TARGETCC
- 用于将目标文件链接到库文件的C编译器(默认CC)LDFLAGS
- 传递给TARGETCC的额外编译标志TARGETOS
- 结果文件的命名约定,在Windows|Linux|Darwin中(默认主机)VARIANTS
- 逗号分隔的附加变体列表,包含在库中并在运行时选择(默认logs)
变体包括:
x86-64-v2
- 为x86-64微架构级别2编译的变体(SSSE3、SSE4.1和POPCOUNT)x86-64-v3
- 为x86-64微架构级别3编译的变体(AVX2、BMI、LZCNT、MOVBE)logs
- 以YAML格式编译日志支持的变体(头部和切片)
|
|
自动化测试程序edge264_test可以浏览给定目录中的文件,解码每个<video>.264
文件,并将其输出与每个同级文件<video>.yuv
(如果找到)进行比较。在AVCv1、FRExt和MVC一致性比特流集合中,109/224个文件无错误解码,其余使用尚不支持的功能。
|
|
示例代码
这是一个完整的示例,从命令行打开Annex B格式的输入文件,并将其解码的帧以平面YUV顺序转储到标准输出。有关更完整的示例,请参阅edge264_test.c,它还可以显示帧。
|
|
API参考
const uint8_t * edge264_find_start_code(buf, end, four_byte)
返回指向下一个三或四字节(0)001起始代码前缀的指针,如果未找到则返回end。
参数:
const uint8_t * buf
- 要搜索的缓冲区的第一个字节const uint8_t * end
- 缓冲区后面的第一个无效字节,停止搜索int four_byte
- 如果为0则寻找001前缀,否则寻找0001
Edge264Decoder * edge264_alloc(n_threads, log_cb, log_arg, log_mbs, alloc_cb, free_cb, alloc_arg)
分配并初始化解码上下文。
参数:
int n_threads
- 后台工作线程数,0禁用多线程,-1在运行时检测逻辑核心数void (* log_cb)(const char * str, void * log_arg)
- 如果不为NULL,则是一个fputs兼容的函数指针,edge264_decode_NAL将调用它以记录每个头部、SEI或宏块(需要logs变体,否则在运行时失败,从同一线程调用,多线程解码中的宏块除外)void * log_arg
- 传递给log_cb的自定义值int log_mbs
- 设置为1以启用宏块日志记录void (* alloc_cb)(void ** samples, unsigned samples_size, void ** mbs, unsigned mbs_size, int errno_on_fail, void * alloc_arg)
- 如果不为NULL,则是一个函数指针,edge264_decode_NAL将调用(在同一线程上)而不是malloc来请求为帧分配样本和宏块缓冲区(errno_on_fail对于强制分配是ENOMEM,或者对于可能跳过以节省内存但减少播放平滑度的分配是ENOBUFS)void (* free_cb)(void * samples, void * mbs, void * alloc_arg)
- 如果不为NULL,则是一个函数指针,edge264_decode_NAL和edge264_free将调用(在同一线程上)以释放通过alloc_cb分配的缓冲区void * alloc_arg
- 传递给alloc_cb和free_cb的自定义值
int edge264_decode_NAL(dec, buf, end, non_blocking, free_cb, free_arg, next_NAL)
解码包含任何参数集或切片的单个NAL单元。
参数:
Edge264Decoder * dec
- 初始化的解码上下文const uint8_t * buf
- NAL单元的第一个字节(包含nal_unit_type)const uint8_t * end
- 缓冲区后面的第一个字节(最大缓冲区大小在32位上是2^31-1,在64位上是2^63-1)int non_blocking
- 如果当前线程有其他处理因此不能在此阻塞,则设置为1void (* free_cb)(void * free_arg, int ret)
- 当多线程时可能从另一个线程调用的回调,以发出解析结束信号并释放NAL缓冲区void * free_arg
- 将传递给free_cb的自定义值const uint8_t ** next_NAL
- 如果不为NULL且返回代码是0|ENOTSUP|EBADMSG,将接收指向Annex B流中下一个起始代码之后的下一个NAL单元的指针
返回代码:
- 0 成功
- ENOTSUP 不支持的流(解码可能继续但可能返回零帧)
- EBADMSG 无效流(解码可能继续但可能显示视觉伪影,如果您可以用另一个解码器检查流实际上是无瑕疵的,请考虑填写错误报告🙏)
- EINVAL 如果函数使用dec == NULL或dec->buf == NULL调用
- ENODATA 如果函数在dec->buf >= dec->end时调用
- ENOMEM 如果malloc未能分配内存
- ENOBUFS 如果应使用edge264_get_frame消耗更多帧以释放图片槽
- EWOULDBLOCK 如果非阻塞函数在图片槽可用之前必须等待
int edge264_get_frame(dec, out, borrow)
获取下一个准备输出的帧。
参数:
Edge264Decoder * dec
- 初始化的解码上下文Edge264Frame *out
- 将填充返回帧数据的结构int borrow
- 如果为0,则可以访问帧直到下一次调用edge264_decode_NAL,否则应使用edge264_return_frame显式返回帧。注意访问不是独占的,它可以同时用作其他帧的参考。
返回代码:
- 0 成功(返回一帧)
- EINVAL 如果函数使用dec == NULL或out == NULL调用
- ENOMSG 如果此时没有要输出的帧
虽然参考帧可能在实际显示之前被解码(例如B-Pyramid技术),但所有帧在显示之前都被缓冲以进行重新排序:
- 解码非参考帧会释放它和所有设置在其之前显示的所有帧。
- 解码关键帧会释放所有存储的帧(但不是关键帧本身,它可能稍后被重新排序)。
- 超过为重新排序保留的最大帧数会释放显示顺序中的下一帧。
- 缺少可用的帧缓冲区会释放显示顺序中的下一个非参考帧(以挽救其缓冲区)和在其之前显示的所有参考帧。
|
|
void edge264_return_frame(dec, return_arg)
如果帧是从先前对edge264_get_frame的调用中借用的,则归还其所有权。
参数:
Edge264Decoder * dec
- 初始化的解码上下文void * return_arg
- 存储在要返回的帧中的值
void edge264_flush(dec)
用于寻址时,停止所有后台处理,刷新所有延迟帧同时保持它们分配,并清除内部解码器状态。
参数:
Edge264Decoder * dec
- 初始化的解码上下文
void edge264_free(pdec)
释放整个解码上下文,并取消设置指针。
参数:
Edge264Decoder ** pdec
- 指向解码上下文的指针,无论是否初始化
路线图
- 压力测试(进行中)
- 多线程(进行中)
- 错误恢复(进行中)
- 集成到VLC/ffmpeg/GStreamer
- ARM32
- PAFF和MBAFF
- 4:0:0、4:2:2和4:4:4
- 9-14位深度,可能具有不同的亮度/色度深度
- 宏块QP==0的变换旁路
- SEI消息
- AVX-2优化
编程技术
我使用edge264来试验新的编程技术,以在现有解码器的基础上提高性能和代码大小,并在FOSDEM'24和FOSDEM'25上介绍了其中一些技术。
- 单头文件 - 包含所有结构定义、常见常量和枚举、SIMD别名、内联函数和宏,以及每个源文件的导出函数。要理解代码库,您应该首先查看此文件。
- 代码块而不是函数 - 主解码循环是一个前向流水线,设计为DAG,松散地类似于硬件解码器,节点是非内联函数,边是尾调用。它有助于在任何可能的地方互化代码分支,从而减少代码大小以帮助适应L1缓存。
- 树分支 - 方向性内部模式使用跳转表实现到树的叶子,然后无条件跳转到主干。它允许在方向模式之间共享底部代码,以减少代码大小。
- 全局上下文寄存器 - 当编译器(GCC)支持时,指向保存上下文数据的主结构的指针被分配给寄存器。随着Clang最终达到同等性能,放弃了这种技术,因此维护此黑客的动机很小。
- 默认邻居值 - 邻居可用性测试被替换为围绕每个帧的假邻居宏块。它减少了主解码循环中的条件测试数量,从而减少了代码大小和分支预测器压力。
- 相对邻居偏移 - 对左/上宏块值的访问是通过内存中的直接偏移完成的,而不是事先将其值复制到缓冲区。它有助于减少主解码循环中的读写。
- 解析不均匀块形状 - 每个使用mb_type和sub_mb_type指定的Inter宏块铺砌首先转换为位掩码,然后在设置位上迭代以获取正确数量的参考索引和运动向量。这有助于减少代码大小和条件块数量。
- 使用向量扩展 - GCC的向量扩展与向量内部函数一起使用,以编写更紧凑的代码。所有来自Intel的内部函数都使用较短的名称别名,这也提供了解码器中使用的所有SIMD指令的枚举。
- 寄存器饱和SIMD - 一些关键的SIMD算法使用比可用寄存器更多的同时向量,有效地饱和寄存器组并有目的地生成堆栈溢出。在某些情况下,这比将算法拆分为较小的位更有效,并且具有随着后续CPU良好扩展的额外好处。
- 活塞缓存比特流读取器 - 比特流位在size_t[2]中间缓存中读取,带有尾随设置位以跟踪缓存位数,允许从缓存中每读访问32/64位,并允许从内存中进行宽刷新。
- 即时SIMD转义移除 - 输入比特流使用向量代码即时取消转义,避免了完整的预处理过程以移除转义序列,从而减少了内存读写。
- 多架构SIMD编程 - 使用向量扩展以及别名内部函数允许支持Intel SSE和ARM NEON,具有约80%的公共代码和少量#if #else块,同时为两种架构保持最新性能。
- 数组结构模式 - 帧缓冲区存储为每个不同字段的数组,而不是结构数组,以使用位和向量运算符表达对帧的操作。多线程的任务缓冲区也部分依赖它。
- 延迟错误检查 - 错误检测在每个类型的NAL单元中执行一次,通过将所有输入值钳制到其预期范围,然后期望rbsp_trailing_bit之后(如果流损坏,捕获错误的概率非常高)。此设计选择在关于解析错误的案例中讨论。
其他尚未介绍的位:
- 具有FFI友好设计的极简API(7个函数和1个结构)。
- CAVLC和CABAC的比特流缓存存储在两个size_t变量中,将来可能映射到全局寄存器变量。
- 输入符号的解码与其解析交错(而不是解析到结构然后解码数据)。它去重了在解析和解码中都存在的分支和循环,甚至消除了存储某些符号的需要(例如mb_type、sub_mb_type、mb_qp_delta)。
测试(进行中)
借助使用edge264输出的相同YAML格式的自定义比特流写入器的帮助,正在tools/raw_tests中创建一组广泛的测试,以压力测试此解码器的最暗角落。下表列出了它们所有,以及实现它们的文件。
(测试表格内容由于篇幅限制,在此不完整翻译,但保持了原文的结构和分类)