はじめに
NvPipe は NVIDIA 製 GPU に搭載されたハードウェアエンコーダ(NVENC)およびデコーダ(NVDEC)機能を簡単に C 言語で使えるようにラップした NVIDIA 製のライブラリです。
NVENC および NVDEC は NVIDIA Video Codec SDK に含まれ、対応した GPU 下で CUDA コアから独立した専用のハードウェアでエンコード・デコードを行うことができます(= グラフィクスの描画に影響を与えずエンコード / デコードできる)。NvPipe では動画フォーマットは H.264 / HEVC、データは RGBA32と 4 / 8 / 16 / 32 bit のグレースケールのデータになります(NVENC / NVDEC 自体はもっと色々サポートしています)。
ユースケースとしては汎用的な圧縮シナリオやリモートレンダリング等に応用できます。今回はまず Unity で使えるように薄くラップしたものを作ってみましたので紹介です。
ダウンロード
まだラップしただけな感じで実用としてはもう一歩なので、もう少し練って次回の更新でパッケージ化しようと思います。試したい方は今は master を clone して実行してみてください。
動作
動画で見ても分かりづらいですが...、左がカメラを RenderTexture
に描画したもの、右がそれをエンコード・デコードして表示したものです。これが GPU 上の専用ハードウェアで行われている形です。
使い方
使い方の詳細は次回のアップデートでまとめる予定ですが、簡単に説明しますと、uNvPipeEncoder
および uNvPipeDecoder
がそれぞれエンコード・デコードを行う NvPipe のラッパーになっており、それぞれの API である Encode()
で与えられたバッファまたは Texture2D
をエンコード、Decode()
で与えられたバッファをデコードします。また、これらは onEncoded
/ onDecoded
のイベントを通じて監視できるようになっています。
サンプルシーンの流れとしては、以下のような形です。
uNvPipeRenderTextureEncoder
が指定されたRenderTexture
のバッファを取り出す- この RGBA バッファを
Encode()
を呼び出してエンコード - エンコードされたバッファを
EncoderToDecoder
のonEncoded
からDecode()
に流し込む - デコードされた RGBA バッファを
uNvPipeDecodedTexture
で CusomtTextureUpdate を通じて更新
3 のシナリオが順序保証付きの UDP などでやり取りするネットワークになればリモートレンダリングが可能な形です。4 については以下を参照してください。
解説
NvPipe
NvPipe は 8ce2d92 を以下の設定でビルドした DLL を利用しています。
NvPipe 自体はものすごく簡単です。例えばエンコーダ(送信側)は次のようになります。
auto* encoder = NvPipe_CreateEncoder( NVPIPE_RGBA32, NVPIPE_H264, NVPIPE_LOSSY, bitrateMbps * 1000 * 1000, targetFPS, width, height); for (;;) { ... // バッファ(rgbaBuffer)の準備 NvPipe_Encode( encoder, rgbaBuffer, rgbaBufferPitch, // width * 4 outputBuffer, outputBufferSize, width, height, forceIframe); ... // バッファの送信 } NvPipe_Destroy(encoder);
デコーダ(受信側)は次のようになります。
auto* decoder = NvPipe_CreateDecoder( NVPIPE_RGBA32, NVPIPE_H264, width, height); for (;;) { ... // バッファ受信 NvPipe_Decode( decoder, receivedData, receivedDataSize, rgbaBuffer, width, height); ... // rgbaBuffer をテクスチャへ反映 } NvPipe_Destroy(decoder);
この処理を C++ でラップして DLL にし、Unity 側から使えるようにしています。
Unity での処理と問題点
サンプルでは、RenderTexture
にカメラ画をレンダリングして、それをとても非効率ですが簡単なので ReadPixels()
および GetPixels()
で読んできています。
// RenderTexture -> Texture2D への変換 var activeRenderTexture = RenderTexture.active; RenderTexture.active = texture; var area = new Rect(0f, 0f, texture2d_.width, texture2d_.height); texture2d_.ReadPixels(area, 0, 0); texture2d_.Apply(); RenderTexture.active = activeRenderTexture; // RGBA バッファの読み取り var pixels = texture.GetPixels32(); var handle = GCHandle.Alloc(pixels, GCHandleType.Pinned); var pointer = handle.AddrOfPinnedObject(); // DLL へ処理を投げる await Encode(pointer, forceIframe); handle.Free();
しかしながらこの ReadPixels()
も GetPixels()
も GPU と CPU とのやり取りがある関係でとても重い処理になっています。また、メインスレッドのみで可能な処理なため非同期化も出来ません。NvPipe ではエンコードの入力データの受付が RGBA / グレースケールのバッファのポインタしかないのでどうしても高速化がそのままでは難しいです。しかしながら NVENC を使うとなんと ID3D11Texture2D
をそのまま突っ込めるので、Texture2D.GetNativeTexturePtr()
などで取ってきたテクスチャを CPU を介さずに流し込むことが出来ます。こちらは次回の内容になります。
おわりに
次回は NVENC の利用による高速化と、リモートへの転送についての記事を書きます。