본문 바로가기

Vulkan

Graphics Pipeline basics - Fixed function

이전 글 : Shader modules

다음 글 : Render passes



Fixed function

예전의 그래픽 API는 그래픽 파이프 라인의 대부분의 각 스테이지에 대한 기본적인 상태를 제공하고 있었습니다. Vulkan에서는 뷰포트 크기에서 색상 혼합 기능에 이르기까지 모든 것에 대해 이러한 상태를 명시해야 합니다. 이 장에서는 이러한 고정 함수 연산을 구성하기 위해 모든 구조체를 채울 것입니다.



Vertex input


VkPipelineVertexInputStateCreateInfo 구조체는 버텍스 쉐이더에 전달 될 정점 데이터의 형식을 기술합니다. 이것은 대략 두 가지 방법으로 이것을 기술합니다 :


  • 바인딩 : 데이터 간 간격과 데이터가 각 정점 또는 각 인스턴스 여부 (인스턴스화 참조)

  • 속성 설명 : 버텍스 쉐이더에 전달 된 속성의 유형, 어떤 바인딩을 로드 할 것인가, 어느 옵셋을 로드 할 것인가


앞서 말했듯이 버텍스 쉐이더에서 버텍스 데이터를 직접 입력 했기 때문에 이 구조체에서는 현재 로드 할 버텍스 데이터가 없음을 지정합니다. 이에 대해서는 버텍스 버퍼 설명에서 다시 살펴 보겠습니다.


VkPipelineVertexInputStateCreateInfo vertexInputInfo = {};
vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
vertexInputInfo.vertexBindingDescriptionCount = 0;
vertexInputInfo.pVertexBindingDescriptions = nullptr; // Optional
vertexInputInfo.vertexAttributeDescriptionCount = 0;
vertexInputInfo.pVertexAttributeDescriptions = nullptr; // Optional


pVertexBindingDescriptions 및 pVertexAttributeDescriptions 멤버는 위에서 언급 한 정점 데이터 로드 세부 정보를 설명하는 구조체 배열을 가리 킵니다. 이 구조체를 shaderStages 배열 바로 뒤에 createGraphicsPipeline 함수에 추가하십시오.





Input assembly


VkPipelineInputAssemblyStateCreateInfo 구조체는 두 가지를 설명합니다. 어떤 종류의 지오메트리가 꼭지점에서 그려지거나 그리고 프리미티브가 다시 시작 했는지 여부입니다. 전자는 토폴로지 구성원에 지정되며 다음과 같은 값을 가질 수 있습니다.


  • VK_PRIMITIVE_TOPOLOGY_POINT_LIST : 정점의 포인트 리스트

  • VK_PRIMITIVE_TOPOLOGY_LINE_LIST : 재사용하지 않는 두개의 꼭지점 선

  • VK_PRIMITIVE_TOPOLOGY_LINE_STRIP : 모든 라인의 끝 정점이 다음 라인의 시작 정점으로 사용됩니다.

  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST : 재사용하지 않고 매 3 정점의 삼각형

  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP : 모든 삼각형의 두 번째 및 세 번째 꼭지점이 다음 삼각형의 처음 두 꼭지점으로 사용됩니다.


일반적으로 정점은 인덱스에 의해 순차적으로 정점 버퍼에서 로드되지만 요소 버퍼를 사용하면 자체적인 인덱스를 사용할 수 있습니다. 이를 통해 정점 재사용과 같은 최적화를 수행 할 수 있습니다. primitiveRestartEnable 멤버를 VK_TRUE로 설정하면 특수 인덱스 0xFFFF 또는 0xFFFFFFFF를 사용하여 _STRIP 토폴로지 모드에서 선과 삼각형을 분할 할 수 있습니다.



이 튜토리얼에서는 삼각형을 그리기 위한 구조에 대해 다음과 같이 설정하여 사용합니다.


VkPipelineInputAssemblyStateCreateInfo inputAssembly = {};
inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
inputAssembly.primitiveRestartEnable = VK_FALSE;





Viewports and scissors

뷰포트와 시저스 (클리핑 개념)


뷰포트는 기본적으로 출력이 렌더링 될 프레임 버퍼의 영역을 말합니다. 이 값은 거의 항상 (0, 0)에서 (너비, 높이)까지 이며 이 강좌 에서도 마찬가지입니다.


VkViewport viewport = {};
viewport.x = 0.0f;
viewport.y = 0.0f;
viewport.width = (float) swapChainExtent.width;
viewport.height = (float) swapChainExtent.height;
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;


