본문 바로가기
Vulkan

7. SPIR-V

by SimonLee 2024. 7. 4.

SPIR는 Standard Portable Immediate Representation의 약어 이며,

SPIR-V는 SPIR로 부터 파생 되었다.

 

SPIR-V가 Vulkan과 OpenCL을 지원하면서 벤더 들은 드라이버 개발과 최적화에 집중을 할 수 있게 되었다.

 

사람이 읽을수 있는 코드 형식으로 된 OpenGL의 셰이딩 언어와는 달리 Vulkan은 하위 계층의 이진 중간 표현 (IR) 언어인

SPIR-V만을 사용한다.

 

https://www.khronos.org/spir/

SPIR의 좌측을 보면

GLSL, HLSL, OpenCL 컴포넌트를 빌드하기 위해서는 DXC, Clang, LLVM 같은 컴파일러가 필요하다.

다양한 언어들이 각 각의 컴파일러 필요 없이 하나의 포맷인 SPIR-V로 컴파일하여,

우측의 언어에서 사용할수 있게 한다.

 

SPIR-V 장점은 다음과 같다.

1. 개발자 들은 새로운 언어를 배울 필요 없이 기존 코드를 사용하여 SPIR-V로 변환할 수 있다.

2. SPIR-V는 고급 언어지만 모든 텍스트, 문자열 파싱을 필요로 하지 않을 만큼 간단해서 성능향상에 매우 도움이 된다.

3. SPIR-V 에서는 고급 언어에 대한 컴파일 등 코드 처리가 필요치 않으므로 드라이버의 복잡성이 크게 줄어든다.

 

SPIR-V 바이트코드로된 파일 헤더 구조 이다.

확장자는 .spv 형식

SPIR-V 언어는 를 어셈블리 처럼 되어있는데

간단한 아래 셰이더 변환을 해봐서 출력을 하면 다음과 같다.

#version 310 es
void main() {
}

 

SPIR-V 변환된 결과

 

역어셈블리 할일은 없겠지.......ㅠ

아무튼

 

기존 셰이더 코드를 SPIR-V로 변환하는 코드가 필요한데

Android NDK에는 이미 SPIR-V 변환을 위한 라이브러리 shaderc가 존재한다.

우리는 shaderc를 갖다쓰자.

1. shaderc 빌드를 하자.

먼저 NDK 설치를 하자.

안드로이드 스튜디오에서 Setting -> Android SDK -> NDK(side by side) 에서 선택하면 된다.

 

설치된 NDK로 설정하려면 build.gradle 파일에서 아래와 같이 추가해주면 된다.

android {
    compileSdk = 34
    ndkVersion = "20.1.5948944"
    ...
}

 

그런데 Window platform 에서

최신 버전 NDK에서 빌드를 하게 되면 llvm 에서 아래처럼 오류가 발생한다....

https://github.com/android/ndk/issues/1815#issuecomment-1397669147

 

[Bug]: llvm-ar.exe error "unknown command" · Issue #1815 · android/ndk

Description Building of shaderc lib with the command ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=Android.mk \ APP_STL:=c++_static APP_ABI=all libshaderc_combined exits with error "[arm64-v8a] Com...

github.com

 

원인은 아래와 같이 echo의 결과 값이 shell과 bash 다르기 때문이라고 함.

bash and powershell :
$ echo "foo" 
foo

cmd:
> echo "foo"
"foo"

 

Android.mk 코드를 바꾸는 방법이 있지만 Powershell 에서 빌드를 하는 방법도 있다고

하는데 ㅡ. ㅡ ;;; 안된다.

 

코드를 바꾸어 주자.

sources\third_party\shaderc\Android.mk 파일에 45 ~ 51 라인을 아래로 바꾸어 주어야 함.

$(1)/combine.ar: $(addprefix $(1)/, $(ALL_LIBS))
	$(file >$(1)/combine.ar,create libshaderc_combined.a)
	$(foreach lib,$(ALL_LIBS),
		$(file >>$(1)/combine.ar,addlib $(lib))
	)
	$(file >>$(1)/combine.ar,save)
	$(file >>$(1)/combine.ar,end)

 

빌드 방법은  Android NDK 경로에서 빌드 명령을 내리면 된다.

