Android の Vulkan シェーダー コンパイラ

Vulkan アプリでは、OpenGL ES アプリとは異なる方法でシェーダーを管理する必要があります。OpenGL ES では、GLSL シェーダー プログラムのソーステキストを形成する一連の文字列としてシェーダーを指定します。一方 Vulkan API では、SPIR-V モジュールのエントリ ポイントの形式でシェーダーを指定する必要があります。

NDK Release 12 以降には、GLSL を SPIR-V にコンパイルするランタイム ライブラリが含まれます。このランタイム ライブラリは、Shaderc オープンソース プロジェクトのものと同じで、バックエンドと同じ Glslang GLSL リファレンス コンパイラを使用します。Shaderc バージョンのコンパイラはデフォルトで、Vulkan 用のコンパイルを想定します。コードが Vulkan に有効かどうかを確認したら、コンパイラは KHR_vulkan_glsl 拡張機能を自動的に有効にします。Shaderc バージョンのコンパイラは、Vulkan 準拠の SPIR-V コードも生成します。

開発中に SPIR-V モジュールを Vulkan アプリにコンパイルできます。この手法は「Ahead-of-time(AOT)コンパイル」と呼ばれます。別の方法として、標準搭載されているシェーダー ソースや手続きにより生成されるシェーダー ソースから、ランタイムに必要に応じて、アプリにコンパイルさせることも可能です。この手法は「ランタイム コンパイル」と呼ばれます。 Android Studio には、Vulkan シェーダーをビルドするためのサポートが統合されています。

ここからは、各手法について詳しく説明します。また、シェーダー コンパイルを Vulkan アプリに統合する方法についても説明します。

AOT コンパイル

以降のセクションで説明するように、シェーダーの AOT コンパイルを実現する方法は 2 つあります。

Android Studio を使用する

シェーダーを app/src/main/shaders/ に追加すると、Android Studio はファイル拡張子でシェーダーを認識し、以下の処理を行います。

  • そのディレクトリ内のすべてのシェーダー ファイルを再帰的にコンパイルする。
  • .spv というサフィックスをコンパイル済みの SPIR-V シェーダー ファイルに追加します。
  • SPIRV シェーダーを APK の assets/shaders/ ディレクトリにまとめます。

アプリは、コンパイル済みのシェーダーを、対応する assets/shaders/ の場所から実行時に読み込みます。コンパイル済みの spv シェーダー ファイル構造は、app/src/main/shaders/ の下にあるアプリの GLSL シェーダー ファイル構造と同じです。

AAsset* file = AAssetManager_open(assetManager,
                     "shaders/tri.vert.spv", AASSET_MODE_BUFFER);
size_t fileLength = AAsset_getLength(file);
char* fileContent = new char[fileLength];
AAsset_read(file, fileContent, fileLength);

Shaderc コンパイル フラグは、次の例に示すように、Gradle DSL shaders ブロック内で構成できます。

Groovy

android {
  defaultConfig {
    shaders {
      glslcArgs.addAll(['-c', '-g'])
      scopedArgs.create('lights') {
        glslcArgs.addAll(['-DLIGHT1=1', '-DLIGHT2=0'])
      }
    }
  }
}

Kotlin

android {
  defaultConfig {
    shaders {
        glslcArgs += listOf("-c", "-g")
        glslcScopedArgs("lights", "-DLIGHT1=1", "-DLIGHT2=0")
    }
  }
}

glslcArgs はすべてのシェーダー コンパイルに適用されます。scopedArgs はそのスコープに対するコンパイルにのみ適用されます。上記の例では、スコープ引数の lights が作成されています。これは app/src/main/shaders/lights/ ディレクトリの GLSL シェーダーにのみ適用されます。使用可能なコンパイル フラグの全一覧については、glslc をご覧ください。NDK 内の Shaderc は、NDK リリース時の GitHub リポジトリのスナップショットです。次のセクションで説明するように、glslc --help コマンドを使用すれば、そのバージョンで確実にサポートされているフラグがわかります。

オフライン コマンドライン コンパイル

GLSL シェーダーは、glslc コマンドライン コンパイラを使用することで、メインアプリに関係なく SPIR-V にコンパイルできます。NDK Release 12 以降では、この使用モデルをサポートするために、ビルド済みの glslc のバージョンと関連ツールが <android-ndk-dir>/shader-tools/ ディレクトリにまとめられています。

このコンパイラは Shaderc プロジェクトからも使用できます。バイナリ バージョンをビルドするには、そのページの手順を行ってください。

