Vulkanを試してみた

2016/06/02
このエントリーをはてなブックマークに追加


はじめに

カブクの甘いもの担当、高橋憲一です。

エンジニアとしての担当領域はサーバーサイドで動作する3Dモデルデータの解析/レンダリングエンジンの開発です。
rinkakやMMSではユーザーの皆さんにアップロードして頂いた3Dモデルデータをプレビュー表示する機能があるのですが、その画像のレンダリングは mesa という OpenGL 互換のライブラリを使って開発したエンジンでサーバーサイドでオフスクリーンレンダリングをしています。で、今回は OpenGL ではなく、新しいグラフィクスAPIである Vulkan を Android N で試しみた話をしたいと思います。

Vulkanとは

AppleのMetalや、AMDのMantle、MicrosoftのDirect X12など、最近のモダンな3D グラフィクスAPIの潮流はドライバ層を薄くしてオーバーヘッドを減らすというものです。前述のOpenGLはこれまで長く、そして広く使われてきた3DグラフィクスAPIで、現在はKhronosという団体が管理と策定をしています。VulkanはそのKhronosが満を持して出してきた「モダンな」APIです。OpenGLが誕生したのは1992年、2003年にはモバイル機器などの組み込み用にOpenGL ESが出たりと何度かの変化を経て今に至りますが、最も大きな変化はシェーダーが導入されて、それまでの固定パイプラインがプログラマブルになった時かと思います。

私がOpenGLを初めて使ったのはちょうど20年前のことで(mesaもその当時からありました。それもまた凄いことかと)、最初に触った当時は、


glBegin(GL_TRIANGLES);   /* 三角形群を描画する指定 */
glVertex3f(x0, y0, z0);  /* 頂点 1 */
glVertex3f(x1, y1, z1);  /* 頂点 2 */
glVertex3f(x2, y2, z2);  /* 頂点 3 */
glEnd();

というようにして頂点ごとに関数を呼んで x, y, z の座標を指定するというスタイルでした。やがてVertex Bufferが導入されて頂点データをGPUに一度に転送するようになり、それをVBO (Vertex Buffer Object)としてオブジェクトをIDで管理して何度も頂点データをロードしなくても良くなったり、頂点毎、ピクセル毎の処理をシェーダーで記述するようになったり…といった変化がこれまでありました。(カブクのサービスであるrinkakやMMSのプレビュー画面で床面に影を落とすドロップシャドウの処理でもシェーダーを活用しています)

…と、老害的な話はこれくらいにしてVulkanの話に入りたいと思います。

開発環境のセットアップ

GoogleのNDKガイドのVulkan Setupにも載っているように、AndroidでVulkanを試すにはハードウェアとしてNexus 5X, Nexus 6P, Nexus Player のいずれかが必要で、Android NのDeveloper Preview 2以降をインストールする必要があります。
開発環境は、

  • Android Studio (バージョンは 2.1 以上)
  • NDK r12 以上 (現時点ではVulkanを使うにはCもしくはC++で実装する必要があります)

が必要です。

まずはTeapotから…

3Dグラフィクスといえば、まずはTeapotの描画からです。その割にはTeapotを使ったVulkanのサンプルは見当たりませんので肩慣らしにやってみました。描画結果はトップ画像の通りです。
ではコードの中を見ていきましょう。
(以降のコードはGoogleから出ているサンプルや、Khronosから出ているリファレンスを参考に、Teapotのデータをロードしてタップ操作で回せるように実装してみたものの断片です)

Vertexのロード

GPU側のメモリを割り当て、アプリケーション側からアクセスできるようにするためにメインメモリ空間のアドレスにマッピング、マッピングされたアドレスに頂点座標データを書き込む、という処理を行います。このようにVulkanのAPIの呼び出しは必要な構造体のメンバに値を設定して、その構造体を渡して何らかの結果を得るというものが多いです。


uint32_t dataSize = vertexDataSize + normalDataSize;
VkBufferCreateInfo buf_info{
        .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
        .pNext = NULL,
        .usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
        .size = dataSize,       // 割当サイズを指定
        .queueFamilyIndexCount = 0,
        .pQueueFamilyIndices = NULL,
        .sharingMode = VK_SHARING_MODE_EXCLUSIVE,
        .flags = 0,
};
VkResult res = vkCreateBuffer(device_, &buf_info, NULL, &vertex_buffer.buf);