스왑 체인의 크기와 이미지는 윈도우의 넓이 및 높이와 다를 수 있음을 기억하십시오. 스왑 체인 이미지는 나중에 프레임 버퍼로 사용되므로 크기를 유지해야 합니다.


minDepth 및 maxDepth 값은 프레임 버퍼에 사용할 심도 값의 범위를 지정합니다. 이 값은 [0.0f, 1.0f] 범위 내에 있어야하지만 minDepth는 maxDepth보다 높을 수 있습니다. 특별한 일을 하지 않는다면 0.0f와 1.0f의 표준값을 사용하기 바랍니다.


뷰포트는 이미지에서 프레임 버퍼로의 변환을 정의하지만 시저스 사각형은 픽셀이 실제로 저장 될 영역을 정의합니다. 시저 위젯 외부의 픽셀은 래스터 라이저에서 버려집니다. 그것들은 변환 보다는 필터처럼 기능합니다. 차이점은 아래에 나와 있습니다. 왼쪽 시저스 사각형은 뷰포트보다 큰 이미지의 결과 일 수 있는 많은 가능성 중 하나 일 뿐입니다.



이 튜토리얼에서는 전체 프레임 버퍼를 단순히 그려야 하므로 전체를 포괄하는 시저스 사각형을 지정합니다.


VkRect2D scissor = {};
scissor.offset = {0, 0};
scissor.extent = swapChainExtent;


이제이 뷰포트와 시저스 영역을 VkPipelineViewportStateCreateInfo 구조체를 사용하여 뷰포트 상태로 결합해야 합니다. 일부 그래픽 카드에서는 여러 개의 뷰포트와 시저 직사각형을 사용할 수 있으므로 멤버는 해당 그래픽 카드의 배열을 참조하게 됩니다. 다중을 사용하려면 GPU 기능을 활성화해야 합니다 (논리 장치 생성 참조).


VkPipelineViewportStateCreateInfo viewportState = {};
viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewportState.viewportCount = 1;
viewportState.pViewports = &viewport;
viewportState.scissorCount = 1;
viewportState.pScissors = &scissor;





Rasterizer

레스터라이저


래스터 라이저는 버텍스 쉐이더의 버텍스에 의해 형성되는 지오메트리를 가져 와서 프래그먼트 쉐이더에 의해 착색 될 프래그먼트로 변환합니다. 또한 깊이 테스트,면 처리 및 시저스 테스트를 수행하며 폴리곤 전체 또는 가장자리 만 채우는 조각 (와이어 프레임 렌더링)을 출력하도록 구성 할 수 있습니다.

이 모든 것은 VkPipelineRasterizationStateCreateInfo 구조체를 사용하여 구성됩니다.


VkPipelineRasterizationStateCreateInfo rasterizer = {};
rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
rasterizer.depthClampEnable = VK_FALSE;


depthClampEnable을 VK_TRUE로 설정하면 근거리 및 원거리 평면을 벗어난 조각이 파기되지 않고 클램핑 됩니다. 그림자 맵과 같은 특별한 경우에 유용합니다. 이를 사용하려면 GPU 기능을 활성화해야 합니다.


rasterizer.rasterizerDiscardEnable = VK_FALSE;


rasterizerDiscardEnable 을 VK_TRUE로 설정하면 형상이 래스터 라이저 단계를 통과하지 않습니다. 이것은 기본적으로 프레임 버퍼에 대한 모든 출력을 비활성화 합니다.


rasterizer.polygonMode = VK_POLYGON_MODE_FILL;


polygonMode는 지오메트리 조각을 생성하는 방법을 결정합니다. 다음 모드를 사용할 수 있습니다.


  • VK_POLYGON_MODE_FILL : 다각형의 영역을 조각으로 채 웁니다.

  • VK_POLYGON_MODE_LINE : 폴리곤 모서리가 선으로 그려집니다.

  • VK_POLYGON_MODE_POINT : 폴리곤 정점이 점으로 그려집니다.


채우기 모드가 아닌 다른 모드를 사용하려면 GPU 기능을 활성화해야 합니다.



rasterizer.lineWidth = 1.0f;


lineWidth 멤버는 직관적인 값으로 , 프래그먼트의 수를 기준으로 선 두께를 나타냅니다. 지원되는 최대 선폭은 하드웨어에 따라 다르며 1.0f보다 두꺼운 선은 wideLines GPU 기능을 활성화해야 합니다.


rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;


