Vulkan shader compilers on Android

A Vulkan app must manage shaders differently from the way an OpenGL ES app does: In OpenGL ES, you provide a shader as a set of strings forming the source text of a GLSL shader program. By contrast, the Vulkan API requires you to provide a shader in the form of an entry point in a SPIR-V module.

The NDK Release 12 and later includes a runtime library for compiling GLSL into SPIR-V. The runtime library is the same as the one in the Shaderc open source project, and uses the same Glslang GLSL reference compiler as its back end. By default, the Shaderc version of the compiler assumes you are compiling for Vulkan. After checking whether your code is valid for Vulkan, the compiler automatically enables the KHR_vulkan_glsl extension. The Shaderc version of the compiler also generates Vulkan-compliant SPIR-V code.

You can choose to compile SPIR-V modules into your Vulkan app during development, a practice called ahead-of-time, or AOT, compilation. Alternatively, you can have your app compile them from shipped or procedurally generated shader source when needed during runtime. This practice is called runtime compiling. Android Studio has integrated support to build Vulkan shaders.

The rest of this page provides more detail about each practice, and then explains how to integrate shader compilation into your Vulkan app.

AOT compilation

There are two ways to achieve shader AOT compilation, described in the following sections.

Use Android Studio

Putting shaders into app/src/main/shaders/, Android Studio recognizes shaders by their file extensions, and will complete the following actions:

  • Compile all shader files recursively under that directory.
  • Append the .spv suffix to the compiled SPIR-V shader files.
  • Pack SPIRV-shaders into the APK's assets/shaders/ directory.

The application would load the compiled shaders from the corresponding assets/shaders/ location at run time; the compiled spv shader file structure is the same as the application's GLSL shader file structure under app/src/main/shaders/:

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 compile flags could be configured inside the gradle DSL shaders block, as shown in the following example:

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 apply to all shader compilations; scopedArgs only apply when compiling for that scope. The above example creates a scope argument lights, which will only apply to GLSL shaders under the app/src/main/shaders/lights/ directory. Refer to glslc for the complete list of available compilation flags. Note that Shaderc inside NDK is a snapshot from that github repo at the NDK release time; you can get the exact supported flags for that version with the command glslc --help, as decribed in the next section.

Offline command-line compilation

GLSL Shaders can be compiled to SPIR-V independent of the main application using the glslc command-line compiler. NDK release 12 and later packs a version of pre-built glslc and related tools in the <android-ndk-dir>/shader-tools/ directory to support this usage model.

The compiler is also available from the Shaderc project; follow the instructions there to build a binary version.

glslc provides a rich set of command-line options for shader compilation to meet various requirements for an application.

The glslc tool compiles a single-source file to a SPIR-V module with a single shader entry point. By default, the output file has the same name as that of the source file, but with the .spv extension appended.

You use filename extensions to tell the glslc tool which graphics shader stage to compile, or whether a compute shader is being compiled. For information on how to use these filename extensions, and options you can use with the tool, see Shader stage specification in the glslc manual.

Runtime compilation

For JIT compilation of shaders during runtime, the NDK provides the libshaderc library, which has both C and C++ APIs.

C++ applications should use the C++ API. We recommend that apps in other languages use the C API, because the C ABI is lower level, and likely to provide better stability.

The following example shows how to use the 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;
}

Integrate into your projects

You can integrate the Vulkan shader compiler into your app using either the project's Android.mk file or Gradle.

Android.mk

Perform the following steps to use your project's Android.mk file to integrate the shader compiler.

  1. Include the following lines in your Android.mk file:
    include $(CLEAR_VARS)
         ...
    LOCAL_STATIC_LIBRARIES := shaderc
         ...
    include $(BUILD_SHARED_LIBRARY)
    
    $(call import-module, third_party/shaderc)
    
  2. Set APP_STL to one of c++_static, c++_shared, gnustl_static, or gnustl_shared in app's Application.mk

Gradle's CMake integration

  1. In a terminal window, navigate to ndk_root/sources/third_party/shaderc/.
  2. Run the following command to build NDK's Shaderc. You only need to run this command once on each NDK version that you use:
    $ ../../../ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=Android.mk \
    APP_STL:=<stl_version> APP_ABI=all libshaderc_combined
    

    This command places two folders in <ndk_root>/sources/third_party/shaderc/. The directory structure is as follows:

    include/
      shaderc/
        shaderc.h
        shaderc.hpp
    libs/
      <stl_version>/
        {all of the abis}
           libshaderc.a
    
  3. Add the generated includes and libs using target_include_directories and target_link_libraries, as you normally do for similar external libraries. Your app's STL type must match one of the stl types specified in stl_version. The NDK recommends using c++_shared or c++_static, although gnustl_static and gnustl_shared are also supported.

Get the latest Shaderc

Shaderc in NDK comes from Android Source tree, which is a snapshot of the upstream Shaderc repo. If you need the latest Shaderc, refer to build instruction for details. The high-level steps are as follows:

  1. Download the latest Shaderc:
    git clone https://github.com/google/shaderc.git
  2. Update dependencies:
    ./utils/git-sync-deps
  3. Build Shaderc:
    <ndk_dir>/ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=Android.mk \
        APP_STL:=c++_static APP_ABI=all libshaderc_combined -j16
    
  4. Configure your project to use your own Shaderc build in your build script file.