SPIR는 Standard Portable Immediate Representation의 약어 이며,
SPIR-V는 SPIR로 부터 파생 되었다.
SPIR-V가 Vulkan과 OpenCL을 지원하면서 벤더 들은 드라이버 개발과 최적화에 집중을 할 수 있게 되었다.
사람이 읽을수 있는 코드 형식으로 된 OpenGL의 셰이딩 언어와는 달리 Vulkan은 하위 계층의 이진 중간 표현 (IR) 언어인
SPIR-V만을 사용한다.
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
원인은 아래와 같이 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 개체
전체 코드 >
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
'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 |