cullMode 변수는 면 제거 유형을 결정합니다. 컬링을 비활성화 하거나, 앞면을 컬링하거나, 뒷면을 컬링하거나, 둘 다를 취소 할 수 있습니다. frontFace 변수는 정면으로 간주 될 면의 정점 순서를 지정하며 시계 방향 또는 반 시계 방향 일 수 있습니다.


rasterizer.depthBiasEnable = VK_FALSE;
rasterizer.depthBiasConstantFactor = 0.0f; // Optional
rasterizer.depthBiasClamp = 0.0f; // Optional
rasterizer.depthBiasSlopeFactor = 0.0f; // Optional


래스터라이저는 상수 값을 추가하거나 프래그먼트의 기울기에 따라 편향 값을 변경하여 깊이 값을 변경할 수 있습니다. 이것은 때때로 그림자 매핑에 사용되지만 우리는 그것을 사용하지 않을 것입니다. depthBiasEnable을 VK_FALSE로 설정하면 됩니다.





Multisampling


VkPipelineMultisampleStateCreateInfo 구조체는 앤티 앨리어싱을 수행하는 방법 중 하나 인 멀티 샘플링을 구성합니다. 래스터화 하는 여러 폴리곤의 프래그먼트 쉐이더 결과를 동일한 픽셀에 결합하여 작동합니다. 이것은 주로 가장자리를 따라 발생하며 가장 눈에 띄는 앨리어싱 결과를 갖습니다.. 하나의 폴리곤 만 픽셀에 매핑되는 경우 프래그먼트 셰이더를 여러 번 실행할 필요가 없으므로 단순히 고해상도로 렌더링 한 다음 축소하는 것보다 훨씬  비용이 저렴합니다. 이를 사용하려면 GPU 기능을 활성화해야 합니다.


VkPipelineMultisampleStateCreateInfo multisampling = {};
multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
multisampling.sampleShadingEnable = VK_FALSE;
multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
multisampling.minSampleShading = 1.0f; // Optional
multisampling.pSampleMask = nullptr; // Optional
multisampling.alphaToCoverageEnable = VK_FALSE; // Optional
multisampling.alphaToOneEnable = VK_FALSE; // Optional


이 튜토리얼에서는 멀티 샘플링을 사용하지 않고 실험 해 볼 수 있습니다. 각 매개 변수의 의미는 스펙을 참조하십시오.



Depth and stencil testing

심도 및 스텐실 테스트


심도 및 / 또는 스텐실 버퍼를 사용하는 경우 VkPipelineDepthStencilStateCreateInfo 를 사용하여 심도 및 스텐실 테스트를 구성해야 합니다. 우리는 지금 당장은 사용하지 않기 때문에, struct 대신에 nullptr을 할당할 것 입니다. 깊이 버퍼링 장에서 다시 살펴 보겠습니다.



Color blending

색상 혼합


프래그먼트 셰이더가 색상을 반환 한 후에는 이미 프레임 버퍼에 있는 색상과 결합해야 합니다. 이 변환을 색상혼합 이라고 하며 두 가지 방법이 있습니다.


  • 이전 값과 새 값을 섞어 최종 색을 만듭니다.

  • 비트 연산을 사용하여 이전 값과 새 값을 결합 합니다.


색상 혼합을 구성하는 구조체에는 두 가지 유형이 있습니다. 첫 번째 구조체 인 VkPipelineColorBlendAttachmentState 에는 연결된 프레임 버퍼(per-frame buffer) 마다의 구성과 두 번째 구조체가 포함되어 있으며 VkPipelineColorBlendStateCreateInfo 에는 전역 색상 혼합 설정이 포함되어 있습니다. 여기서는 하나의 프레임 버퍼 만 있습니다.


VkPipelineColorBlendAttachmentState colorBlendAttachment = {};
colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
colorBlendAttachment.blendEnable = VK_FALSE;
colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_ONE; // Optional
colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ZERO; // Optional
colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; // Optional
colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; // Optional
colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; // Optional
colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; // Optional


이 per-framebuffer 구조체를 사용하면 색상 혼합의 첫 번째 방법을 구성 할 수 있습니다. 수행 될 조작은 다음과 같은 의사 코드를 사용합니다.


if (blendEnable) {
   finalColor.rgb = (srcColorBlendFactor * newColor.rgb) <colorBlendOp> (dstColorBlendFactor * oldColor.rgb);
   finalColor.a = (srcAlphaBlendFactor * newColor.a) <alphaBlendOp> (dstAlphaBlendFactor * oldColor.a);
} else {
   finalColor = newColor;
}

finalColor = finalColor & colorWriteMask;


