본문 바로가기

Vulkan

Graphics Pipeline basics - Shader modules


이전 글 : Graphics Pipeline basics
다음 글 : Fixed function

Shader modules

쉐이더 모듈

이전 API와 달리 Vulkan의 쉐이더 코드는 GLSL 및 HLSL과 같이 사람이 읽을 수 있는 구문이 아닌 바이트 코드 형식으로 준비 되어야 합니다. 이 바이트 코드 형식은 SPIR-V라고하며 Vulkan 및 OpenCL (둘 다 Khronos API)과 함께 사용하도록 설계되었습니다. 그래픽을 작성하고 셰이더를 계산하는 데 사용할 수 있는 형식이지만 이 튜토리얼에서는 Vulkan의 그래픽 파이프 라인에 사용 된 셰이더에 초점을 맞출 것입니다.


바이트 코드 형식을 사용하는 이점은 GPU 공급 업체가 쉐이더 코드를 이진코드로 바꾸기 위해 작성한 컴파일러가 훨씬 덜 복잡하다는 것 입니다. 과거에는 GLSL과 같이 인간이 읽을 수 있는 구문을 사용하여 일부 GPU 공급 업체가 표준에 대한 해석이 다소 유연했다는 것을 알 수 있었습니다. 이러한 공급 업체 중 하나에서 GPU가 있는 사소한 쉐이더를 작성하는 경우 구문 오류로 인해 다른 공급 업체의 드라이버가 코드를 거부하거나 컴파일러 버그로 인해 쉐이더가 다르게 실행될 위험이 있습니다. SPIR-V와 같은 간단한 바이트 코드 형식을 사용하면 이러한 문제를 피할 수 있습니다.


그러나 이것이 우리가 직접 이 바이트 코드를 작성해야 한다는 것을 의미하지는 않습니다. Khronos는 GLSL을 SPIR-V로 컴파일하는 벤더 독립적 인 컴파일러를 출시했습니다. 이 컴파일러는 쉐이더 코드가 표준을 완벽하게 준수하는지 확인하고 프로그램과 함께 제공되는 컴파일러가 SPIR-V 바이너리를 생성해 줍니다. 이 컴파일러를 런타임에 SPIR-V를 생성하는 라이브러리로 포함시킬 수도 있지만,이 튜토리얼에서는 이 컴파일러를 사용하지 않을 것 입니다. 컴파일러는 이미 LunarG SDK에 glslangValidator.exe로 포함되어 있으므로 추가로 다운로드 할 필요가 없습니다.


GLSL은 C 스타일 구문을 사용하는 쉐이딩 언어입니다. 작성된 프로그램에는 모든 오브젝트에 대해 호출되는 주요 기능이 있습니다. GLSL은 입력에 매개 변수를 사용하고 출력으로 반환 값을 사용하는 대신 입력 및 출력을 처리하기 위해 전역 변수를 사용합니다.

이 언어에는 내장 된 벡터 및 행렬 기본 요소와 같은 그래픽 프로그래밍을 지원하는 많은 기능이 포함되어 있습니다. .벡터 곱, 행렬 - 벡터 곱 및 벡터 주위의 반사와 같은 연산을 위한 함수가 포함됩니다. 벡터 유형은 요소의 갯수를 나타내는 숫자와 함께 vec 를 사용합니다. 예를 들어, 3D 위치는 vec3에 저장 됩니다. .x와 같은 멤버를 통해 단일 구성 요소에 액세스 할 수도 있지만 동시에 여러 구성 요소에서 새 벡터를 만들 수도 있습니다. 예를 들어, 표현식 vec3 (1.0, 2.0, 3.0) .xy는 vec2가 됩니다. 벡터의 생성자는 벡터 객체와 스칼라 값의 조합을 취할 수도 있습니다. 예를 들어, vec3은 vec3 (vec2 (1.0, 2.0), 3.0)을 사용하여 생성 할 수 있습니다.


이전 장에서 언급했듯이 정점 셰이더와 프래그먼트 셰이더를 작성하여 화면에 삼각형을 출력 해야 합니다. 다음 두 섹션은 각각의 GLSL 코드를 다루고 두 개의 SPIR-V 바이너리를 생성하여 프로그램에 로드하는 방법을 보여줄 것입니다.





Vertex shader

정점 쉐이더, 버텍스 쉐이더


버텍스 쉐이더는 들어오는 각 정점을 처리합니다. 월드 위치, 색상, 법선 및 텍스처 좌표와 같은 속성을 입력으로 사용합니다. 출력은 클립 좌표의 마지막 위치이며 색상 및 텍스처 좌표와 같이 프래그먼트 셰이더로 전달 되어야 하는 속성입니다. 이 값들은 래스터 라이저에 의해 파편들 위에 보간되어 부드러운 그라디언트가 생성됩니다.


클립 좌표는 버텍스 쉐이더의 4 차원 벡터 이며 전체 벡터를 마지막 구성 요소로 나눠 정규화 된 장치 좌표로 바뀝니다. 이러한 정규화 된 장치 좌표는 프레임 버퍼를 [-1, 1] x [-1,1] 좌표계로 매핑하는 동차좌표 입니다.



