前回の記事minSDK=16でNDKビルドしても新しい端末ではAHardwareBuffer_xxxを使いたいんじゃぁ〜(その1)でminSDK=16でビルドしても端末がAPI>=26以降であればAHardwareBuffer_xxxを使えるように動的リンクするようにしてみました。
テスト条件
今回は、AHardwareBuffer_xxx関数を使ってどれぐらい描画処理が改善できるかのテストとその結果です。
AHardwareBufferが関係するのは、映像データをGPU側へ転送してテクスチャとして使えるようにする処理になります。
元々はglTexSubImage2Dで直接転送またはPBOとglTexSubImage2Dを使った非同期転送となっていましたが、そこへAHardwareBufferでの転送もできるように追加して、次の4通りを比べてみます。
- glTexSubImage2Dを使う場合
- glTexSubImage2DとPBOのピンポンバッファを使う場合
- glTexSubImage2Dの代わりにAHardwareBuffer_xxx関数を使う場合
- AHardwareBuffer_xxx関数を使ってMJPEGを直接テクスチャへ展開する場合
1つ目の普通にglTexSubImage2Dでセットする場合はこんなコード(MJPEG展開は別途実行)。
1 2 3 4 5 6 7 |
glTexSubImage2D(TEX_TARGET, 0, // ミップマップレベル 0, 0, // オフセットx,y mImageWidth, mImageHeight, // 上書きするサイズ PIXEL_FORMAT, // 引き渡すデータのフォーマット DATA_TYPE, // データの型 src); // ピクセルデータ |
2つ目のglTexSubImage2DとPBOのピンポンバッファを使ってセットするときはこんなコード(MJPEG展開は別途実行)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// PBOを使う時 const int write_ix = pbo_ix; const int next_ix = pbo_ix = (pbo_ix + 1) % 2; // PBOをバインド glBindBuffer(GL_PIXEL_UNPACK_BUFFER, mPBO[write_ix]); // Pboからテクスチャへコピー glTexSubImage2D(TEX_TARGET, 0, // ミップマップレベル 0, 0, // オフセットx,y mImageWidth, mImageHeight, // 上書きするサイズ PIXEL_FORMAT, // 引き渡すデータのフォーマット DATA_TYPE, // データの型 nullptr); // ピクセルデータとしてPBOを使う // 次のデータを書き込む glBindBuffer(GL_PIXEL_UNPACK_BUFFER, mPBO[next_ix]); glBufferData(GL_PIXEL_UNPACK_BUFFER, image_size, nullptr, GL_DYNAMIC_DRAW); auto dst = (uint8_t *)glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, image_size, GL_MAP_WRITE_BIT); if (LIKELY(dst)) { // write image data into PBO memcpy(dst, src, image_size); glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER); } glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); |
でもって、3つ目のAHardwareBufferを使うときのコード。 AAHardwareBuffer_lock / AAHardwareBuffer_unlock で囲って単純にメモリーコピーするだけです(この場合もMJPEG展開は別途実行)。
1 2 3 4 5 6 |
uint8_t *dst; AAHardwareBuffer_lock(graphicBuffer, AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN, -1, nullptr, (void**) &dst); { memcpy(dst, src, image_size); } AAHardwareBuffer_unlock(graphicBuffer, nullptr); |
ちなみに、AHardwareBuffer_xxx関数の前に「A」が余分に付いているのは間違いではありません。
前回の記事で書いたとおり、NDKr23以降対応でminSDK=16で動的リンクした場合とminSDK>=26でビルドした場合のソースコード互換性のためにエリアスとして「A」をプレフィックスしてあります。
4つ目は、同じくAHardwareBufferを使うのですが、MJPEG展開先のメモリーをハードウエアバッファーとする場合。この場合はなんとMJPEGを展開するだけでテクスチャへの転送が完了してしまいます😳
1 2 3 4 5 |
hwBuffer->lock(); { // テクスチャのバックバッファのハードウエアバッファ上へ直接mjpegを展開する result = converter.copy_to(frame_mjpeg, *hwBuffer, MJPEG_DECODE_TARGET); } hwBuffer->unlock(); |
PBOやAHardwareBufferを使う場合は自前で明示的にコピー処理が必要です。glTexSubImage2Dはたぶん内部で同じようにコピーしているのでしょう。
テストする端末は、Android12/API30のPixel3で、映っている映像内容によってMJPEGの展開に掛かる時間が変化しないようにテスト中に映像ができるだけ変化しないようにしてあります。
映像データは、MJPEG 1280×720@20fps(露出設定の都合で実際には18fpsぐらいで送られてきてました), カメラ側でのMJPEG圧縮時のサブサンプリングはYUV422で、MJPEG展開時にはYUV422Planarとして展開&テクスチャへ転送し、その後フラグメントシェーダーでYUV422PlanarをRGBXとして描画しています。
テスト結果
結果をどーん(^o^)/
コピー時間[ms] | MJPEG展開時間[ms] | 合計[ms] | |
---|---|---|---|
glTexSubImage2D | 1.1 | 14.6 | 15.7 |
glTexSubImage2D&PBO | 3.0 | 14.6 | 17.6 |
AHardwareBuffer | 1.2 | 14.6 | 15.8 |
AHardwareBuffer直接 | --- | 14.9 | 14.9 |
参考 UCCamera(libusb+libuvc) | 13.8 | 18.9 | 32.7 |
上3つはMJPEG展開処理とテクスチャへのコピー処理が別になっていますが、一番下のMJPEGの展開先をハードウエアバッファーにする場合はAHardwareBuffer_xxx関数呼び出しとMJPEG展開処理が一体になっていて分割できないので、こういう形の測定としました。
2つ目のglTexSubImage2DとPBOのピンポンバッファを使った場合の結果が振るいませんね。
Pixel3が比較的新しいハイエンドな端末なのでglTexSubImage2Dで直接テクスチャへセットしてもそこそこ高速に処理できているのでPBOを使う場合のオーバーヘッドの方が大きかったようです。GPUスペックの低いミドルレンジ以下の端末だと違った結果になったのかもしれません。
3つ目のglTexSubImage2Dの代わりにAHardwareBufferを使う場合は、 AAHardwareBuffer_lock / AAHardwareBuffer_unlock呼び出しのオーバーヘッドがあるせいかglTexSubImage2Dよりも少し遅い結果でした。
そして4つ目、AAHardwareBuffer_xxx関数を使ってMJPEGを直接テクスチャへ展開する場合は、前3つとは異なりメモリーコピーが1回少ないのでAAHardwareBuffer_xxx関数呼び出しのオーバーヘッドがあっても他より高速な結果でした。
今回の結果では約5%の性能向上でしたが、解像度等にもよりますがテストした範囲ではMJPEGデータをテクスチャへ転送する処理において5-10%程度の性能向上が見込めそうです。
MJPEG展開処理時間を除いたテクスチャへの転送処理としてみると、1.1msが0.3msになっているので3.7倍高速とも処理時間1/3とも言えるので結構大きな性能向上ですね。\nn
参考のためにいにしえのUVCCameraリポジトリ(libusb+libuvcのAndroid用に改変したもの)のサンプルアプリで同条件の測定をした結果も乗せています。
UVCCameraリポジトリのサンプルアプリ基準だとMJPEG展開込みでも処理時間半減、MJPEG展開抜きだと50倍ぐらい高速化できています(処理方法自体がそこそこ違うので直接的な比較にはなりませんが)
おまけ
MJPEG展開(デコード)する場合、特に何も考えずなければRGB565かRGBA/RGBXにしてしまうと思います。
しかしMJPEG映像を画面へ描画する場合には、これでは処理速度がでないのです。
というのもwebカメラ等のUVC機器から送られてくるmjpeg画像は、jpeg圧縮の際にサブサンプリングされてデータ量が削減されています。
具体的には、YUV422, YUV422, YUV420, YUV440, YUV411, Y8のいずれかですが、手持ちの機材ではYUV422が多く一部YUV420のようです。
ですので、出力フォーマットをRGB565かRGBA/RGBXにしてしまうと、JPEG展開と一口に言っても実際に複数の段階を経ているのです。
- jpeg圧縮自体の展開
- サブサンプリングされた画素から元の画素への逆サンプリングと色空間の変換
- (場合によっては)他の画像フォーマットへの変換処理
libjpeg/libjpeg-turboを使う場合、圧縮時のサブサンプリングそのままのPLANAR形式(例えばYUV422PLANAR)でMJPEG展開すると、2),3)の処理をスキップできますが、RGB565やRGBX/RGBAだと2)3)の処理が行われるためJPEG展開速度が遅くなるのです。先ほどのテスト結果がMJPEG展開部分だけを見ても25%ほど違うのは余分な逆サンプリングや色空間・画像フォーマットの変換が入っていたからなのです。
先ほどのテスト結果で参考にあげたUVCCamera(libusb+libuvc)でもlibjpeg-turboを使ってMJPEG展開しているのですが、当時の端末で平均的に高速な結果が得られたのが、libjpeg-turboでMJPEG → YCbCrへ展開した後YUY2へ変換してからRGB565で描画するというでした(メモリー容量が少ない&GPUもあまり速くない&CPU-CPU間/CPU⇔GPU間でもメモリーコピーがかなり遅かった&PLANAR形式よりもインターリーブ形式の方が速かったので)
おしまい
端末の性能向上の方が影響は大きいのですが、MJPEG展開方法だったり、描画方法だったりでもそこそこパフォーマンスが変わります。
最近の端末のCPUがマルチコア・高クロック・高性能になっているとはいえ、基本的な部分の処理負荷が高ければ追加で何か処理をしようとしても実用にならなかったりします。特に画像処理はCPU/GPU負荷が高いものがほとんどですので、こうやって継続的に地道に負荷を下げる努力・検証が必要なのです。儲からないけど(´・ω・`)