blendEnable을 VK_FALSE로 설정하면 프래그먼트 쉐이더의 새 색상이 수정되지 않은 채로 전달됩니다. 그렇지 않은 경우 두 가지 혼합 작업을 수행하여 새 색상을 계산합니다. 결과 색상은 colorWriteMask와 AND되어 어떤 채널이 실제로 전달되는지를 결정합니다.


색상 혼합을 사용하는 가장 일반적인 방법은 알파 블렌딩을 구현하는 것입니다. 여기서 알파 블렌딩은 불투명도를 기반으로 새 색상을 이전 색상과 혼합 하려는 것입니다. finalColor는 다음과 같이 계산됩니다.


finalColor.rgb = newAlpha * newColor + (1 - newAlpha) * oldColor;
finalColor.a = newAlpha.a;


이것은 다음 매개 변수를 사용하여 수행 할 수 있습니다.


colorBlendAttachment.blendEnable = VK_TRUE;
colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD;
colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD;



VkBlendFactor 및 VkBlendOp 열거 형에서 가능한 모든 연산을 찾을 수 있습니다.


두 번째 구조체는 모든 프레임 버퍼에 대한 구조체 배열을 참조하며 앞에서 설명한 계산에서 혼합 요소로 사용할 수 있는 혼합식 상수를 설정할 수 있습니다.


VkPipelineColorBlendStateCreateInfo colorBlending = {};
colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
colorBlending.logicOpEnable = VK_FALSE;
colorBlending.logicOp = VK_LOGIC_OP_COPY; // Optional
colorBlending.attachmentCount = 1;
colorBlending.pAttachments = &colorBlendAttachment;
colorBlending.blendConstants[0] = 0.0f; // Optional
colorBlending.blendConstants[1] = 0.0f; // Optional
colorBlending.blendConstants[2] = 0.0f; // Optional
colorBlending.blendConstants[3] = 0.0f; // Optional


블렌딩의 두 번째 방법 (비트 조합)을 사용하려면 logicOpEnable을 VK_TRUE로 설정해야합니다. 비트 연산은 logicOp 필드에 지정할 수 있습니다. 이것은 연결된 모든 프레임 버퍼에 대해 blendEnable을 VK_FALSE로 설정 한 것처럼 첫 번째 방법을 자동으로 비활성화 합니다! colorWriteMask는 이 모드에서 실제로 영향을 받을 프레임 버퍼의 채널을 결정하는 데에도 사용됩니다. 여기에서 설명한 것처럼 두 모드를 모두 비활성화 할 수도 있습니다.이 경우 프래그먼트 색상이 수정되지 않고 프레임 버퍼에 기록됩니다.



Dynamic state

동적 상태


이전 구조체에서 지정한 상태의 제한된 양은 파이프 라인을 다시 만들지 않고 실제로 변경할 수 있습니다. 뷰포트의 크기, 선 너비 및 블렌드 상수를 예로 들 수 있습니다. 그렇게하고 싶다면 다음과 같이 VkPipelineDynamicStateCreateInfo 구조체를 사용해야 합니다 :


VkDynamicState dynamicStates[] = {
   VK_DYNAMIC_STATE_VIEWPORT,
   VK_DYNAMIC_STATE_LINE_WIDTH
};

VkPipelineDynamicStateCreateInfo dynamicState = {};
dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dynamicState.dynamicStateCount = 2;
dynamicState.pDynamicStates = dynamicStates;


이로 인해 이 값의 구성이 무시되고 그리기 시간에 데이터를 지정해야 합니다. 우리는 장래의 장에서 이것을 다시 할 것입니다. 이 구조체는 나중에 동적 상태가 없는 경우 nullptr로 대체 될 수 있습니다.


Pipeline layout


셰이더의 uniform 값은 셰이더를 다시 만들지 않고 셰이더의 동작을 변경하기 위해 드로잉 타임에 변경할 수있는 동적 상태 변수와 비슷한 전역의 셰이더를 사용할 수 있습니다. 이들은 변형 행렬을 버텍스 셰이더에 전달하거나 프래그먼트 셰이더에 텍스처 샘플러를 생성하는 데 일반적으로 사용됩니다.


VkPipelineLayout 개체를 만들어 파이프 라인을 만드는 동안 이러한 균일 한 값을 지정해야 합니다. 향후 장까지는 사용하지 않겠지 만, 우리는 여전히 빈 파이프 라인 레이아웃을 만들어야 합니다.


이 객체를 갖고 있을 클래스 멤버를 만듭니다. 나중에 다른 함수에서 참조 할 것이기 때문입니다.


VkPipelineLayout pipelineLayout;