이전에 컴퓨터 그래픽을 해보셨다면 이미 익숙해 져 있을 것입니다. 또한 이전에 OpenGL을 사용한 적이 있다면 Y 좌표의 부호가 뒤집혀 있음을 알 수 있습니다. Z 좌표는, Direct3D와 같게, 0 ~ 1의 범위를 사용한다


우리의 첫 번째 삼각형에 대해서는 변환을 적용하지 않을 것이며, 세 개의 정점의 위치를 정규화 된 장치 좌표로 직접 지정하여 다음 모양을 생성합니다.





마지막 요소(.w) 를 1로 설정 한 버텍스 쉐이더에서 클립 좌표로 출력하여 정규화 된 장치 좌표를 직접 출력 할 수 있습니다. 이렇게 하면 클립 좌표를 정규화 된 장치 좌표로 변환하는 부분에서 아무 것도 바뀌지 않습니다.


일반적으로 이러한 좌표는 정점 버퍼에 저장되지만 Vulkan에 정점 버퍼를 만들고 데이터로 채우는 것은 쉽지 않습니다. 따라서 삼각형 모양을 화면에서 볼 수 있게 될 때까지 미루기로 했습니다. 한편으로는 조금 정통성이 없는 작업을 수행 할 것입니다. 좌표를 정점 셰이더 내부에 직접 포함 시키십시오. 코드는 다음과 같습니다.


#version 450
#extension GL_ARB_separate_shader_objects : enable

out gl_PerVertex {
   vec4 gl_Position;
};

vec2 positions[3] = vec2[](
   vec2(0.0, -0.5),
   vec2(0.5, 0.5),
   vec2(-0.5, 0.5)
);

void main() {
   gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
}


main 함수는 모든 정점에 대해 호출됩니다. 내장 된 gl_VertexIndex 변수는 현재 정점의 인덱스를 포함합니다. 이것은 보통 정점 버퍼에 대한 인덱스이지만, 여기서는 정점 데이터의 하드 코딩 된 배열에 대한 인덱스가 됩니다. 각 꼭지점의 위치는 셰이더의 상수 배열에서 액세스되고 더미 z 및 w 구성 요소와 결합되어 클립 좌표에서 위치를 생성합니다. 기본 제공 변수 gl_Position은 출력으로 사용됩니다. Vulkan 쉐이더가 작동하려면 GL_ARB_separate_shader_objects 확장 기능이 필요합니다.






Fragment shader

프래그먼트 쉐이더


버텍스 쉐이더의 위치에 의해 형성된 삼각형은 스크린상의 영역을 조각으로 채 웁니다. 프래그먼트 쉐이더는 이러한 프래그먼트에서 호출되어 프레임 버퍼의 색상과 깊이를 생성합니다. 전체 삼각형에 대해 빨간색을 출력하는 간단한 프래그먼트 쉐이더는 다음과 같습니다.


#version 450
#extension GL_ARB_separate_shader_objects : enable

layout(location = 0) out vec4 outColor;

void main() {
   outColor = vec4(1.0, 0.0, 0.0, 1.0);
}



main 함수는 버텍스 쉐이더에서 모든 정점에 대해 정점 쉐이더의 main 함수가 호출되는 것처럼 모든 조각에 대해 호출됩니다. GLSL의 색은 [0, 1] 범위 내의 R, G, B 및 알파 채널이있는 4 성분 벡터입니다. 버텍스 쉐이더의 gl_Position과 달리, 현재 프래그먼트의 색상을 출력하는 내장 변수는 없습니다. layout (location = 0) 수정자가 프레임 버퍼의 인덱스를 지정하는 각 프레임 버퍼에 대해 자신 만의 출력 변수를 지정해야 합니다. 빨강 색은 인덱스 0에있는 첫 번째 (그리고 유일한) 프레임 버퍼에 연결된 이 outColor 변수에 쓰여집니다.




Per-vertex colors

각 정점별 색상


전체 삼각형을 빨간색으로 만드는 것은 별로 흥미롭지 않습니다. 다음과 같은 모양이 훨씬 좋을까요?

이를 위해서는 두 쉐이더를 몇 가지 변경해야 합니다. 먼저, 세 개의 꼭지점마다 고유 한 색을 지정해야 합니다. 정점 쉐이더는 이제 위치와 마찬가지로 색상이있는 배열을 포함해야 합니다.


vec3 colors[3] = vec3[](
   vec3(1.0, 0.0, 0.0),
   vec3(0.0, 1.0, 0.0),
   vec3(0.0, 0.0, 1.0)
);


이제 우리는 이러한 꼭지점 별 색상을 프래그먼트 쉐이더에 전달하여 보간 된 값을 프레임 버퍼에 출력 할 수 있습니다. 정점 쉐이더에 컬러 출력 부분을 추가하고 main 함수 작성 합니다. :


layout(location = 0) out vec3 fragColor;

void main() {
   gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
   fragColor = colors[gl_VertexIndex];
}


다음으로 프래그먼트 셰이더에 일치하는 입력을 추가해야 합니다.


layout(location = 0) in vec3 fragColor;

void main() {
   outColor = vec4(fragColor, 1.0);
}


입력 변수가 반드시 동일한 이름을 사용할 필요는 없으며, 위치 지정 문에 의해 지정된 색인을 사용하여 함께 링크됩니다. 주요 기능은 알파 값과 함께 색상을 출력하도록 수정되었습니다. 위 이미지에서 볼 수 있듯이 fragColor의 값은 세 꼭지점 사이의 조각에 대해 자동으로 보간되어 부드럽게 그라디언트됩니다.



