Android의 Vulkan 셰이더 컴파일러

Vulkan 앱은 OpenGL ES 앱과는 다른 방식으로 셰이더를 관리해야 합니다. OpenGL ES에서는 GLSL 셰이더 프로그램의 소스 텍스트를 구성하는 문자열 집합으로 셰이더를 제공합니다. 이에 반해, Vulkan API에서는 SPIR-V 모듈의 진입점 형식으로 셰이더를 제공해야 합니다.

NDK 릴리스 12 이상 버전에서는 GLSL을 SPIR-V로 컴파일하는 런타임 라이브러리가 포함되어 있습니다. 이 런타임 라이브러리는 Shaderc 오픈소스 프로젝트의 라이브러리와 동일하며, 백엔드가 사용하는 것과 동일한 Glslang GLSL 참조 컴파일러를 사용합니다. Shaderc 버전의 컴파일러는 기본적으로 Vulkan용으로 컴파일하는 것으로 가정합니다. 컴파일러는 코드가 Vulkan에서 유효한지 확인한 후 KHR_vulkan_glsl 확장 프로그램을 자동으로 사용 설정합니다. 또한, Shaderc 버전의 컴파일러는 Vulkan 호환 SPIR-V 코드를 생성합니다.

ahead-of-time 또는 AOT라는 컴파일 방식으로, 개발하는 중에 SPIR-V 모듈을 Vulkan 앱으로 컴파일하도록 선택할 수 있습니다. 또는 런타임 중 필요한 경우에 제공되거나 절차상 생성된 셰이더 소스의 SPIR-V 모듈을 앱이 컴파일하도록 지정할 수 있습니다. 이 방식은 런타임 컴파일이라고 합니다. Android Studio에는 Vulkan 셰이더 빌드 지원이 통합되었습니다.

이 페이지의 나머지 부분에서는 각 방식에 관한 자세한 정보를 제공한 다음, Vulkan 앱에 셰이더 컴파일을 통합하는 방법을 설명합니다.

AOT 컴파일

다음 섹션에서는 셰이더 AOT 컴파일을 수행하는 두 가지 방법에 대해 설명합니다.

Android 스튜디오 사용

셰이더를 app/src/main/shaders/에 배치하면 Android 스튜디오는 파일 확장자로 셰이더를 인식한 후 다음 작업을 완료합니다.

  • 위 디렉터리에서 모든 셰이더 파일을 반복적으로 컴파일합니다.
  • 컴파일된 SPIR-V 셰이더 파일에 .spv 접미사를 추가합니다.
  • 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 블록 내에 구성할 수 있습니다.

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

glslcArgs는 모든 셰이더 컴파일에 적용됩니다. scopedArgs는 이 범위의 컴파일에만 적용됩니다. 위의 예에서는 범위 인수 lights를 만듭니다. 이 인수는 app/src/main/shaders/lights/ 디렉터리의 GLSL 셰이더에만 적용됩니다. 사용 가능한 컴파일 플래그의 전체 목록은 glslc를 참조하세요. NDK 출시 시점에 NDK에 포함된 Shaderc는 GitHub 저장소의 스냅샷입니다. 다음 섹션에 설명된 대로 glslc --help 명령어를 사용하여 정확히 해당 버전에서 지원되는 플래그를 가져올 수 있습니다.

오프라인 명령줄 컴파일

GLSL 셰이더는 기본 애플리케이션에 상관없이 glslc 명령줄 컴파일러를 사용하여 SPIR-V로 컴파일할 수 있습니다. NDK 릴리스 12 이상에서는 이 사용 모델을 지원하도록 미리 빌드된 glslc 버전과 관련 도구가 <android-ndk-dir>/shader-tools/ 디렉터리에 있습니다.

이 컴파일러는 Shaderc 프로젝트에서도 사용할 수 있습니다. 프로젝트에서 안내에 따라 바이너리 버전을 빌드하세요.

glslc는 애플리케이션의 다양한 요구사항을 충족할 수 있도록 셰이더 컴파일에 사용되는 여러 명령줄 옵션을 제공합니다.

glslc 도구는 하나의 셰이더 진입점을 사용하여 단일 소스 파일을 SPIR-V 모듈로 컴파일합니다. 기본적으로 출력 파일은 소스 파일과 이름이 같지만 .spv 확장자가 추가됩니다.

파일 이름 확장자를 사용하여 컴파일할 그래픽 셰이더 단계 또는 계산 셰이더 컴파일 여부를 glslc 도구에 알려줍니다. 이러한 파일 이름 확장자를 사용하는 방법 및 도구와 함께 사용할 수 있는 옵션에 관한 자세한 내용은 glslc 매뉴얼의 셰이더 단계 사양을 참조하세요.

런타임 컴파일

런타임 중 셰이더의 JIT 컴파일을 위해 NDK는 C 및 C++ API가 모두 포함된 libshaderc 라이브러리를 제공합니다.

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++_static, c++_shared, gnustl_static, gnustl_shared 중 하나로 설정합니다.

Gradle의 CMake 통합

  1. 터미널 창에서 ndk_root/sources/third_party/shaderc/로 이동합니다.
  2. 다음 명령어를 실행하여 NDK Shaderc를 빌드합니다. 이 명령어는 사용하는 NDK 버전마다 한 번만 실행해야 합니다.
    $ ../../../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/에 두 개의 폴더를 배치합니다. 디렉터리 구조는 다음과 같습니다.

    include/
      shaderc/
        shaderc.h
        shaderc.hpp
    libs/
      <stl_version>/
        {all of the abis}
           libshaderc.a
    
  3. 유사한 외부 라이브러리에서 일반적으로 하는 것처럼 생성된 includes 및 libs를 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-dep
  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 빌드를 사용하도록 프로젝트를 구성합니다.