记一次从C语言到C++的迁移——初次接触VapourSynth API

引言

想在VapourSynth中实现Jinc(EWA Lanczos)放大算法,已经有一份实现,https://github.com/Lypheo/EWA-Resampling-VS 。但这份代码仅支持整型16bit输入(严格来说是仅支持整型9~16bit输入,作者说仅支持16bit RGB输入,但无论看代码还是实际测试,16bit YUV和10bit YUV都是支持的)。

所以我想在原作者代码的基础上,先完善上述输入问题。至少要支持整型8bit YUV输入,后续再考虑支持32bit浮点数输入。

阅读了原作者代码和其他一些较为成熟的基于VapourSynth实现的代码,大致明白了VapourSynth API调用的过程和基础的代码结构。至少对于Jinc算法,在整型8-16bit的范围内,支持的输入类型仅取决于关键变量的类型定义(这话看起来是一句正确的废话,要怪就怪C++是一门强类型语言Orz)。

再具体一点,支持8bit需要使用uint8_t来定义关键变量,支持9~16bit需要使用unit16_t来定义关键变量。所以要在代码中,根据输入视频的位深进行类型转换。

在C语言中,我能想到的就是使用宏来完成上述过程。但是复杂一点宏我就不会写…而且宏也不安全…我还是用C++的模板来写吧。看了一下不同前辈们的代码,也都是用模板来写的,方便Copy

准备工作

不着急写模板,从C到C++,代码还是要改一下的。

  1. 在C++中,使用uint8_t等类型需要include<cstdint>头文件。

  2. 在C++使用圆周率M_PI的宏,要include相应的数学头文件,随后又带来了新的问题,所以直接定义一个宏吧。

  3. 内存控制方面,在C++中使用malloc()分配内存,需要进行强制类型转换。具体到这份代码,将

    1
    data = malloc(sizeof(d));

    修改为

    1
    data = (FilterData *)malloc(sizeof(d));

    其中,FilterData是自定义类型。

理解VapourSynth API和VapourSynth插件的基础代码

理解VapourSynth API

解决了基础的代码语法问题,还需要理解VapourSynth API,起码要明白怎么读写视频、怎么获取视频格式信息、视频处理前后储存在什么变量中、这些变量是怎么声明的。

借助VapourSynth API,上述问题的解答如下。

  1. 视频的读取写入通过vsapi->getReadPtrvsapi->getWritePtrvsapi->newVideoFrame实现。
  2. 视频格式信息通过d->vi->format获取,具体信息对应具体函数,如获取位深使用bytesPerSample;视频尺寸信息通过vsapi->getFrameHeightvsapi->getFramewidth获取。
  3. 储存视频的变量类型是VSFrameRef*(通过指针储存),分平面储存;输入视频要加上const限制。
  4. 好像3已经回答了变量声明的问题。
  5. 顺带一提,读取用户端输入的函数变量通过类似vsapi->propGetFloat的方式完成。

VapourSynth的Hello World

写一个VapourSynth插件,或者说写一个基于VapourSynth框架的算法实现,要包含这么几个部分。

1
2
3
4
5
6
7
8
9
10
11
// 代码主体
void VS_CC filterInit(VSMap* in, VSMap* out, void** instanceData, VSNode* node, VSCore* core, const VSAPI* vsapi) {}

const VSFrameRef* VS_CC filterGetFrame(int n, int activationReason, void** instanceData, void** frameData, VSFrameContext* frameCtx, VSCore* core, const VSAPI* vsapi) {}

void VS_CC filterFree(void* instanceData, VSCore* core, const VSAPI* vsapi) {}

void VS_CC filterCreate(const VSMap* in, VSMap* out, void* userData, VSCore* core, const VSAPI* vsapi) {}

// Python接口
VS_EXTERNAL_API(void) VapourSynthPluginInit(VSConfigPlugin configFunc, VSRegisterFunction registerFunc, VSPlugin* plugin) {}