Compiling the shaders


프로젝트의 루트 디렉토리에 shaders라는 디렉토리를 만들고 shader.vert라는 파일에 정점 셰이더를 저장하고 해당 디렉토리의 shader.frag 라는 파일에 프래그먼트 쉐이더를 작성 합니다.. GLSL 쉐이더는 공식적으로 이러한 구별을 하라고 하지 않지만 이 두 가지 기능을 구별하기 위해 일반적으로 이렇게 사용 합니다.


shader.vert의 내용은 다음과 같습니다.


#version 450
#extension GL_ARB_separate_shader_objects : enable

out gl_PerVertex {
   vec4 gl_Position;
};

layout(location = 0) out vec3 fragColor;

vec2 positions[3] = vec2[](
   vec2(0.0, -0.5),
   vec2(0.5, 0.5),
   vec2(-0.5, 0.5)
);

vec3 colors[3] = vec3[](
   vec3(1.0, 0.0, 0.0),
   vec3(0.0, 1.0, 0.0),
   vec3(0.0, 0.0, 1.0)
);

void main() {
   gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
   fragColor = colors[gl_VertexIndex];
}



shader.frag의 내용은 다음과 같습니다.


#version 450
#extension GL_ARB_separate_shader_objects : enable

layout(location = 0) in vec3 fragColor;

layout(location = 0) out vec4 outColor;

void main() {
   outColor = vec4(fragColor, 1.0);
}


우리는 glslangValidator 프로그램을 사용하여 이것을 SPIR-V 바이트 코드로 컴파일 할 것입니다.









Windows


다음과 같은 내용으로 compile.bat 파일을 작성 하십시오.


C:/VulkanSDK/1.0.17.0/Bin32/glslangValidator.exe -V shader.vert
C:/VulkanSDK/1.0.17.0/Bin32/glslangValidator.exe -V shader.frag
pause


위의 배치파일에서 glslangValidator.exe의 경로를 자신의 Vulkan SDK를 설치 한 경로로 바꾸십시오. 파일을 두 번 클릭하여 실행하십시오.



Linux


다음 내용으로 compile.sh 파일을 작성하십시오.


/home/user/VulkanSDK/x.x.x.x/x86_64/bin/glslangValidator -V shader.vert
/home/user/VulkanSDK/x.x.x.x/x86_64/bin/glslangValidator -V shader.frag


glslangValidator의 경로를 Vulkan SDK를 설치 한 경로로 바꿉니다. 스크립트를 chmod + x compile.sh로 실행 가능하게 만들고 실행하십시오.




플랫폼 별 지침의 끝


이 두 명령어는 GLSL 소스 파일을 SPIR-V 바이트 코드로 컴파일 하도록 알려주는 -V 플래그 옵션으로 컴파일러를 수행합니다. 컴파일 스크립트를 실행하면 vert.spv와 frag.spv라는 두 개의 SPIR-V 바이너리가 생성됩니다. 이름은 셰이더 유형에서 자동으로 파생되지만 이름을 원하는대로 변경할 수 있습니다. 셰이더를 컴파일 할 때 누락 된 기능에 대한 경고가 표시 될 수 있지만 무시해도 됩니다.


셰이더에 구문 오류가 있으면 컴파일러에서 줄 번호와 문제를 알려줍니다. 예를 들어 세미콜론을 사용하지 않고 컴파일 스크립트를 다시 실행 해 보십시오. 컴파일러 실행 옵션을 보기 위해서는 어떤 인자 입력도 없이 컴파일러를 실행하여 지원하는 플래그의 종류를 확인 할 수 있습니다. 예를 들어, 바이트 코드를 사람이 읽을 수있는 형식으로 출력하여 쉐이더가 수행중인 작업과 이 단계에서 적용된 최적화 작업을 확인해 볼 수도 있습니다.









Loading a shader

쉐이더 로딩


이제 우리는 SPIR-V 쉐이더를 생성 할 수 있는 방법을 알게 되었으므로, 프로그램에 로드하여 그래픽 파이프 라인에 연결해야 합니다. 먼저 파일에서 이진 데이터를 로드하는 간단한 도우미 함수를 작성합니다.


#include <fstream>

...

static std::vector<char> readFile(const std::string& filename) {
   std::ifstream file(filename, std::ios::ate | std::ios::binary);

   if (!file.is_open()) {
       throw std::runtime_error("failed to open file!");
   }
}


readFile 함수는 지정된 파일에서 모든 바이트를 읽고 std :: vector로 관리되는 바이트 배열로 반환합니다. 먼저 두 개의 플래그로 파일을 열어 보겠습니다.


  • ate : 파일의 끝에서 읽기 시작

  • binary : 파일을 바이너리 파일로 읽음 (혹시 모를 텍스트 변환을 피하기 위해)


ate 옵션을 사용하여 파일의 끝으로 보내는 것은 장점은 파일의 크기를 검사하여 읽기 위한 버퍼를 할당하기 위한 것 입니다.


size_t fileSize = (size_t) file.tellg();
std::vector<char> buffer(fileSize);



그 후, 우리는 파일의 시작 부분으로 되돌아 가서 한 번에 모든 바이트를 읽을 수 있습니다 :