그런 다음 createGraphicsPipeline 함수에서 객체를 만듭니다.


VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 0; // Optional
pipelineLayoutInfo.pSetLayouts = nullptr; // Optional
pipelineLayoutInfo.pushConstantRangeCount = 0; // Optional
pipelineLayoutInfo.pPushConstantRanges = 0; // Optional

if (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) {
   throw std::runtime_error("failed to create pipeline layout!");
}


이 구조는 푸시 상수(push constant) 를 지정합니다. 푸시 상수는 향후 장에서 얻을 수있는 셰이더에 동적 값을 전달하는 또 다른 방법입니다. 파이프 라인 레이아웃은 프로그램의 수명 내내 참조 될 수 있으므로 마지막에 제거 해야합니다.


void cleanup() {
   vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
   ...
}



결론


지금까지의 설명은 모든 고정 기능 상태를 위한 것입니다!  이 모든 것을 처음부터 새로 시작하는 것은 많은 작업이지만 장점은 그래픽 파이프 라인에서 진행되는 모든 작업을 거의 완전히 인식하고 있다는 것입니다. 특정 구성 요소의 기본 상태가 예상 한 것과 다르거나 하는 이유에 의하여 예기치 않은 동작을 발생할 가능성을 줄여 줍니다.


그러나 그래픽 파이프 라인을 생성하기 전에 생성해야 할 오브젝트가 하나 더 있습니다. 렌더링 패스입니다.



버텍스 쉐이더


#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;

   VkPipelineLayout pipelineLayout;

   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() {
       vkDestroyPipelineLayout(device, pipelineLayout, nullptr);

       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};

       VkPipelineVertexInputStateCreateInfo vertexInputInfo = {};
       vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
       vertexInputInfo.vertexBindingDescriptionCount = 0;
       vertexInputInfo.vertexAttributeDescriptionCount = 0;

       VkPipelineInputAssemblyStateCreateInfo inputAssembly = {};
       inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
       inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
       inputAssembly.primitiveRestartEnable = VK_FALSE;

       VkViewport viewport = {};
       viewport.x = 0.0f;
       viewport.y = 0.0f;
       viewport.width = (float) swapChainExtent.width;
       viewport.height = (float) swapChainExtent.height;
       viewport.minDepth = 0.0f;
       viewport.maxDepth = 1.0f;

       VkRect2D scissor = {};
       scissor.offset = {0, 0};
       scissor.extent = swapChainExtent;

       VkPipelineViewportStateCreateInfo viewportState = {};
       viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
       viewportState.viewportCount = 1;
       viewportState.pViewports = &viewport;
       viewportState.scissorCount = 1;
       viewportState.pScissors = &scissor;

       VkPipelineRasterizationStateCreateInfo rasterizer = {};
       rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
       rasterizer.depthClampEnable = VK_FALSE;
       rasterizer.rasterizerDiscardEnable = VK_FALSE;
       rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
       rasterizer.lineWidth = 1.0f;
       rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
       rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;
       rasterizer.depthBiasEnable = VK_FALSE;

       VkPipelineMultisampleStateCreateInfo multisampling = {};
       multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
       multisampling.sampleShadingEnable = VK_FALSE;
       multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;

       VkPipelineColorBlendAttachmentState colorBlendAttachment = {};
       colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
       colorBlendAttachment.blendEnable = VK_FALSE;

       VkPipelineColorBlendStateCreateInfo colorBlending = {};
       colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
       colorBlending.logicOpEnable = VK_FALSE;
       colorBlending.logicOp = VK_LOGIC_OP_COPY;
       colorBlending.attachmentCount = 1;
       colorBlending.pAttachments = &colorBlendAttachment;
       colorBlending.blendConstants[0] = 0.0f;
       colorBlending.blendConstants[1] = 0.0f;
       colorBlending.blendConstants[2] = 0.0f;
       colorBlending.blendConstants[3] = 0.0f;

       VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
       pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
       pipelineLayoutInfo.setLayoutCount = 0;
       pipelineLayoutInfo.pushConstantRangeCount = 0;

       if (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) {
           throw std::runtime_error("failed to create pipeline layout!");
       }

       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;
}



이전 글 : Shader modules

다음 글 : Render passes

'Vulkan' 카테고리의 다른 글

Graphics pipeline basics- Conclusion  (0) 2018.01.24
Graphics Pipeline basics - Render passes  (0) 2018.01.24
Graphics Pipeline basics - Shader modules  (0) 2018.01.24
Graphics Pipeline basics - introduction  (0) 2018.01.23
Presentation - Image views  (0) 2018.01.23