代码主体的四个函数中,filterInit()filterFree()是辅助性质的函数,主要代码在filterGetFrame()filterCreate()中。其中filterCreat()多是用来获取用户端的输入参数,所以实现算法的核心代码一般位于filterGetFrame()中。

写模板

Coding过程

语法和API调用都搞明白了,下面开始写模板。具体需求在引言部分已经写清楚了。现在回到原作者的代码上,寻找鲁棒性缺失的地方。

容易发现,在GetFrame部分,直接使用uint8_tuint16_t声明了储存视频的变量。

1
2
const uint8_t *scrp = vsapi->getReadPtr(frame, plane);
uint8_t *targetp = vsapi->getWritePtr(dst, plane);
1
2
uint16_t *framep = (uint16_t *) scrp;
uint16_t *dstp = (uint16_t *) targetp;

这就是原代码仅支持9~16bit的原因(之一),要用函数模板替代上述直接声明。

当年上C++课的时候,老师说模板不考…所以在教材目录上画个叉,模板的学习到此为止…写模板倒容易写,像定义函数那样照猫画虎。但怎么调用模板,花了一段时间才弄明白。

实现引言中的需求,逻辑很简单,伪代码如下。

1
2
3
4
5
6
7
8
9
// 定义模板
template<typename T>
void bitdepth() {}

// 根据输入视频位深,使用不同的类型定义变量
if (depth <= 8)
bitdepth<uint8_t>();
else
bitdepth<uint16_t>();

我之所以不知道怎么调用函数模板,是因为我一开始不想改动太多的原始代码。原始代码用uint16_t直接声明变量,我也想借助函数模板直接做类似的事情。于是,我的代码写成了下面这个样子。

1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename T>
static const T* process1( ... ) {
const T* framep = reinterpret_cast<const T*>(vsapi->getReadPtr(src, plane));
return framep;
}

if (d->vi->format->bytesPerSample == 1) {
const uint8_t *scrp = process1<uint8_t>( ... );
uint8_t *framep = (uint8_t *) scrp;
}
else {
...
}

我想当然的以为通过if语句,就可以让代码根据视频输入格式选择变量类型,但我忘了一件事情…if语句内的变量是局部变量,作用域仅限if语句内…

好吧…那我把后面的代码分别往if和else内复制一遍,直接在if语句内处理到底…这也太蠢了

坦白地讲,我心里很清楚,这就是半路出家自学和科班的差距。

看了前辈的代码,将函数模板写成void函数,在GetFrame部分仅进行函数模板的调用,处理的代码均写在函数模板内。把该写的框架都写全,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
template<typename T>
static void process(const VSFrameRef* frame, VSFrameRef* dst, const FilterData* const VS_RESTRICT d, const VSAPI* vsapi) noexcept {
for (int plane = 0; plane < d->vi-format->numPlanes; plane++) {
// 储存读取写入视频的变量
const T* framep = reinterpret_cast<const T*>(vsapi->getReadPtr(frame, plane));
T* VS_RESTRICT dstp = reinterpret_cast<T*>(vsapi->getWritePtr(dst, plane));

// 原始代码是除以2,若以8bit输入,应当是除以1,所以用sizeof(T)代替具体数字
int frame_stride = vsapi->getStride(frame, plane) / sizeof(T);
int dst_stride = vsapi->getStride(dst, plane) / sizeof(T);

// Jinc算法实现部分
...
}
}

static const VSFrameRef* VS_CC filterGetFrame(int n, int activationReason, void** instanceData, void** frameData, VSFrameContext* frameCtx, VSCore* core, const VSAPI* vsapi) {
if (activationReason == arInitial) {
vsapi=>requestFrameFilter(n, d->node, frameCtx);
}
else if (activationReason == arAllFramesReady) {
const VSFormat* fi = d->vi->format;

const VSFrameRef* frame = vsapi->getFrameFilter(n, d->node, frame, core);
VSFrameRef* dst = vsapi->newVideoFrame(fi, d->w, d-h, frame, core);
// w和h是在filterCreat部分获取的用户端参数,指用户设定的放大后的视频尺寸。

if (fi->bytesPerSample == 1)
process<uint8_t>(frame, dst, d, vsapi);
else
prcess<uint16_t>(frame, dst, d, vsapi);
// 更鲁棒的写法,可以支持浮点数视频输入
// 但我前面的函数模板还没有支持浮点数,且一般用户也很少使用到浮点数视频,所以上述代码堪用
// else if (fi->bytesPerSample == 2)
// process<uint16_t>(frame, dst, d, vsapi);
// else
// process<float>(frame, dst, d, vsapi);

vsapi->freeFrame(frame); // 释放内存
return dst;
}

return 0;
}