// Window Platform
cd ....\sources\third_party\shaderc >
> ..\..\..\ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=Android.mk ^ APP_ABI=all \
APP_STL=c++_static ^ -j4 clean libshaderc_combined

 

빌드를 하게되면 아래와 같은 폴더와 정적 파일이 생성이 된다.

sources/third_party/shaderc/include
sources/third_party/shaderc/libs/c++_static/arm64-v8a/libshaderc.a

 

CmakesFile에서 해당파일을 링킹해준다.

set(CMAKE_SHADERRC ${CMAKE_ANDROID_NDK}/sources/third_party/shaderc)

add_library(shaderc INTERFACE)

target_include_directories(shaderc INTERFACE
        ${CMAKE_SHADERRC}/include)

target_link_libraries(shaderc INTERFACE
        ${CMAKE_SHADERRC}/libs/c++_static/${CMAKE_ANDROID_ARCH_ABI}/libshaderc.a
)

 

2. SPIR-V 사용하여 셰이더 모듈을 생성하자.

 

링킹이 완료가 되었다면 이제 셰이더 코드를 SPIR-V 이진 코드로 변환하여 구현할 수 있다.

셰이더 코드를 SPIR-V 코드로 변환하는 함수는 shaderc.a 라이브러리 내부에 있다.

// Compiles the given source GLSL and returns a SPIR-V binary module
// compilation result.
// Like the previous CompileGlslToSpv method but uses default options.
SpvCompilationResult CompileGlslToSpv(const char* source_text,
                                    size_t source_text_size,
                                    shaderc_shader_kind shader_kind,
                                    const char* input_file_name) const {
shaderc_compilation_result_t compilation_result =
    shaderc_compile_into_spv(compiler_, source_text, source_text_size,
                             shader_kind, input_file_name, "main", nullptr);
return SpvCompilationResult(compilation_result);
}

- souce_text : 셰이더 코드 버퍼 포인터

- source_text_size : 셰이더 코드 바이트 사이즈

- shader_kind : 셰이더 종류 (아래 참고)

- input_file_name : fileName, 런타임의 경우 아무거나 넣음.

# shaderc.h
typedef enum {
  // Forced shader kinds. These shader kinds force the compiler to compile the
  // source code as the specified kind of shader.
  shaderc_vertex_shader,
  shaderc_fragment_shader,
  shaderc_compute_shader,
  shaderc_geometry_shader,
  shaderc_tess_control_shader,
  shaderc_tess_evaluation_shader,
} shaderc_shader_kind;
# USAGE
shaderc::Compiler compiler;
auto result = compiler.CompileGlslToSpv(shaderCode.data(),
                                        shaderCode.size(),
                                        static_cast<shaderc_shader_kind>(shaderType),
                                        tag.c_str());
                                        
*shaderBinary = std::vector<uint32_t>(result.cbegin(), result.cend());

 

변환된 shaderBinary는 VkShaderModuleCreateInfo 내부 변수 pCode로 들어간다.

Shader Module을 만들기 위해 VkShaderModuleCreateInfo를 생성한다.

// Provided by VK_VERSION_1_0
typedef struct VkShaderModuleCreateInfo {
    VkStructureType              sType;
    const void*                  pNext;
    VkShaderModuleCreateFlags    flags;
    size_t                       codeSize;
    const uint32_t*              pCode;
} VkShaderModuleCreateInfo;

- sType : VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO

- next : 확장판 지정 구조체 가리키는 포인터

- flags : 추후 사용

- codeSize : 소스 코드의 길이를 바이트 단위로 지정

- pCode : 셰이더 모듈을 생성하는데 사용할 소스 코드를 가리킨다.

 

 

셰이더 모듈을 생성하는 함수는 vKCreateShaderModule

// Provided by VK_VERSION_1_0
VkResult vkCreateShaderModule(
    VkDevice                                    device,
    const VkShaderModuleCreateInfo*             pCreateInfo,
    const VkAllocationCallbacks*                pAllocator,
    VkShaderModule*                             pShaderModule);

- device : 셰이더 모듈이 연결될 논리적 장치의 핸들

- pCreateInfo : VkShaderModuleCreateInfo 구조체 포인터

- pAllocator : 호스트 메모리 할당 과정

- pShaderModule : 생성된 VkShaderModule 개체

 

전체 코드 >

