FFmpegのアクセラレーションとして Intel PACを使ってみた。

## 概要
PACに画像処理をRTL実装し、FFmpegから、この機能を呼び出せるように ソフト側も実装し、実行するまでの過程を説明する。

## 開発環境準備
A10PAC が搭載されたマシン上で、[プログラマブル・アクセラレーション・カード (インテルArria 10 GX FPGA対応)のインテル・アクセラレーション・スタックのクイック・スタートガイド内の 2.4.2. ホストマシン上のインテルのアクセラレーション・スタックのDevelopmentパッケージのインストール](https://www.intel.com/content/www/jp/ja/programmable/documentation/iyu1522005567196.html#isp1528500494491) を参照してアクセラレーション・スタック1.2のDevelopmentパッケージをインストールします。

[上記と同じクイック・スタートガイド内にある 7.3. 仮想マシンでのHello FPGAサンプルの実行](https://www.intel.com/content/www/jp/ja/programmable/documentation/iyu1522005567196.html#rjf1522449114516)の Hello FPGAサンプルが実行できる環境にします。
具体的には、以下のコマンドを実行します。


```shell
source (inteldevstackインストールディレクトリ)/init_env.sh
sudo fpgaconf $OPAE_PLATFORM_ROOT/hw/samples/nlb_mode_0/bin/nlb_mode_0.gbs
sudo sh -c "echo 20 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages"
```

## PAC プラグインライブラリ作成
Hello FPGAサンプル(nlb_mode_0.gbs と hello_fpga.c) は、FPGAのループバックテストプログラムになっています。
このサンプルを改造して、FFmpegのプラグインライブラリを作成します。

まずは、hello_fpga.c を改造した hello_fpga_pac.c を作成し、ループバック機能をプラグインします。
hello_fpga.cのコードを以下の３関数に分けます。


|Function   |Description                            |
|-----------|---------------------------------------|
|init_pac   |ffmpegが起動時に一回呼ぶ初期設定関数。 |
|frame_pac  |ffmpegがFrame処理時に呼ぶ関数。        |
|close_pac  |ffmpegが終了前に一回呼ぶ関数。         |

```
frame_pac()は、以下の修正もしました。
- 複数回呼ばれることに対応。
- 入力、出力データの引数を追加。
- 出力データは、データサイズが変わる場合でもフレキシブルに対応できるようにコールバック関数で対応。
```


作成した、hello_fpga_pac.c は[こちら](https://github.com/ryo3ryo3/a10pac_plugin/blob/main/nlb_mode_0/sw/hello_fpga_pac.c)になります。


そして、以下のようにヘッダファイル[pacif.h](https://github.com/ryo3ryo3/a10pac_plugin/blob/main/local/include/pacif.h)も作成します。

```c
#ifndef PACIF_H
#define PACIF_H

#include <stdint.h>

int init_pac(void);
int frame_pac(const void** src, int frame_bytes, void (*callback)(void*,uint8_t*,int,int), void *pkt);
int close_pac(void);

#endif //#ifndef PACIF_H
```

ffmpegから呼び出せるように共有ライブラリ化します。

以下の環境変数を設定します。

例：

```shell
export INSTALL_PATH=$HOME/local
export PATH=$INSTALL_PATH/bin:$PATH
export LIBRARY_PATH=$INSTALL_PATH/lib:$LIBRARY_PATH
export LD_LIBRARY_PATH=$INSTALL_PATH/lib:$LD_LIBRARY_PATH
export C_INCLUDE_PATH=$INSTALL_PATH/include:$C_INCLUDE_PATH
export PKG_CONFIG_PATH+=$INSTALL_PATH/lib/pkgconfig:$PKG_CONFIG_PATH
```
その後、以下のコマンドを実行します。

```shell
mkdir $INSTALL_PATH/include/pactest
mkdir $INSTALL_PATH/lib
cp pacif.h $INSTALL_PATH/include/pactest
gcc -c -fstack-protector -fPIC -std=gnu99 ./hello_fpga_pac.c
gcc -shared hello_fpga_pac.o -o $INSTALL_PATH/lib/libpac.so -z noexecstack -z relro -z now -luuid -lpthread -ljson-c -lopae-c
```

これで、プラグインライブラリの
ヘッダファイル `$INSTALL_PATH/include/pactest/pacif.h` と
共有ライブラリ `$INSTALL_PATH/lib/libpac.so`
の準備が完了です。

## FFmpegからプラグインライブラリを呼び出す

次にffmpeg側をこのプラグインライブラリを呼び出せるように改造します。
任意のディレクトリで、以下のコマンドでffmpeg をGitHubから取得します。
（リビジョンは筆者が作業したものということ以上の意味はありません。）

```shell
git clone https://github.com/FFmpeg/FFmpeg.git
cd FFmpeg
git checkout 60b1f85b
```

筆者が慣れていることもあり、今回は libavcodec に仮のエンコーダを登録して、
そのエンコーダを実行時に指定した時、PACでアクセラレーションできるように改造します。

FFmpeg/libavcodec の下にlibpactest.c というファイルを作成し、
libpactest というエンコーダを作成します。
libpactest.c内でエンコーダの基本構成を以下のように作成しました。

```c
AVCodec ff_libpactest_encoder = {
    .name           = "pac_plugin",
    .long_name      = NULL_IF_CONFIG_SMALL("PAC Plug-in TEST"),
    .type           = AVMEDIA_TYPE_VIDEO,
    .id             = AV_CODEC_ID_NONE,
    .priv_data_size = sizeof(PACContext),
    .init           = pactest_init,
    .close          = pactest_close,
    .encode2        = pactest_frame,
    .pix_fmts       = (const enum AVPixelFormat[]){
                      AV_PIX_FMT_UYVY422,
                      AV_PIX_FMT_NONE},
    .capabilities   = AV_CODEC_CAP_AUTO_THREADS | AV_CODEC_CAP_DELAY,
    .caps_internal  = FF_CODEC_CAP_INIT_CLEANUP,
    .priv_class     = &pac_plugin_class,
};
```

ffmpeg実行時にこの（仮の）エンコーダを呼ぶために "pac_plugin" を指定するようにしました。
対応する入力フォーマットは、 "uyvy422"のみとしました。
以下のようなffmpegの実行コマンドを想定しています。

ffmpeg -s 1920x1080 -f rawvideo -pix_fmt uyvy422 -i input.yuv -vcodec pac_plugin output.yuv

また、以下の関数を作成しました。


|Function       |Description                            |
|---------------|---------------------------------------|
|pactest_init   |ffmpegが起動時に一回呼ぶ初期設定関数。<br>プラグインライブラリのinit_pac() を呼びます。|
|pactest_frame  |ffmpegがFrame処理時に呼ぶ関数。<br>プラグインライブラリのframe_pac()を呼びます。|
|pactest_close  |ffmpegが終了前に一回呼ぶ関数。<br>プラグインライブラリのclose_pac()を呼びます。|



作成した libpactest.c は[こちら](https://github.com/ryo3ryo3/a10pac_plugin/blob/main/FFmpeg/libavcodec/libpactest.c)になります。

また、libpactest をコンフィグ、コンパイルするために必要な修正を
行います。以下のファイルを修正します。

- FFmpeg/confirure
- FFmpeg/libavcodec/Makefile
- FFmpeg/libavcodec/allcodec.c

これら修正したファイルはこちらに置いたので参照して下さい。
https://github.com/ryo3ryo3/a10pac_plugin/tree/main/FFmpeg

あとは、FFmpeg/ に移動し、以下のコマンドで、コンフィグ、コンパイル、インストールを行います。

```shell
./configure --prefix=$INSTALL_PATH --enable-shared --enable-libpactest --enable-ffplay > configure.log 2>&1
make all
make install
```

これで、単純なループバックではありますが、ffmpeg からPACでのアクセラレーションできる環境が整いました。
実行テストします。
入力ファイルは、適当な動画ファイルから、以下のコマンドで作成して下さい。

```shell
ffmpeg -i (動画ファイル) -f rawvideo -pix_fmt uyvy422 -s 1920x1080 input.yuv
```

適当な動画ファイルがない場合は、以下のコマンドで作成してもいいです。

```shell
ffmpeg -f lavfi -i testsrc=size=1920x1080 -pix_fmt uyvy422 -frames 100 -f rawvideo input.yuv
```

以下のコマンド実行テストします。

```shell
ffmpeg -s 1920x1080 -f rawvideo -pix_fmt uyvy422 -i input.yuv -vcodec pac_plugin output.yuv
```

output.yuv を以下のffplayコマンド で再生して、input.yuv と同じなら成功です。

```shell
ffplay -video_size 1920x1080 -f rawvideo -pixel_format uyvy422 output.yuv
```

## PAC内のFPGAロジックに画像処理を追加する

単純なループバックですと、PAC内のロジックが呼ばれている実感が湧きません。
そこで、Hello FPGAサンプルの FPGAロジックに画像処理を追加して、nlb_mode_0.gbsを作り直します。
まずは以下のコマンドで合成環境を作ります。

```shell
cd $OPAE_PLATFORM_ROOT/hw/samples/nlb_mode_0
afu_synth_setup --source hw/rtl/filelist_mode_0.txt build_synth
```

`$OPAE_PLATFORM_ROOT/hw/samples/nlb_mode_0/hw/rtl/nlb_lpbk.sv`
にあるデータパスに、画像処理を追加します。

今回は、比較的簡単な左右反転処理を実装します。
更にシンプルにするため 入力画像は、横幅1920固定の仕様としています。
作成した回路は以下です。
uyvy422の1ラインのデータ数は 1920\*2 = 3840byte (1画素は Y と UV で 2byte)。
512bitのデータパスになっているので　(3840\*8)/512 = 60ワードの１ライン分のデータをバッファにためてから、逆順(LIFO)に出力しています。
(hello_fpga_pac.c内の転送単位 "LPBK1_BUFFER_SIZE" も60wordの倍数にする必要がありましたので、1024\*1024 から 1200\*1024に修正しています。)
データを出力している間も、バッファにためれるように２ライン分のバッファを用意しました。


```verilog
reg   [6:0]   r_wwordcnt;
wire  [6:0]   w_wwordcnt;
reg   [6:0]   r_rwordcnt;
wire  [6:0]   w_rwordcnt;
reg   [551:0] r_lbuf[0:119];
wire          w_wlastword = ( (r_wwordcnt[6:0]==7'd59) || (r_wwordcnt[6:0]==7'd119) ) ? 1'b1 : 1'b0;
wire          w_rlastword = ( (r_rwordcnt[6:0]==7'd0)  || (r_rwordcnt[6:0]==7'd60)  ) ? 1'b1 : 1'b0;
reg   [1:0]   r_wordstocknum;
wire  [1:0]   w_wordstocknum;
wire          w_wordstocken;
wire          w_wordstock_empty   = (r_wordstocknum[1:0] == 2'd0) ? 1'b1 : 1'b0;
wire          w_wordstock_full_p  = (r_wordstocknum[1:0] == 2'd1) ? ( (w_wlastword & w_wen) & ~(w_rlastword & w_ren) ) : 1'b0;
reg           r_wordstock_full;
wire          w_wordstock_almfull = ( (r_wordstocknum[1:0] == 2'd1) && ((r_wwordcnt[6:0]==7'd52) || (r_wwordcnt[6:0]==7'd112)) ) ? 1'b1 : 1'b0;
reg           r_wordstock_almfull;
wire          w_wen = ~r_wordstock_full & ab2re_WrEn;
wire          w_ren = ~w_wordstock_empty & re2ab_WrSent;
reg           r_lstwen_1z;

wire  [511:0] w_selword;
wire  [511:0] w_sendword;
wire          w_sendworden;

assign w_wwordcnt[6:0] = (r_wwordcnt[6:0]==7'd119) ? 7'd0 : (r_wwordcnt[6:0]+7'd1);
always @(posedge Clk_400) begin
    if (~test_SoftReset)  r_wwordcnt[6:0] <= 7'd0;
    else if(w_wen)        r_wwordcnt[6:0] <= w_wwordcnt[6:0];
end

assign w_rwordcnt[6:0] = (r_rwordcnt[6:0]==7'd0) ? 7'd119 : (r_rwordcnt[6:0]-7'd1);
always @(posedge Clk_400) begin
    if (~test_SoftReset)  r_rwordcnt[6:0] <= 7'd59;
    else if(w_ren)        r_rwordcnt[6:0] <= w_rwordcnt[6:0];
end

always @(posedge Clk_400) begin
    if (~test_SoftReset)   r_lstwen_1z <= 1'b0;
    else                   r_lstwen_1z <= (w_wlastword & w_wen);
end

assign w_wordstocknum[1:0] = ( r_lstwen_1z & ~(w_rlastword & w_ren) ) ? (r_wordstocknum[1:0] + 2'd1) :
                             (~r_lstwen_1z &  (w_rlastword & w_ren) ) ? (r_wordstocknum[1:0] - 2'd1) :
                                                                         r_wordstocknum[1:0];
assign w_wordstocken = r_lstwen_1z | (w_rlastword & w_ren);
always @(posedge Clk_400) begin
    if (~test_SoftReset)   r_wordstocknum[1:0] <= 2'd0;
    else if(w_wordstocken) r_wordstocknum[1:0] <= w_wordstocknum[1:0];
end

always @(posedge Clk_400) begin
    if (~test_SoftReset)         r_wordstock_almfull <= 1'b0;
    else if(w_rlastword & w_ren) r_wordstock_almfull <= 1'b0;
    else if(w_wordstock_almfull) r_wordstock_almfull <= 1'b1;
end

always @(posedge Clk_400) begin
    if (~test_SoftReset)         r_wordstock_full <= 1'b0;
    else if(w_rlastword & w_ren) r_wordstock_full <= 1'b0;
    else if(w_wordstock_full_p)  r_wordstock_full <= 1'b1;
end


always @(posedge Clk_400) begin
    if (w_wen) r_lbuf[r_wwordcnt[6:0]][551:0] <= {ab2re_WrAddr[19:0], ab2re_WrTID[15:0], ab2re_WrFence, ab2re_WrLen[1:0], ab2re_WrSop, ab2re_WrDin[511:0]};
end

assign w_selword[511:0]  = r_lbuf[r_rwordcnt[6:0]][511:0];
                           //   Y1                  V0                  Y0                  U0
assign w_sendword[511:0] = {
                                w_selword[ 15:  8], w_selword[ 23: 16], w_selword[ 31: 24], w_selword[  7:  0],
                                w_selword[ 47: 40], w_selword[ 55: 48], w_selword[ 63: 56], w_selword[ 39: 32],
                                w_selword[ 79: 72], w_selword[ 87: 80], w_selword[ 95: 88], w_selword[ 71: 64],
                                w_selword[111:104], w_selword[119:112], w_selword[127:120], w_selword[103: 96],
                                w_selword[143:136], w_selword[151:144], w_selword[159:152], w_selword[135:128],
                                w_selword[175:168], w_selword[183:176], w_selword[191:184], w_selword[167:160],
                                w_selword[207:200], w_selword[215:208], w_selword[223:216], w_selword[199:192],
                                w_selword[239:232], w_selword[247:240], w_selword[255:248], w_selword[231:224],
                                w_selword[271:264], w_selword[279:272], w_selword[287:280], w_selword[263:256],
                                w_selword[303:296], w_selword[311:304], w_selword[319:312], w_selword[295:288],
                                w_selword[335:328], w_selword[343:336], w_selword[351:344], w_selword[327:320],
                                w_selword[367:360], w_selword[375:368], w_selword[383:376], w_selword[359:352],
                                w_selword[399:392], w_selword[407:400], w_selword[415:408], w_selword[391:384],
                                w_selword[431:424], w_selword[439:432], w_selword[447:440], w_selword[423:416],
                                w_selword[463:456], w_selword[471:464], w_selword[479:472], w_selword[455:448],
                                w_selword[495:488], w_selword[503:496], w_selword[511:504], w_selword[487:480]
                            };
                             
assign w_sendworden      = w_ren;
wire [7:0]   w_addridx = (r_rwordcnt[6:0] < 7'd60) ? (8'd59 - {1'b0, r_rwordcnt[6:0]} ) : (8'd179 - {1'b0, r_rwordcnt[6:0]} );
wire [19:0]  w_WrAddr  = r_lbuf[w_addridx[6:0]][551:532];
wire [15:0]  w_WrTID   = r_lbuf[w_addridx[6:0]][531:516];
wire         w_WrFence = r_lbuf[w_addridx[6:0]][515];
wire [1:0]   w_WrLen   = r_lbuf[w_addridx[6:0]][514:513];
wire         w_WrSop   = r_lbuf[w_addridx[6:0]][512];
```

```
512bit のバスには、各8bitの UYVYデータ64個が以下の順番でパックされているので、
(MSB)                                                           (LSB)
Y31 V31 Y30 U30 Y29 V29 Y28 U28 ..... Y3  V3  Y2  U2  Y1  V1  Y0  U0
以下のように並べ替えを行っています。
Y0  V1  Y1  U0  Y2  V3  Y3  U2  ..... Y28 V29 Y29 U28 Y30 V31 Y31 U30
```

作成した、nlb_lpbk.sv は[こちら](https://github.com/ryo3ryo3/a10pac_plugin/blob/main/nlb_mode_0/hw/rtl/nlb_lpbk.sv)になります。

修正した回路を以下のコマンドで再合成します。

```shell
cd $OPAE_PLATFORM_ROOT/hw/samples/nlb_mode_0/build_synth
run.sh
```


出来上がった GBSファイルを書き込みます。

```shell
sudo fpgaconf $OPAE_PLATFORM_ROOT/hw/samples/nlb_mode_0/build_synth/nlb_400.gbs
```

以下の以前と同じ、ffmpegコマンドを実行します。

```shell
ffmpeg -s 1920x1080 -f rawvideo -pix_fmt uyvy422 -i input.yuv -vcodec pac_plugin output.yuv
```

以下のコマンドで映像を再生して、input.yuv と output.yuv が左右逆転していれば成功です。

```shell
ffplay -video_size 1920x1080 -f rawvideo -pixel_format uyvy422 input.yuv
ffplay -video_size 1920x1080 -f rawvideo -pixel_format uyvy422 output.yuv
```

(input.yuvの画像キャプチャ)
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/752281/f503f0e9-a5d2-025d-d73d-75d086e7d4ee.jpeg" width=50%>

(output.yuvの画像キャプチャ)
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/752281/640a67b1-0471-a833-3b03-6823361bea02.jpeg" width=50%>


## おわりに

今回行ったFPGAアクセラレーションは、画像処理の負荷、作成したFPGAの処理の能力を考えると、オフロードやアクセラレーション効果はありませんが、FPGAアクセラレーションを繋げるまでの作業手順がどなたかのお役に立てれば幸いです。

## References
- [プログラマブル・アクセラレーション・カード (インテルArria 10 GX FPGA対応)のインテル・アクセラレーション・スタックのクイック・スタートガイド](https://www.intel.co.jp/content/www/jp/ja/programmable/documentation/iyu1522005567196.html)