至此,完成了模板的撰写(及小小的代码重构)和调用,测试一下,8bit、10bit、16bit YUV格式输入都没有问题。

值得注意的基础知识(2019.12.18)

上面这个模板是从前辈的一处代码中照猫画猫得到的(当时,模板函数的声明应该是copy过来的,改了一下,没太动脑子)(照着模板写模板,禁止套娃),有些细节没注意,或者现在忘了,来记一下。

noexcept关键字与异常处理、编译优化

在模板函数的函数声明和函数体之间(上述代码的第2行),有这么一个关键字,noexcept

noexcept是C++11引入的新特性(我居然能注意不同的C++标准了,以前觉得这是要学很久才能去学的东西Orz)。根据这篇博文,C++11 带来的新特性 (3)—— 关键字noexceptnoexcept会告诉编译器,使用了noexcept的函数不会发生异常。由于异常处理是在运行而非编译时执行,所以异常处理会影响编译器的优化,通过noexcept这么一“声明”,便于编译器做更多的优化。

顺带一提,在引入noexcept关键字之前,是使用throw()来实现类似功能。

博文中还有更深入的内容,以后还值得拿出来再看。

两个const

我第一次注意到const FilterData* const VS_RESTRICT d这样的声明,有点惊呆了,怎么有两个const

总结

总结一下,这篇博文的标题是从C语言到C++的迁移。就我目前的感受,C语言和C++的区别体现在C++有模板,而且对类型转换更加严格。

另外,文章用相当长的篇幅描述了VapourSynth的C++ API,我搞明白这些东西花了挺长一段时间,即使只算“开窍”后有明显进步的时间,也有一个多星期。如果现在让我从头用VapourSynth API写一个插件,照猫画虎应该没什么大问题,而且通过这段时间对C++的语法也有了更多的了解。

但另一方面,对于算法实现,这个真要花更多时间。我之前只知道Jinc的数学公式,知道怎样“手动推演”实现视频放大,但完全不知道在代码实现时,还有反向映射这一回事。在代码实现中,除了定义Jinc函数的部分,其余都是简单的加减乘除四则运算。关于8bit、16bit位深处理的地方,也没有涉及数学运算,区别只是变量的声明。这些东西,都是从算法到代码要学习的东西。同时,代码的异常处理、鲁棒性,也真实地接触到了。

之后的工作,具体到这份代码,就是实现taps功能。这份代码之外,我希望能够从头实现一个算法。

后记

关于编译后运行速度问题的。

关于鲁棒性,能否接受灰度视频/单平面视频输入。

C++与C头文件的区别,使用std库是否能提高速度,内存分配时的类型转换是否是降低速度的主因?

代码重构的预备

测试了半个下午,想解决dll运行慢的问题。(我实在不能理解也无法接受作者提供的dll速度是1s/帧,到我这就是9s/帧)。

  • 不是编译器配置的锅,我自行编译了Deblock,速度很快,虽然我的编译器配置与原作者的并不完全相同。
  • 又重复了一次,修改预处理后,除了malloc()函数部分,其他与作者代码保持一致,仍然是9s/帧的速度。不是我添加了函数模板导致的速度变慢
  • 换用gcc编译器…一时半会还没折腾好,先不用了..
  • 我现在真的是无法理解,作者的data = malloc(sizeof(d));是怎么编译通过的,我目前查到的所有资料,都说在malloc()函数前需要声明变量类型。
  • 我现在还担心,等我代码重构了,还是慢怎么办…