file.seekg(0);
file.read(buffer.data(), fileSize);


마지막으로 파일을 닫고 바이트를 반환합니다.


file.close();

return buffer;


이제이 함수를 createGraphicsPipeline에서 호출하여 두 쉐이더의 바이트 코드를 로드합니다.


void createGraphicsPipeline() {
   auto vertShaderCode = readFile("shaders/vert.spv");
   auto fragShaderCode = readFile("shaders/frag.spv");
}


버퍼 크기를 출력하고 실제 파일 크기 (바이트)와 일치하는지 확인하여 셰이더가 올바르게 로드 되었는지 확인하십시오.




Creating a shader module is simple, we only need to specify a pointer to the buffer with the bytecode and the length of it. This information is specified in a VkShaderModuleCreateInfo structure. The one catch is that the size of the bytecode is specified in bytes, but the bytecode pointer is a uint32_t pointer rather than a char pointer. Therefore we will need to cast the pointer with reinterpret_cast as shown below. When you perform a cast like this, you also need to ensure that the data satisfies the alignment requirements of uint32_t. Lucky for us, the data is stored in an std::vector where the default allocator already ensures that the data satisfies the worst case alignment requirements.

Creating shader modules


코드를 파이프 라인에 전달하기 전에 VkShaderModule 객체로 코드를 래핑해야 합니다. 이를 위해 createShaderModule이라는 헬퍼 함수를 만들어 보겠습니다.


VkShaderModule createShaderModule(const std::vector<char>& code) {

}


이 함수는 바이트 코드가 있는 버퍼를 매개 변수로 사용하여 VkShaderModule 을 만들 것 입니다..


셰이더 모듈을 만드는 것은 간단합니다. 바이트 코드와 길이를 가진 버퍼에 대한 포인터 만 지정하면 됩니다. 이 정보는 VkShaderModuleCreateInfo 구조체에 지정됩니다. 하나의 캐치는 바이트 코드의 크기가 바이트로 지정 되지만 바이트 코드 포인터는 char 포인터가 아니라 uint32_t 포인터입니다. 따라서 아래와 같이 reinterpret_cast를 사용하여 포인터를 캐스팅해야합니다. 이와 같은 캐스트를 수행 할 때 데이터가 uint32_t의 정렬 요구 사항을 충족하는지 확인해야 합니다. 운 좋게도 데이터는 std :: vector에 저장되어 있기 때문에 에 컨테이너가 자체의 기본 할당자가 이미 입력되는 데이터가 적합한 데이터 정렬 요구 형식  사항에  충족하는지 확인할 것 입니다.


VkShaderModuleCreateInfo createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
createInfo.codeSize = code.size();
createInfo.pCode = reinterpret_cast<const uint32_t*>(code.data());


그런 다음 vkCreateShaderModule 을 호출하여 VkShaderModule 을 만들 수 있습니다.


VkShaderModule shaderModule;
if (vkCreateShaderModule(device, &createInfo, nullptr, &shaderModule) != VK_SUCCESS) {
   throw std::runtime_error("failed to create shader module!");
}


매개 변수는 이전 개체 생성 함수의 매개 변수와 동일합니다. 논리 장치, 정보 구조 만들기 포인터, 사용자 지정 할당 자에 대한 선택적 포인터 및 출력 변수 처리. 코드가 있는 버퍼는 셰이더 모듈을 만든 직후에 해제 할 수 있습니다. 생성 된 셰이더 모듈을 반환하는 것을 잊지 마십시오.


return shaderModule;



셰이더 모듈 객체는 파이프 라인 생성 과정에서만 필요하므로 클래스 멤버로 선언하는 대신 createGraphicsPipeline 함수에서 로컬 변수로 만듭니다.


VkShaderModule vertShaderModule;
VkShaderModule fragShaderModule;


셰이더 모듈을 로드하기 위해 생성 한 도우미 함수를 호출하십시오.


vertShaderModule = createShaderModule(vertShaderCode);
fragShaderModule = createShaderModule(fragShaderCode);


그래픽 파이프 라인이 생성되고 createGraphicsPipeline 이 반환 될 때 이들을 정리해야 하므로 함수의 끝에서 삭제 합니다.


  ...
   vkDestroyShaderModule(device, fragShaderModule, nullptr);
   vkDestroyShaderModule(device, vertShaderModule, nullptr);
}







Shader stage creation

쉐이더 스테이지 생성


VkShaderModule 객체는 바이트 코드 버퍼를 둘러싼 단순한 래퍼입니다. 셰이더는 아직 서로 연결되어 있지 않으며 아직 목적이 주어지지 않았습니다. 셰이더 모듈을 파이프 라인의 버텍스 또는 프래그먼트 셰이더 스테이지에 할당하는 것은 실제 파이프 라인 생성 프로세스의 일부인 VkPipelineShaderStageCreateInfo 구조를 통해 이루어집니다.


먼저 createGraphicsPipeline 함수에서 정점 쉐이더의 구조를 채워 보겠습니다.


VkPipelineShaderStageCreateInfo vertShaderStageInfo = {};
vertShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
vertShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT;


의무적으로 사용하는  sType 멤버 외에 첫 번째 단계는 Vulkan에게 파이프라인 단계에서 쉐이더가 사용될 것임을 알리는 것입니다. 이전 장에서 설명 된 프로그램 가능한 스테이지에 대한 enum 값이 있었습니다.