VkMemoryRequirements mem_reqs;
vkGetBufferMemoryRequirements(device_, vertex_buffer.buf,
                              &mem_reqs);

VkMemoryAllocateInfo alloc_info = {};
alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
alloc_info.pNext = NULL;
alloc_info.memoryTypeIndex = 0;

alloc_info.allocationSize = mem_reqs.size;
pass = memory_type_from_properties(mem_reqs.memoryTypeBits,
                                   VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
                                   VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
                                   &alloc_info.memoryTypeIndex);
// GPU側メモリの割り当て
res = vkAllocateMemory(device_, &alloc_info, NULL,
                       &(vertex_buffer.mem));

vertex_buffer.buffer_info.range = mem_reqs.size;
vertex_buffer.buffer_info.offset = 0;

uint8_t *pData;
// メインメモリのアドレス空間へのマッピング
res = vkMapMemory(device_, vertex_buffer.mem, 0, mem_reqs.size, 0,
                  (void **)&pData);

const float *vData = vertexData;  // 頂点座標 (x0, y0, z0, x1, y1, z1, ...)
const float *nData = normalData;  // 法線ベクトル (nx0, ny0, nz0, ny1, ny1, ny2, ...)
float *vBuf = (float *)pData;
uint32_t elementNum = vertexDataSize / (sizeof(float) * 3);
// GPU側のメモリにコピー
for (int i = 0; i < elementNum; i++) {
    // vertex
    *vBuf++ = *vData++;
    *vBuf++ = *vData++;
    *vBuf++ = *vData++;
    // normal
    *vBuf++ = *nData++;
    *vBuf++ = *nData++;
    *vBuf++ = *nData++;
}

vkUnmapMemory(device_, vertex_buffer.mem);
res = vkBindBufferMemory(device_, vertex_buffer.buf, vertex_buffer.mem, 0);

この辺りは自分でOpenGLのVBO (Vertex Buffer Object)の機能を実装しているかのような感覚があります。
同様にして三角形を定義する頂点データへのインデックスも usage = VK_BUFFER_USAGE_INDEX_BUFFER_BIT を設定してロードします。

パイプラインの生成とシェーダー

三角形のリストを描画する指定と、頂点を処理するバーテックスシェーダー、およびピクセルを処理するフラグメントシェーダーを関連付けてパイプラインを生成します。(シェーダーをロードするコードはここでは省略しています)


VkPipelineInputAssemblyStateCreateInfo ia{  // パイプラインインプットアセンブリ *1
        .sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
        .pNext = NULL,
        .flags = 0,
        .primitiveRestartEnable = VK_FALSE,
        .topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, // 三角形のリストの指定
};

// パイプラインの生成とシェーダーの関連付け
VkPipelineShaderStageCreateInfo shaderStages[2] { // シェーダーステージ *2
        {
                .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
                .stage = VK_SHADER_STAGE_VERTEX_BIT,
                .module = vertexShader,  // バーテックスシェーダー
                .pSpecializationInfo = nullptr,
                .pName = "main",
        },
        {
                .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
                .stage = VK_SHADER_STAGE_FRAGMENT_BIT,
                .module = fragmentShader,  // フラグメントシェーダー
                .pName = "main",
        }
};

VkGraphicsPipelineCreateInfo pipelineInfo{
        .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
        .pNext = NULL,
        .layout = pipelineLayout,
        .basePipelineHandle = VK_NULL_HANDLE,
        .basePipelineIndex = 0,
        .flags = 0,
        .pVertexInputState = &vi,
        .pInputAssemblyState = &ia, // パイプラインインプットアセンブリ *1
        .pRasterizationState = &rs,
        .pColorBlendState = &cb,
        .pTessellationState = NULL,
        .pMultisampleState = &ms,
        .pDynamicState = &dynamicState,
        .pViewportState = &vp,
        .pDepthStencilState = &ds,
        .pStages = shaderStages, // シェーダーステージ *2
        .stageCount = 2,
        .renderPass = render_pass,
        .subpass = 0,
};
// パイプラインの生成
res = vkCreateGraphicsPipelines(device_, pipelineCache, 1,
                                &pipelineInfo, NULL, &pipeline);

コマンドバッファ

