概要
PACに画像処理をRTL実装し、FFmpegから、この機能を呼び出せるように ソフト側も実装し、実行するまでの過程を説明する。
開発環境準備
A10PAC が搭載されたマシン上で、プログラマブル・アクセラレーション・カード (インテルArria 10 GX FPGA対応)のインテル・アクセラレーション・スタックのクイック・スタートガイド内の 2.4.2. ホストマシン上のインテルのアクセラレーション・スタックのDevelopmentパッケージのインストール を参照してアクセラレーション・スタック1.2のDevelopmentパッケージをインストールします。
上記と同じクイック・スタートガイド内にある 7.3. 仮想マシンでのHello FPGAサンプルの実行の Hello FPGAサンプルが実行できる環境にします。
具体的には、以下のコマンドを実行します。
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のコードを以下の3関数に分けます。
Function | Description |
---|---|
init_pac | ffmpegが起動時に一回呼ぶ初期設定関数。 |
frame_pac | ffmpegがFrame処理時に呼ぶ関数。 |
close_pac | ffmpegが終了前に一回呼ぶ関数。 |
frame_pac()は、以下の修正もしました。
- 複数回呼ばれることに対応。
- 入力、出力データの引数を追加。
- 出力データは、データサイズが変わる場合でもフレキシブルに対応できるようにコールバック関数で対応。
作成した、hello_fpga_pac.c はこちらになります。
そして、以下のようにヘッダファイルpacif.hも作成します。
#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から呼び出せるように共有ライブラリ化します。
以下の環境変数を設定します。
例:
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
その後、以下のコマンドを実行します。
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から取得します。
(リビジョンは筆者が作業したものということ以上の意味はありません。)
git clone https://github.com/FFmpeg/FFmpeg.git
cd FFmpeg
git checkout 60b1f85b
筆者が慣れていることもあり、今回は libavcodec に仮のエンコーダを登録して、
そのエンコーダを実行時に指定した時、PACでアクセラレーションできるように改造します。
FFmpeg/libavcodec の下にlibpactest.c というファイルを作成し、
libpactest というエンコーダを作成します。
libpactest.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が起動時に一回呼ぶ初期設定関数。 プラグインライブラリのinit_pac() を呼びます。 |
pactest_frame | ffmpegがFrame処理時に呼ぶ関数。 プラグインライブラリのframe_pac()を呼びます。 |
pactest_close | ffmpegが終了前に一回呼ぶ関数。 プラグインライブラリのclose_pac()を呼びます。 |
作成した libpactest.c はこちらになります。
また、libpactest をコンフィグ、コンパイルするために必要な修正を
行います。以下のファイルを修正します。
- FFmpeg/confirure
- FFmpeg/libavcodec/Makefile
- FFmpeg/libavcodec/allcodec.c
これら修正したファイルはこちらに置いたので参照して下さい。
https://github.com/ryo3ryo3/a10pac_plugin/tree/main/FFmpeg
あとは、FFmpeg/ に移動し、以下のコマンドで、コンフィグ、コンパイル、インストールを行います。
./configure --prefix=$INSTALL_PATH --enable-shared --enable-libpactest --enable-ffplay > configure.log 2>&1
make all
make install
これで、単純なループバックではありますが、ffmpeg からPACでのアクセラレーションできる環境が整いました。
実行テストします。
入力ファイルは、適当な動画ファイルから、以下のコマンドで作成して下さい。
ffmpeg -i (動画ファイル) -f rawvideo -pix_fmt uyvy422 -s 1920x1080 input.yuv
適当な動画ファイルがない場合は、以下のコマンドで作成してもいいです。
ffmpeg -f lavfi -i testsrc=size=1920x1080 -pix_fmt uyvy422 -frames 100 -f rawvideo input.yuv
以下のコマンド実行テストします。
ffmpeg -s 1920x1080 -f rawvideo -pix_fmt uyvy422 -i input.yuv -vcodec pac_plugin output.yuv
output.yuv を以下のffplayコマンド で再生して、input.yuv と同じなら成功です。
ffplay -video_size 1920x1080 -f rawvideo -pixel_format uyvy422 output.yuv
PAC内のFPGAロジックに画像処理を追加する
単純なループバックですと、PAC内のロジックが呼ばれている実感が湧きません。
そこで、Hello FPGAサンプルの FPGAロジックに画像処理を追加して、nlb_mode_0.gbsを作り直します。
まずは以下のコマンドで合成環境を作ります。
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ワードの1ライン分のデータをバッファにためてから、逆順(LIFO)に出力しています。
(hello_fpga_pac.c内の転送単位 "LPBK1_BUFFER_SIZE" も60wordの倍数にする必要がありましたので、1024*1024 から 1200*1024に修正しています。)
データを出力している間も、バッファにためれるように2ライン分のバッファを用意しました。
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 はこちらになります。
修正した回路を以下のコマンドで再合成します。
cd $OPAE_PLATFORM_ROOT/hw/samples/nlb_mode_0/build_synth
run.sh
出来上がった GBSファイルを書き込みます。
sudo fpgaconf $OPAE_PLATFORM_ROOT/hw/samples/nlb_mode_0/build_synth/nlb_400.gbs
以下の以前と同じ、ffmpegコマンドを実行します。
ffmpeg -s 1920x1080 -f rawvideo -pix_fmt uyvy422 -i input.yuv -vcodec pac_plugin output.yuv
以下のコマンドで映像を再生して、input.yuv と output.yuv が左右逆転していれば成功です。
ffplay -video_size 1920x1080 -f rawvideo -pixel_format uyvy422 input.yuv
ffplay -video_size 1920x1080 -f rawvideo -pixel_format uyvy422 output.yuv
おわりに
今回行ったFPGAアクセラレーションは、画像処理の負荷、作成したFPGAの処理の能力を考えると、オフロードやアクセラレーション効果はありませんが、FPGAアクセラレーションを繋げるまでの作業手順がどなたかのお役に立てれば幸いです。