vertShaderStageInfo.module = vertShaderModule;
vertShaderStageInfo.pName = "main";


다음 두 멤버는 코드가 포함 된 쉐이더 모듈과 호출 할 함수를 지정합니다. 즉, 여러 버텍스 쉐이더를 하나의 쉐이더 모듈에 결합하고 각기 다른 진입 점을 사용하여 동작을 구별 할 수 있습니다. 이 경우 우리는 흔히 사용하는 “main” 함수를 사용할 것 입니다.


추가로 (선택 사항) 멤버 인 pSpecializationInfo 가 하나 있는데 여기서는 사용하지 않지만 논의 할 가치가 있습니다. 여기에는 쉐이더 상수의 값을 지정할 수 있습니다. 하나의 쉐이더 모듈을 사용할 수 있는데 이는 쉐이더 모듈에서 사용되는 상수에 대해 다른 값을 지정하여 파이프 라인 생성시 동작을 구성 할 수 있습니다. 이것은 렌더링시 변수를 사용하여 셰이더를 구성하는 것보다 효율적입니다. 컴파일러는 이러한 값에 의존하는 if 문을 제거하는 것과 같은 최적화를 수행 할 수 있기 때문입니다. 그런 상수가 없다면 멤버를 nullptr로 설정할 수 있습니다.이 구조체 초기화는 자동으로 수행됩니다



이를 기반으로 프래그먼트 쉐이더에 맞도록 구조문를 수정하는 것은 쉽습니다.


VkPipelineShaderStageCreateInfo fragShaderStageInfo = {};
fragShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
fragShaderStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
fragShaderStageInfo.module = fragShaderModule;
fragShaderStageInfo.pName = "main";


이 두 구조체를 포함하는 배열을 정의하여 마무리 합니다. 나중에 이 구조체를 실제 파이프 라인 작성 단계에서 참조하는 데 사용합니다.


VkPipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo};


여기서는 파이프 라인의 프로그램 가능한 단계를 설명하는 것이 전부입니다. 다음 장에서는 고정 함수 단계를 살펴볼 것입니다.



버텍스 쉐이더 파일 입니다.

#version 450
#extension GL_ARB_separate_shader_objects : enable

out gl_PerVertex {
   vec4 gl_Position;
};

layout(location = 0) out vec3 fragColor;

vec2 positions[3] = vec2[](
   vec2(0.0, -0.5),
   vec2(0.5, 0.5),
   vec2(-0.5, 0.5)
);

vec3 colors[3] = vec3[](
   vec3(1.0, 0.0, 0.0),
   vec3(0.0, 1.0, 0.0),
   vec3(0.0, 0.0, 1.0)
);

void main() {
   gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
   fragColor = colors[gl_VertexIndex];
}



프래그먼트 쉐이더 파일 입니다.

#version 450
#extension GL_ARB_separate_shader_objects : enable

layout(location = 0) in vec3 fragColor;

layout(location = 0) out vec4 outColor;

void main() {
   outColor = vec4(fragColor, 1.0);
}



C++ 코드 입니다.

#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>

#include <iostream>
#include <fstream>
#include <stdexcept>
#include <algorithm>
#include <vector>
#include <cstring>
#include <set>

const int WIDTH = 800;
const int HEIGHT = 600;

const std::vector<const char*> validationLayers = {
   "VK_LAYER_LUNARG_standard_validation"
};

const std::vector<const char*> deviceExtensions = {
   VK_KHR_SWAPCHAIN_EXTENSION_NAME
};

#ifdef NDEBUG
const bool enableValidationLayers = false;
#else
const bool enableValidationLayers = true;
#endif

VkResult CreateDebugReportCallbackEXT(VkInstance instance, const VkDebugReportCallbackCreateInfoEXT* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDebugReportCallbackEXT* pCallback) {
   auto func = (PFN_vkCreateDebugReportCallbackEXT) vkGetInstanceProcAddr(instance, "vkCreateDebugReportCallbackEXT");
   if (func != nullptr) {
       return func(instance, pCreateInfo, pAllocator, pCallback);
   } else {
       return VK_ERROR_EXTENSION_NOT_PRESENT;
   }
}

void DestroyDebugReportCallbackEXT(VkInstance instance, VkDebugReportCallbackEXT callback, const VkAllocationCallbacks* pAllocator) {
   auto func = (PFN_vkDestroyDebugReportCallbackEXT) vkGetInstanceProcAddr(instance, "vkDestroyDebugReportCallbackEXT");
   if (func != nullptr) {
       func(instance, callback, pAllocator);
   }
}

struct QueueFamilyIndices {
   int graphicsFamily = -1;
   int presentFamily = -1;

   bool isComplete() {
       return graphicsFamily >= 0 && presentFamily >= 0;
   }
};

struct SwapChainSupportDetails {
   VkSurfaceCapabilitiesKHR capabilities;
   std::vector<VkSurfaceFormatKHR> formats;
   std::vector<VkPresentModeKHR> presentModes;
};

class HelloTriangleApplication {
public:
   void run() {
       initWindow();
       initVulkan();
       mainLoop();
       cleanup();
   }

private:
   GLFWwindow* window;