glslc には、シェーダー コンパイルのコマンドライン オプションが豊富に用意されているため、アプリのさまざまな要件を満たすことができます。

glslc ツールは、シェーダー エントリ ポイントが 1 つある、1 つのソースファイルを SPIR-V モジュールにコンパイルします。デフォルトでは、出力ファイルの名前は、ソースファイルと同じ名前に .spv という拡張子が付いたものになります。

ファイル名拡張子を使って、コンパイルするのはどのグラフィック シェーダー ステージか、または演算シェーダーかどうかを glslc ツールに伝えます。ファイル名拡張子の使い方と、ツールで使用できるオプションについては、glslc のマニュアルでシェーダー ステージの仕様をご覧ください。

ランタイム コンパイル

NDK では、ランタイムでのシェーダーの JIT コンパイル用に、libshaderc ライブラリが用意されています。これには C と C++ 両方の API があります。

C++ アプリケーションでは C++ API を使用する必要があります。C ABI は下位レベルであり、安定性が向上する可能性が高いため、他の言語のアプリでは C API を使用することをおすすめします。

次の例は、C++ API の使用方法を示しています。

#include <iostream>
#include <string>
#include <vector>
#include <shaderc/shaderc.hpp>

std::vector<uint32_t> compile_file(const std::string& name,
                                   shaderc_shader_kind kind,
                                   const std::string& data) {
  shaderc::Compiler compiler;
  shaderc::CompileOptions options;

  // Like -DMY_DEFINE=1
  options.AddMacroDefinition("MY_DEFINE", "1");

  shaderc::SpvCompilationResult module = compiler.CompileGlslToSpv(
      data.c_str(), data.size(), kind, name.c_str(), options);

  if (module.GetCompilationStatus() !=
      shaderc_compilation_status_success) {
    std::cerr << module.GetErrorMessage();
  }

  std::vector<uint32_t> result(module.cbegin(), module.cend());
  return result;
}

プロジェクトに統合する

プロジェクトの Android.mk ファイルか Gradle を使用して、Vulkan シェーダー コンパイラをアプリに統合できます。

Android.mk

プロジェクトの Android.mk ファイルを使用してシェーダー コンパイラを統合するには、次の手順を行います。

  1. Android.mk ファイルに次の行を含めます。
    include $(CLEAR_VARS)
         ...
    LOCAL_STATIC_LIBRARIES := shaderc
         ...
    include $(BUILD_SHARED_LIBRARY)
    
    $(call import-module, third_party/shaderc)
    
  2. アプリの Application.mk で APP_STL を c++_staticc++_sharedgnustl_staticgnustl_shared のいずれかに設定します。

Gradle の CMake 統合

  1. ターミナル ウィンドウで ndk_root/sources/third_party/shaderc/ に移動します。
  2. 以下のコマンドを実行して NDK の Shaderc をビルドします。このコマンドは、使用する NDK バージョンごとに 1 回だけ実行する必要があります。
    $ ../../../ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=Android.mk \
    APP_STL:=<stl_version> APP_ABI=all libshaderc_combined
    

    このコマンドによって、<ndk_root>/sources/third_party/shaderc/ に 2 つのフォルダが配置されます。ディレクトリ構造は次のようになります。

    include/
      shaderc/
        shaderc.h
        shaderc.hpp
    libs/
      <stl_version>/
        {all of the abis}
           libshaderc.a
    
  3. 同様の外部ライブラリに対して通常行うように、生成されたインクルードやライブラリを target_include_directoriestarget_link_libraries を使って追加します。アプリの STL のタイプは、stl_version で指定されている stl タイプのいずれかに一致する必要があります。NDK では c++_shared または c++_static の使用をおすすめしています。ただし、gnustl_staticgnustl_shared もサポートしています。

最新の Shaderc を入手する

NDK の Shaderc は、Shaderc のアップストリーム リポジトリのスナップショットである Android ソースツリーにあります。最新の Shaderc が必要であれば、ビルド手順の詳細をご確認ください。 大まかな手順は次のとおりです。

  1. 最新の Shaderc をダウンロードします。
    git clone https://github.com/google/shaderc.git
  2. 依存関係を更新します。
    ./utils/git-sync-deps
  3. Shaderc をビルドします。
    <ndk_dir>/ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=Android.mk \
        APP_STL:=c++_static APP_ABI=all libshaderc_combined -j16
    
  4. ビルド スクリプト ファイルで独自の Shaderc ビルドを使用するようにプロジェクトを設定します。