凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

NvPipe で Unity のテクスチャを H.264 にハードウェアエンコード / デコードしてみる

はじめに

NvPipeNVIDIAGPU に搭載されたハードウェアエンコーダ(NVENC)およびデコーダNVDEC)機能を簡単に C 言語で使えるようにラップした NVIDIA 製のライブラリです。

github.com

NVENC および NVDEC は NVIDIA Video Codec SDK に含まれ、対応した GPU 下で CUDA コアから独立した専用のハードウェアでエンコード・デコードを行うことができます(= グラフィクスの描画に影響を与えずエンコード / デコードできる)。NvPipe では動画フォーマットは H.264 / HEVC、データは RGBA32と 4 / 8 / 16 / 32 bit のグレースケールのデータになります(NVENC / NVDEC 自体はもっと色々サポートしています)。

developer.nvidia.com

ユースケースとしては汎用的な圧縮シナリオやリモートレンダリング等に応用できます。今回はまず Unity で使えるように薄くラップしたものを作ってみましたので紹介です。

ダウンロード

github.com

まだラップしただけな感じで実用としてはもう一歩なので、もう少し練って次回の更新でパッケージ化しようと思います。試したい方は今は master を clone して実行してみてください。

動作

動画で見ても分かりづらいですが...、左がカメラを RenderTexture に描画したもの、右がそれをエンコード・デコードして表示したものです。これが GPU 上の専用ハードウェアで行われている形です。

使い方

使い方の詳細は次回のアップデートでまとめる予定ですが、簡単に説明しますと、uNvPipeEncoder および uNvPipeDecoder がそれぞれエンコード・デコードを行う NvPipe のラッパーになっており、それぞれの API である Encode() で与えられたバッファまたは Texture2DエンコードDecode() で与えられたバッファをデコードします。また、これらは onEncoded / onDecoded のイベントを通じて監視できるようになっています。

f:id:hecomi:20190730010451p:plain

f:id:hecomi:20190730010508p:plain

サンプルシーンの流れとしては、以下のような形です。

  1. uNvPipeRenderTextureEncoder が指定された RenderTexture のバッファを取り出す
  2. この RGBA バッファを Encode() を呼び出してエンコード
  3. エンコードされたバッファを EncoderToDecoderonEncoded から Decode() に流し込む
  4. デコードされた RGBA バッファを uNvPipeDecodedTexture で CusomtTextureUpdate を通じて更新

3 のシナリオが順序保証付きの UDP などでやり取りするネットワークになればリモートレンダリングが可能な形です。4 については以下を参照してください。

tips.hecomi.com

解説

NvPipe

NvPipe は 8ce2d92 を以下の設定でビルドした DLL を利用しています。

f:id:hecomi:20190729221735p:plain

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 の利用による高速化と、リモートへの転送についての記事を書きます。