   VkInstance instance;
   VkDebugReportCallbackEXT callback;
   VkSurfaceKHR surface;

   VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
   VkDevice device;

   VkQueue graphicsQueue;
   VkQueue presentQueue;

   VkSwapchainKHR swapChain;
   std::vector<VkImage> swapChainImages;
   VkFormat swapChainImageFormat;
   VkExtent2D swapChainExtent;
   std::vector<VkImageView> swapChainImageViews;

   void initWindow() {
       glfwInit();

       glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
       glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);

       window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
   }

   void initVulkan() {
       createInstance();
       setupDebugCallback();
       createSurface();
       pickPhysicalDevice();
       createLogicalDevice();
       createSwapChain();
       createImageViews();
       createGraphicsPipeline();
   }

   void mainLoop() {
       while (!glfwWindowShouldClose(window)) {
           glfwPollEvents();
       }
   }

   void cleanup() {
       for (auto imageView : swapChainImageViews) {
           vkDestroyImageView(device, imageView, nullptr);
       }

       vkDestroySwapchainKHR(device, swapChain, nullptr);
       vkDestroyDevice(device, nullptr);
       DestroyDebugReportCallbackEXT(instance, callback, nullptr);
       vkDestroySurfaceKHR(instance, surface, nullptr);
       vkDestroyInstance(instance, nullptr);

       glfwDestroyWindow(window);

       glfwTerminate();
   }

   void createInstance() {
       if (enableValidationLayers && !checkValidationLayerSupport()) {
           throw std::runtime_error("validation layers requested, but not available!");
       }

       VkApplicationInfo appInfo = {};
       appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
       appInfo.pApplicationName = "Hello Triangle";
       appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
       appInfo.pEngineName = "No Engine";
       appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
       appInfo.apiVersion = VK_API_VERSION_1_0;

       VkInstanceCreateInfo createInfo = {};
       createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
       createInfo.pApplicationInfo = &appInfo;

       auto extensions = getRequiredExtensions();
       createInfo.enabledExtensionCount = static_cast<uint32_t>(extensions.size());
       createInfo.ppEnabledExtensionNames = extensions.data();

       if (enableValidationLayers) {
           createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());
           createInfo.ppEnabledLayerNames = validationLayers.data();
       } else {
           createInfo.enabledLayerCount = 0;
       }

       if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) {
           throw std::runtime_error("failed to create instance!");
       }
   }

   void setupDebugCallback() {
       if (!enableValidationLayers) return;

       VkDebugReportCallbackCreateInfoEXT createInfo = {};
       createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_REPORT_CALLBACK_CREATE_INFO_EXT;
       createInfo.flags = VK_DEBUG_REPORT_ERROR_BIT_EXT | VK_DEBUG_REPORT_WARNING_BIT_EXT;
       createInfo.pfnCallback = debugCallback;

       if (CreateDebugReportCallbackEXT(instance, &createInfo, nullptr, &callback) != VK_SUCCESS) {
           throw std::runtime_error("failed to set up debug callback!");
       }
   }

   void createSurface() {
       if (glfwCreateWindowSurface(instance, window, nullptr, &surface) != VK_SUCCESS) {
           throw std::runtime_error("failed to create window surface!");
       }
   }

   void pickPhysicalDevice() {
       uint32_t deviceCount = 0;
       vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr);

       if (deviceCount == 0) {
           throw std::runtime_error("failed to find GPUs with Vulkan support!");
       }

       std::vector<VkPhysicalDevice> devices(deviceCount);
       vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data());

       for (const auto& device : devices) {
           if (isDeviceSuitable(device)) {
               physicalDevice = device;
               break;
           }
       }

       if (physicalDevice == VK_NULL_HANDLE) {
           throw std::runtime_error("failed to find a suitable GPU!");
       }
   }

   void createLogicalDevice() {
       QueueFamilyIndices indices = findQueueFamilies(physicalDevice);

       std::vector<VkDeviceQueueCreateInfo> queueCreateInfos;
       std::set<int> uniqueQueueFamilies = {indices.graphicsFamily, indices.presentFamily};

       float queuePriority = 1.0f;
       for (int queueFamily : uniqueQueueFamilies) {
           VkDeviceQueueCreateInfo queueCreateInfo = {};
           queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
           queueCreateInfo.queueFamilyIndex = queueFamily;
           queueCreateInfo.queueCount = 1;
           queueCreateInfo.pQueuePriorities = &queuePriority;
           queueCreateInfos.push_back(queueCreateInfo);
       }

       VkPhysicalDeviceFeatures deviceFeatures = {};

       VkDeviceCreateInfo createInfo = {};
       createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;

       createInfo.queueCreateInfoCount = static_cast<uint32_t>(queueCreateInfos.size());
       createInfo.pQueueCreateInfos = queueCreateInfos.data();

       createInfo.pEnabledFeatures = &deviceFeatures;

       createInfo.enabledExtensionCount = static_cast<uint32_t>(deviceExtensions.size());
       createInfo.ppEnabledExtensionNames = deviceExtensions.data();

       if (enableValidationLayers) {
           createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());
           createInfo.ppEnabledLayerNames = validationLayers.data();
       } else {
           createInfo.enabledLayerCount = 0;
       }

       if (vkCreateDevice(physicalDevice, &createInfo, nullptr, &device) != VK_SUCCESS) {
           throw std::runtime_error("failed to create logical device!");
       }

       vkGetDeviceQueue(device, indices.graphicsFamily, 0, &graphicsQueue);
       vkGetDeviceQueue(device, indices.presentFamily, 0, &presentQueue);
   }

   void createSwapChain() {
       SwapChainSupportDetails swapChainSupport = querySwapChainSupport(physicalDevice);

       VkSurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(swapChainSupport.formats);
       VkPresentModeKHR presentMode = chooseSwapPresentMode(swapChainSupport.presentModes);
       VkExtent2D extent = chooseSwapExtent(swapChainSupport.capabilities);

       uint32_t imageCount = swapChainSupport.capabilities.minImageCount + 1;
       if (swapChainSupport.capabilities.maxImageCount > 0 && imageCount > swapChainSupport.capabilities.maxImageCount) {
           imageCount = swapChainSupport.capabilities.maxImageCount;
       }

       VkSwapchainCreateInfoKHR createInfo = {};
       createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
       createInfo.surface = surface;

       createInfo.minImageCount = imageCount;
       createInfo.imageFormat = surfaceFormat.format;
       createInfo.imageColorSpace = surfaceFormat.colorSpace;
       createInfo.imageExtent = extent;
       createInfo.imageArrayLayers = 1;
       createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;

       QueueFamilyIndices indices = findQueueFamilies(physicalDevice);
       uint32_t queueFamilyIndices[] = {(uint32_t) indices.graphicsFamily, (uint32_t) indices.presentFamily};

       if (indices.graphicsFamily != indices.presentFamily) {
           createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
           createInfo.queueFamilyIndexCount = 2;
           createInfo.pQueueFamilyIndices = queueFamilyIndices;
       } else {
           createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
       }

       createInfo.preTransform = swapChainSupport.capabilities.currentTransform;
       createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
       createInfo.presentMode = presentMode;
       createInfo.clipped = VK_TRUE;

       createInfo.oldSwapchain = VK_NULL_HANDLE;

       if (vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapChain) != VK_SUCCESS) {
           throw std::runtime_error("failed to create swap chain!");
       }

       vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr);
       swapChainImages.resize(imageCount);
       vkGetSwapchainImagesKHR(device, swapChain, &imageCount, swapChainImages.data());

       swapChainImageFormat = surfaceFormat.format;
       swapChainExtent = extent;
   }

   void createImageViews() {
       swapChainImageViews.resize(swapChainImages.size());

       for (size_t i = 0; i < swapChainImages.size(); i++) {
           VkImageViewCreateInfo createInfo = {};
           createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
           createInfo.image = swapChainImages[i];
           createInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
           createInfo.format = swapChainImageFormat;
           createInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;
           createInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;
           createInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;
           createInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;
           createInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
           createInfo.subresourceRange.baseMipLevel = 0;
           createInfo.subresourceRange.levelCount = 1;
           createInfo.subresourceRange.baseArrayLayer = 0;
           createInfo.subresourceRange.layerCount = 1;

           if (vkCreateImageView(device, &createInfo, nullptr, &swapChainImageViews[i]) != VK_SUCCESS) {
               throw std::runtime_error("failed to create image views!");
           }
       }
   }

   void createGraphicsPipeline() {
       auto vertShaderCode = readFile("shaders/vert.spv");
       auto fragShaderCode = readFile("shaders/frag.spv");

       VkShaderModule vertShaderModule = createShaderModule(vertShaderCode);
       VkShaderModule fragShaderModule = createShaderModule(fragShaderCode);

       VkPipelineShaderStageCreateInfo vertShaderStageInfo = {};
       vertShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
       vertShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT;
       vertShaderStageInfo.module = vertShaderModule;
       vertShaderStageInfo.pName = "main";

       VkPipelineShaderStageCreateInfo fragShaderStageInfo = {};
       fragShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
       fragShaderStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
       fragShaderStageInfo.module = fragShaderModule;
       fragShaderStageInfo.pName = "main";

       VkPipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo};

       vkDestroyShaderModule(device, fragShaderModule, nullptr);
       vkDestroyShaderModule(device, vertShaderModule, nullptr);
   }

   VkShaderModule createShaderModule(const std::vector<char>& code) {
       VkShaderModuleCreateInfo createInfo = {};
       createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
       createInfo.codeSize = code.size();
       createInfo.pCode = reinterpret_cast<const uint32_t*>(code.data());

       VkShaderModule shaderModule;
       if (vkCreateShaderModule(device, &createInfo, nullptr, &shaderModule) != VK_SUCCESS) {
           throw std::runtime_error("failed to create shader module!");
       }

       return shaderModule;
   }

   VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector<VkSurfaceFormatKHR>& availableFormats) {
       if (availableFormats.size() == 1 && availableFormats[0].format == VK_FORMAT_UNDEFINED) {
           return {VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR};
       }

       for (const auto& availableFormat : availableFormats) {
           if (availableFormat.format == VK_FORMAT_B8G8R8A8_UNORM && availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
               return availableFormat;
           }
       }

       return availableFormats[0];
   }

   VkPresentModeKHR chooseSwapPresentMode(const std::vector<VkPresentModeKHR> availablePresentModes) {
       VkPresentModeKHR bestMode = VK_PRESENT_MODE_FIFO_KHR;

       for (const auto& availablePresentMode : availablePresentModes) {
           if (availablePresentMode == VK_PRESENT_MODE_MAILBOX_KHR) {
               return availablePresentMode;
           } else if (availablePresentMode == VK_PRESENT_MODE_IMMEDIATE_KHR) {
               bestMode = availablePresentMode;
           }
       }

       return bestMode;
   }

   VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) {
       if (capabilities.currentExtent.width != std::numeric_limits<uint32_t>::max()) {
           return capabilities.currentExtent;
       } else {
           VkExtent2D actualExtent = {WIDTH, HEIGHT};

           actualExtent.width = std::max(capabilities.minImageExtent.width, std::min(capabilities.maxImageExtent.width, actualExtent.width));
           actualExtent.height = std::max(capabilities.minImageExtent.height, std::min(capabilities.maxImageExtent.height, actualExtent.height));

           return actualExtent;
       }
   }

   SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice device) {
       SwapChainSupportDetails details;

       vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &details.capabilities);

       uint32_t formatCount;
       vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr);

       if (formatCount != 0) {
           details.formats.resize(formatCount);
           vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, details.formats.data());
       }

       uint32_t presentModeCount;
       vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, nullptr);

       if (presentModeCount != 0) {
           details.presentModes.resize(presentModeCount);
           vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, details.presentModes.data());
       }

       return details;
   }

   bool isDeviceSuitable(VkPhysicalDevice device) {
       QueueFamilyIndices indices = findQueueFamilies(device);

       bool extensionsSupported = checkDeviceExtensionSupport(device);

       bool swapChainAdequate = false;
       if (extensionsSupported) {
           SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device);
           swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty();
       }

       return indices.isComplete() && extensionsSupported && swapChainAdequate;
   }

   bool checkDeviceExtensionSupport(VkPhysicalDevice device) {
       uint32_t extensionCount;
       vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, nullptr);

       std::vector<VkExtensionProperties> availableExtensions(extensionCount);
       vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, availableExtensions.data());

       std::set<std::string> requiredExtensions(deviceExtensions.begin(), deviceExtensions.end());

       for (const auto& extension : availableExtensions) {
           requiredExtensions.erase(extension.extensionName);
       }

       return requiredExtensions.empty();
   }

   QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) {
       QueueFamilyIndices indices;

       uint32_t queueFamilyCount = 0;
       vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr);

       std::vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);
       vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, queueFamilies.data());

       int i = 0;
       for (const auto& queueFamily : queueFamilies) {
           if (queueFamily.queueCount > 0 && queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT) {
               indices.graphicsFamily = i;
           }

           VkBool32 presentSupport = false;
           vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport);

           if (queueFamily.queueCount > 0 && presentSupport) {
               indices.presentFamily = i;
           }

           if (indices.isComplete()) {
               break;
           }

           i++;
       }

       return indices;
   }

   std::vector<const char*> getRequiredExtensions() {
       uint32_t glfwExtensionCount = 0;
       const char** glfwExtensions;
       glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);

       std::vector<const char*> extensions(glfwExtensions, glfwExtensions + glfwExtensionCount);

       if (enableValidationLayers) {
           extensions.push_back(VK_EXT_DEBUG_REPORT_EXTENSION_NAME);
       }

       return extensions;
   }

   bool checkValidationLayerSupport() {
       uint32_t layerCount;
       vkEnumerateInstanceLayerProperties(&layerCount, nullptr);

       std::vector<VkLayerProperties> availableLayers(layerCount);
       vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data());

       for (const char* layerName : validationLayers) {
           bool layerFound = false;

           for (const auto& layerProperties : availableLayers) {
               if (strcmp(layerName, layerProperties.layerName) == 0) {
                   layerFound = true;
                   break;
               }
           }

           if (!layerFound) {
               return false;
           }
       }

       return true;
   }

   static std::vector<char> readFile(const std::string& filename) {
       std::ifstream file(filename, std::ios::ate | std::ios::binary);

       if (!file.is_open()) {
           throw std::runtime_error("failed to open file!");
       }

       size_t fileSize = (size_t) file.tellg();
       std::vector<char> buffer(fileSize);

       file.seekg(0);
       file.read(buffer.data(), fileSize);

       file.close();

       return buffer;
   }

   static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback(VkDebugReportFlagsEXT flags, VkDebugReportObjectTypeEXT objType, uint64_t obj, size_t location, int32_t code, const char* layerPrefix, const char* msg, void* userData) {
       std::cerr << "validation layer: " << msg << std::endl;

       return VK_FALSE;
   }
};

int main() {
   HelloTriangleApplication app;

   try {
       app.run();
   } catch (const std::runtime_error& e) {
       std::cerr << e.what() << std::endl;
       return EXIT_FAILURE;
   }

   return EXIT_SUCCESS;
}


이전 글 : Graphics Pipeline basics
다음 글 : Fixed function




'Vulkan' 카테고리의 다른 글

Graphics Pipeline basics - Render passes  (0) 2018.01.24
Graphics Pipeline basics - Fixed function  (0) 2018.01.24
Graphics Pipeline basics - introduction  (0) 2018.01.23
Presentation - Image views  (0) 2018.01.23
Presentation - Swap chain  (1) 2018.01.23