VertexShaderModule, FragmentShaderModule을 생성하자.
VkShaderModule mVertexShaderModule;
VkShaderModule mFragmentShaderModule;

void VkRenderer::CreateShaderModule() {
    aout << "CreateShaderModule" << endl;
    // Vertex VkShaderModule 생성
    string_view vertexShaderCode = {
            "#version 310 es                                        \n"
            "                                                       \n"
            "void main() {                                          \n"
            "    vec2 pos[3] = vec2[3](vec2(-0.5,  0.5),            \n"
            "                          vec2( 0.5,  0.5),            \n"
            "                          vec2( 0.0, -0.5));           \n"
            "                                                       \n"
            "    gl_Position = vec4(pos[gl_VertexIndex], 0.0, 1.0); \n"
            "}                                                      \n"
    };

    std::vector<uint32_t> vertexShaderBinary;
    VK_CHECK_ERROR(vkCompileShader(vertexShaderCode,
                                   VK_SHADER_TYPE_VERTEX,
                                   &vertexShaderBinary));

    VkShaderModuleCreateInfo vertexShaderModuleCreateInfo{
            .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
            .codeSize = vertexShaderBinary.size() * sizeof(uint32_t),
            .pCode = vertexShaderBinary.data()
    };

    VK_CHECK_ERROR(vkCreateShaderModule(mDevice,
                                        &vertexShaderModuleCreateInfo,
                                        nullptr,
                                        &mVertexShaderModule));
    
    string_view fragmentShaderCode = {
            "#version 310 es                                        \n"
            "precision mediump float;                               \n"
            "                                                       \n"
            "layout(location = 0) out vec4 fragmentColor;           \n"
            "                                                       \n"
            "void main() {                                          \n"
            "    fragmentColor = vec4(1.0, 0.0, 0.0, 1.0);          \n"
            "}                                                      \n"
    };

    std::vector<uint32_t> fragmentShaderBinary;
    VK_CHECK_ERROR(vkCompileShader(fragmentShaderCode,
                                   VK_SHADER_TYPE_FRAGMENT,
                                   &fragmentShaderBinary));

    VkShaderModuleCreateInfo fragmentShaderModuleCreateInfo{
            .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
            .codeSize = fragmentShaderBinary.size() * sizeof(uint32_t),
            .pCode = fragmentShaderBinary.data()
    };

    VK_CHECK_ERROR(vkCreateShaderModule(mDevice,
                                        &fragmentShaderModuleCreateInfo,
                                        nullptr,
                                        &mFragmentShaderModule));
}



typedef enum VkShaderType {
    VK_SHADER_TYPE_VERTEX = shaderc_vertex_shader,
    VK_SHADER_TYPE_FRAGMENT = shaderc_fragment_shader
} VkShaderType;

inline VkResult
vkCompileShader(std::string_view shaderCode, VkShaderType shaderType,
                std::vector<uint32_t> *shaderBinary) {
    std::random_device device;
    std::mt19937 generator(device());
    std::uniform_int_distribution<> distribution('a', 'z');
    std::string tag('a', 4);

    for (auto i = 0; i < 16; ++i) {
        tag[i] = static_cast<char>(distribution(generator));
    }
    aout << "tag : " << tag << std::endl;

    shaderc::Compiler compiler;
    auto result = compiler.CompileGlslToSpv(shaderCode.data(),
                                            shaderCode.size(),
                                            static_cast<shaderc_shader_kind>(shaderType),
                                            tag.c_str());

    if (result.GetCompilationStatus() != shaderc_compilation_status_success) {
        aout << result.GetErrorMessage() << std::endl;
        return VK_ERROR_UNKNOWN;
    }

    *shaderBinary = std::vector<uint32_t>(result.cbegin(), result.cend());
    return VK_SUCCESS;
}

 

Reference

https://registry.khronos.org/SPIR-V/papers/WhitePaper.html

'Vulkan' 카테고리의 다른 글

9. Pipeline and Pipeline State  (0) 2024.07.08
8. Vertex Buffer and Memory  (0) 2024.07.05
6. RenderPass  (0) 2024.07.01
5. Presentation And Synchronize ( Fence, Semaphore, Event )  (0) 2024.06.27
4. Pipeline Barrier And Image Layout  (0) 2024.06.24