パイプラインと関連付けて、ロードした頂点データを使用して描画するための定義をするのがコマンドバッファです。


VkRenderPassBeginInfo rp_begin{
        .sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
        .pNext = NULL,
        .renderPass = render_pass,
        .framebuffer = framebuffers[i],
        .renderArea.offset.x = 0,
        .renderArea.offset.y = 0,
        .renderArea.extent.width = width,
        .renderArea.extent.height = height,
        .clearValueCount = 2,
        .pClearValues = clear_values,
};

vkCmdBeginRenderPass(cmdBuffer[i], &rp_begin, VK_SUBPASS_CONTENTS_INLINE);

// パイプラインとコマンドバッファの関連付け
vkCmdBindPipeline(cmdBuffer[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
vkCmdBindDescriptorSets(cmdBuffer[i], VK_PIPELINE_BIND_POINT_GRAPHICS,
                        pipelineLayout, 0, NUM_DESCRIPTOR_SETS,
                        desc_set.data(), 0, NULL);

const VkDeviceSize offsets[1] = {0};
// 頂点バッファの指定
vkCmdBindVertexBuffers(cmdBuffer[i], 0, 1, &vertex_buffer.buf, offsets);
// インデックスバッファの指定
vkCmdBindIndexBuffer(cmdBuffer[i], indexBuf, offsets[0], VK_INDEX_TYPE_UINT16);

// インデックスを使用した描画(drawElementNumで要素数を指定)
vkCmdDrawIndexed(cmdBuffer[i], drawElementNum, drawInstanceNum, 0, 0, 0);
vkCmdEndRenderPass(cmdBuffer[i]);

VkImageMemoryBarrier prePresentBarrier {
        .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
        .pNext = NULL,
        .srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
        .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT,
        .oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
        .newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
        .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
        .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
        .subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
        .subresourceRange.baseMipLevel = 0,
        .subresourceRange.levelCount = 1,
        .subresourceRange.baseArrayLayer = 0,
        .subresourceRange.layerCount = 1,
        .image = buffers[i].image,
};
vkCmdPipelineBarrier(cmdBuffer[i], VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
                     VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, NULL, 0,
                     NULL, 1, &prePresentBarrier);

res = vkEndCommandBuffer(cmdBuffer[i]);

補足として、マルチスレッド環境での最適化が不得意だったOpenGLと異なり、このコマンドバッファをスレッドごとに生成して処理ができるということもVulkanの大きな利点の一つです。ARMのMALIのデベロッパー向けサイトにマルチスレッドを活用した良いサンプルがあります。

雑感

teapotを描画するために如何にたくさんのコードを書く必要があるかということを示す形(これでまだ3分の1くらいです)になってしまいましたが、ドライバの層が薄くなったということは、これまでドライバがやってくれていた処理をアプリケーション側で実装するということなのだと改めて実感した次第です。これはその分だけ、メモリの割り当てや描画命令の実行タイミングなどの細かい制御が可能になっているということになるので、頑張ればGPUの持つパフォーマンスを限界まで絞り出せるということです。

最後に

SGIのグラフィクスワークステーションに始まり、ケータイ、そしてAndroidやiPhoneと、プラットフォームは変わりながらもOpenGLとのつきあいが長い私としては、その新世代版と言えるVulkanを試さずにはいられず書いたのがこのブログ記事です。せっかくパフォーマンスを上げるための新しいAPIなのですから、Teapotを一つ表示して回しておしまいではなく、その限界を見てみたいのと、OpenGL ESとはどのくらい差がでるのか試してみたいと思っています。(なんて曖昧な言い方をしていると、マスター・ヨーダに "Do or do not. There is no try." と怒られそうなので「次の自分の番でやります」)
そして一番言いたいことは、カブクはそんなZ軸ジャンキーなエンジニアも活躍できる場所だということです 😉

その他の記事

Other Articles

2019/11/06
GraphQLの入門書を翻訳しました

2019/09/20
Kabuku Connect 即時見積機能のバックエンド開発

2019/08/14
Maker Faire Tokyo 2019でARゲームを出展しました

2019/07/25
夏休みだョ!WebAssembly Proposal全員集合!!

2019/07/08
鵜呑みにしないで! —— 書籍『クリーンアーキテクチャ』所感 ≪null 篇≫

2019/07/03
W3C Workshop on Web Games参加レポート

2019/06/28
TypeScriptでObject.assign()に正しい型をつける

2019/06/25
カブクエンジニア開発合宿に行ってきました 2019夏

2019/06/21
Hola! KubeCon Europe 2019の参加レポート

2019/06/19
Clean Resume きれいな環境できれいな履歴書を作成する

2019/05/20
[Web フロントエンド] 状態更新ロジックをフレームワークから独立させる

2019/04/16
C++のenable_shared_from_thisを使う

2019/04/12
OpenAPI 3 ファーストな Web アプリケーション開発(Python で API 編)

2019/04/08
WebGLでレイマーチングを使ったCSGを実現する

2019/04/02
『エンジニア採用最前線』に感化されて2週間でエンジニア主導の求人票更新フローを構築した話

2019/03/29
その1 Jetson TX2でk3s(枯山水)を動かしてみた

2019/03/27
任意のブラウザ上でJestで書いたテストを実行する

2019/02/08
TypeScript で “radian” と “degree” を間違えないようにする

2019/02/05
Python3でGoogle Cloud ML Engineをローカルで動作する方法

2019/01/18
SIGGRAPH Asia 2018 参加レポート

2019/01/08
お正月だョ!ECMAScript Proposal全員集合!!

2019/01/08
カブクエンジニア開発合宿に行ってきました 2018秋

2018/12/25
OpenAPI 3 ファーストな Web アプリケーション開発(環境編)

2018/12/23
いまMLKitカスタムモデル(TF Lite)は使えるのか

2018/12/21
[IoT] Docker on JetsonでMQTTを使ってCloud IoT Coreと通信する

2018/12/11
TypeScriptで実現する型安全な多言語対応(Angularを例に)

2018/12/05
GASでCompute Engineの時間に応じた自動停止/起動ツールを作成する 〜GASで簡単に好きなGoogle APIを叩く方法〜

2018/12/02
single quotes な Black を vendoring して packaging

2018/11/14
3次元データに2次元データの深層学習の技術(Inception V3, ResNet)を適用

2018/11/04
Node Knockout 2018 に参戦しました

2018/10/24
SIGGRAPH 2018参加レポート-後編(VR/AR)

2018/10/11
Angular 4アプリケーションをAngular 6に移行する

2018/10/05
SIGGRAPH 2018参加レポート-特別編(VR@50)

2018/10/03
Three.jsでVRしたい

2018/10/02
SIGGRAPH 2018参加レポート-前編

2018/09/27
ズーム可能なSVGを実装する方法の解説

2018/09/25
Kerasを用いた複数入力モデル精度向上のためのTips

2018/09/21
競技プログラミングの勉強会を開催している話

2018/09/19
Ladder Netwoksによる半教師あり学習

2018/08/10
「Maker Faire Tokyo 2018」に出展しました

2018/08/02
Kerasを用いた複数時系列データを1つの深層学習モデルで学習させる方法

2018/07/26
Apollo GraphQLでWebサービスを開発してわかったこと

2018/07/19
【深層学習】時系列データに対する1次元畳み込み層の出力を可視化

2018/07/11
きたない requirements.txt から Pipenv への移行

2018/06/26
CSS Houdiniを味見する

2018/06/25
不確実性を考慮した時系列データ予測

2018/06/20
Google Colaboratory を自分のマシンで走らせる

2018/06/18
Go言語でWebAssembly

2018/06/15
カブクエンジニア開発合宿に行ってきました 2018春

2018/06/08
2018 年の tree shaking

2018/06/07
隠れマルコフモデル 入門

2018/05/30
DASKによる探索的データ分析(EDA)

2018/05/10
TensorFlowをソースからビルドする方法とその効果

2018/04/23
EGLとOpenGLを使用するコードのビルド方法〜libGLからlibOpenGLへ

2018/04/23
技術書典4にサークル参加してきました

2018/04/13
Python で Cura をバッチ実行するためには

2018/04/04
ARCoreで3Dプリント風エフェクトを実現する〜呪文による積層造形映像制作の舞台裏〜

2018/04/02
深層学習を用いた時系列データにおける異常検知

2018/04/01
音声ユーザーインターフェースを用いた新方式積層造形装置の提案

2018/03/31
Container builderでコンテナイメージをBuildしてSlackで結果を受け取る開発スタイルが捗る

2018/03/23
ngUpgrade を使って AngularJS から Angular に移行

2018/03/14
Three.jsのパフォーマンスTips

2018/02/14
C++17の新機能を試す〜その1「3次元版hypot」

2018/01/17
時系列データにおける異常検知

2018/01/11
異常検知の基礎

2018/01/09
three.ar.jsを使ったスマホAR入門

2017/12/17
Python OpenAPIライブラリ bravado-core の発展的な使い方

2017/12/15
WebAssembly(wat)を手書きする

2017/12/14
AngularJS を Angular に移行: ng-annotate 相当の機能を TypeScrpt ファイルに適用

2017/12/08
Android Thingsで4足ロボットを作る ~ Android ThingsとPCA9685でサーボ制御)

2017/12/06
Raspberry PIとDialogflow & Google Cloud Platformを利用した、3Dプリンターボット(仮)の開発 (概要編)

2017/11/20
カブクエンジニア開発合宿に行ってきました 2017秋

2017/10/19
Android Thingsを使って3Dプリント戦車を作ろう ① ハードウェア準備編

2017/10/13
第2回 魁!! GPUクラスタ on GKE ~PodからGPUを使う編~

2017/10/05
第1回 魁!! GPUクラスタ on GKE ~GPUクラスタ構築編~

2017/09/13
「Maker Faire Tokyo 2017」に出展しました。

2017/09/11
PyConJP2017に参加しました

2017/09/08
bravado-coreによるOpenAPIを利用したPythonアプリケーション開発

2017/08/23
OpenAPIのご紹介

2017/08/18
EuroPython2017で2名登壇しました。

2017/07/26
3DプリンターでLチカ

2017/07/03
Three.js r86で何が変わったのか

2017/06/21
3次元データへの深層学習の適用

2017/06/01
カブクエンジニア開発合宿に行ってきました 2017春

2017/05/08
Three.js r85で何が変わったのか

2017/04/10
GCPのGPUインスタンスでレンダリングを高速化

2017/02/07
Three.js r84で何が変わったのか

2017/01/27
Google App EngineのFlexible EnvironmentにTmpfsを導入する

2016/12/21
Three.js r83で何が変わったのか

2016/12/02
Three.jsでのクリッピング平面の利用

2016/11/08
Three.js r82で何が変わったのか

2016/12/17
SIGGRAPH 2016 レポート

2016/11/02
カブクエンジニア開発合宿に行ってきました 2016秋

2016/10/28
PyConJP2016 行きました

2016/10/17
EuroPython2016で登壇しました

2016/10/13
Angular 2.0.0ファイナルへのアップグレード

2016/10/04
Three.js r81で何が変わったのか

2016/09/14
カブクのエンジニアインターンシッププログラムについての詩

2016/09/05
カブクのエンジニアインターンとして3ヶ月でやった事 〜高橋知成の場合〜

2016/08/30
Three.js r80で何が変わったのか

2016/07/15
Three.js r79で何が変わったのか

2016/05/20
MakerGoの作り方

2016/05/08
TensorFlow on DockerでGPUを使えるようにする方法

2016/04/27
Blenderの3DデータをMinecraftに送りこむ

2016/04/20
Tensorflowを使ったDeep LearningにおけるGPU性能調査

→
←

関連職種

Recruit

フロントエンドエンジニア(TypeScript)

業務内容

当ポジションは弊社Webサービスのフロントエンド機能設計及び実装を担当します。 サービス毎の開発チームで2週間スプリントのスクラム開発を実施しています。 週次で開発チームミーティングを実施し、実装設計の相談や工数見積もりを行います。 全ての開発コードはレビューと自動テストによって品質を保っています。 また、リファクタリングやフレームワークのバージョンアップも開発フローに組込み、技術的負債を放置しない開発を目指しています。

インターン(Webエンジニア)

業務内容

業務から独立した、調査・研究系のタスクをおまかせしています。コードレビュー、 社内での報告会、 ブログ記事執筆を通して着実にスキルアップしていただくことを目指しています。 (希望があれば、プロダクトの開発業務もおまかせします。)

→
←

お客様のご要望に「Kabuku」はお応えいたします。
ぜひお気軽にご相談ください。

お電話でも受け付けております
03-6380-2750
営業時間:09:30~18:00
※土日祝は除く