벌컨에서 compute pipeline을 처음 사용해 봤는데, 성공하기까지 4일 정도 밤 새웠다.
혹시라도 벌린이가 있다면 이 글을 보고 나 처럼 몇 날 몇일을 밤 새우지 않았으면 좋겠다.
실행 영상

computePIpeline 생성
std::vector<char> compShaderCode = readFile("/home/sbj/바탕화면/VULKAN/spv/Compute/exam.spv");
VkShaderModule compShaderModule = createShaderModule(compShaderCode);
// compute용의 shaderModule을 생성한다.
VkPipelineShaderStageCreateInfo pipelineShaderStageCreateInfo{};
pipelineShaderStageCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
pipelineShaderStageCreateInfo.pNext = 0;
pipelineShaderStageCreateInfo.flags = 0;
pipelineShaderStageCreateInfo.stage = VK_SHADER_STAGE_COMPUTE_BIT;
pipelineShaderStageCreateInfo.module = compShaderModule;
pipelineShaderStageCreateInfo.pName = "main";
pipelineShaderStageCreateInfo.pSpecializationInfo = 0;
// 파이프라인에 상수값을 넣고자 하는 경우 다음과 같이 디스크립터를 정의하자
VkPushConstantRange pushConstantRange;
pushConstantRange.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
pushConstantRange.offset = 0;
pushConstantRange.size = sizeof(float);
// 파이프라인 레이아웃을 만들자 이는 컴퓨트 파이프라인과 (Device-Instance)관계 역할을 할 것이다.
// (Device-Instance) 관계 :
// pipeline - pipelinelayout
// discriptor - discriptorlayout
// resource - dispatch
// framebuffer - renderpass
VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo{};
pipelineLayoutCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutCreateInfo.pNext = 0;
pipelineLayoutCreateInfo.flags = 0;
pipelineLayoutCreateInfo.setLayoutCount = 1;
pipelineLayoutCreateInfo.pSetLayouts = &descriptorSetLayout;
pipelineLayoutCreateInfo.pushConstantRangeCount = 1;
pipelineLayoutCreateInfo.pPushConstantRanges = &pushConstantRange;
if ( vkCreatePipelineLayout(device, &pipelineLayoutCreateInfo, nullptr, &this->computePipelineLayout) != VK_SUCCESS ) {
throw std::runtime_error("pipelineLayout 생성 실패");
}
// 마지막으로 위에서 만든 인스턴스를 이용하여 ComputePipeline을 구축하자
VkComputePipelineCreateInfo computePipelineCreateInfo{};
computePipelineCreateInfo.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO;
computePipelineCreateInfo.pNext = 0;
computePipelineCreateInfo.flags = 0;
computePipelineCreateInfo.stage = pipelineShaderStageCreateInfo;
computePipelineCreateInfo.layout = this->computePipelineLayout;
computePipelineCreateInfo.basePipelineHandle = 0;
computePipelineCreateInfo.basePipelineIndex = 0;
if ( vkCreateComputePipelines(device, 0, 1, &computePipelineCreateInfo, nullptr, &this->computesPipeline) != VK_SUCCESS) {
throw std::runtime_error("computePipeline 생성 실패");
}
// 그리고 물론 다 쓴거는 바로바로 버리자
vkDestroyShaderModule(device, compShaderModule, 0);
compute Pipeline에서 사용 되어질 버퍼 생성.
(해당 지문 예시로는 top_layout -> transpose_layout -> bottom_layout(backBuffer) 이후에 UniformBuffer로 사용이 될 예정)
// 리소스 사이즈는 언제 어디서든 사용해야 할 경우가 많기에 객체 안에 저장해 두는것이 정신건강에 좋다.
VkDeviceSize bufferSize = sizeof(TexelBufferObject[0]) * TexelBufferObject.size();
this->texelDeviceSize = bufferSize;
// 컴퓨트 파이프라인에서 생성 할 버퍼를 생성하자
VkBufferCreateInfo bufferInfo{};
bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufferInfo.size = bufferSize;
// 어떤 목적으로 버퍼를 사용할 지 적는 칸이다. 복수로 적어 놓고 이후 메모리 베리어를 통해서 여러 목적으로 사용 되어지는 것이 가능해 진다.
bufferInfo.usage = VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
if (vkCreateBuffer(device, &bufferInfo, nullptr, &texelUniformBuffer) != VK_SUCCESS) {
throw std::runtime_error("failed to create buffer!");
}
// 혹시나 모르는 사람을 위해 적자면 위 버퍼에서 요구하는 메모리 유형을 찾는 알고리즘이다.
VkMemoryRequirements memRequirements;
vkGetBufferMemoryRequirements(device, texelUniformBuffer, &memRequirements);
VkPhysicalDeviceMemoryProperties memProp;
vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProp);
int memTypeIdx = -1;
for (int i = 0; i < memProp.memoryTypeCount; i++) {
if (memRequirements.memoryTypeBits & (1 << i) && memProp.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT == VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) {
memTypeIdx = i;
break;
}
}
if (memTypeIdx == -1)
throw std::runtime_error("요구되는 메모리를 할당하지 못함.");
// 버퍼에서 요구하는 유형의 메모리를 찾았다면 해당 메모리에 할당을 시키자.
VkMemoryAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memRequirements.size;
allocInfo.memoryTypeIndex = memTypeIdx;
if (vkAllocateMemory(device, &allocInfo, nullptr, &texelUniformBuffersMemory) != VK_SUCCESS) {
throw std::runtime_error("failed to allocate buffer memory!");
}
// 다음 해당 메모리에 버퍼를 바인딩
vkBindBufferMemory(device, texelUniformBuffer, texelUniformBuffersMemory, 0);
// BufferView 생성 texel_uniform 역할로 디스크립터에 보내려면 필요한 모양이더라
VkBufferViewCreateInfo bufferViewCreateInfo{};
bufferViewCreateInfo.sType = VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO;
bufferViewCreateInfo.pNext = 0;
bufferViewCreateInfo.flags = 0;
bufferViewCreateInfo.buffer = texelUniformBuffer;
bufferViewCreateInfo.format = VK_FORMAT_R32G32B32A32_SFLOAT;
bufferViewCreateInfo.offset = 0;
bufferViewCreateInfo.range = VK_WHOLE_SIZE;
if (vkCreateBufferView(device, &bufferViewCreateInfo, nullptr, &texelUniformBuffersView) != VK_SUCCESS) {
throw std::runtime_error("texelUniformBuffers 생성 실패");
}
// 이제 스테깅 버퍼를 만들어서 실 데이터 값 -> stagingBuffer -> texelBuffer로 보낼 것이다.
VkBuffer stagingBuffer;
VkDeviceMemory stagingMemory;
bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufferInfo.size = bufferSize;
// stagingBuffer2texeBuffer 니까 당연히 staging이 SOURCE가 되어야 한다.(받는 쪽은 DESTINATION)
bufferInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
if (vkCreateBuffer(device, &bufferInfo, nullptr, &stagingBuffer) != VK_SUCCESS) {
throw std::runtime_error("failed to create buffer!");
}
// 이후 위에서 처럼 메모리 버퍼도 만들자
vkGetBufferMemoryRequirements(device, stagingBuffer, &memRequirements);
vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProp);
memTypeIdx = -1;
for (int i = 0; i < memProp.memoryTypeCount; i++) {
if (memRequirements.memoryTypeBits & (1 << i) && memProp.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT == VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) {
memTypeIdx = i;
break;
}
}
if (memTypeIdx == -1)
throw std::runtime_error("요구되는 메모리를 할당하지 못함.");
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memRequirements.size;
allocInfo.memoryTypeIndex = memTypeIdx;
if (vkAllocateMemory(device, &allocInfo, nullptr, &stagingMemory) != VK_SUCCESS) {
throw std::runtime_error("failed to allocate buffer memory!");
}
vkBindBufferMemory(device, stagingBuffer, stagingMemory, 0);
// 바인딩 시킨 스테깅 버퍼 메모리에 실 데이터 값을 카피 시키자.
void* data;
vkMapMemory(device, stagingMemory, 0, bufferSize, 0, &data);
memcpy(data, TexelBufferObject.data(), bufferSize);
// 이후 Buffer에 Flushing
// stagingBuffer에 copy받은 값 flushing.
VkMappedMemoryRange memoryRange{};
memoryRange.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE;
memoryRange.pNext = 0;
memoryRange.offset = 0;
memoryRange.size = bufferSize;
memoryRange.memory = stagingMemory;
vkFlushMappedMemoryRanges(device, 1, &memoryRange);
vkUnmapMemory(device, stagingMemory);
// 레코딩 용 버퍼를 만들자
VkCommandBuffer recordBuffer;
VkCommandBufferAllocateInfo cmdbufAllocInfo{};
cmdbufAllocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
cmdbufAllocInfo.pNext = 0;
cmdbufAllocInfo.commandPool = commandPool;
cmdbufAllocInfo.commandBufferCount = 1;
cmdbufAllocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
vkAllocateCommandBuffers(device, &cmdbufAllocInfo, &recordBuffer);
// 레코딩 버퍼의 레코딩을 시작하자
VkCommandBufferBeginInfo cmdbufBeginInfo{};
cmdbufBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
cmdbufBeginInfo.pNext = 0;
cmdbufBeginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
cmdbufBeginInfo.pInheritanceInfo = 0;
vkBeginCommandBuffer(recordBuffer, &cmdbufBeginInfo);
// stagingBuffer->TexelBuffer 의 Transfer목적으로 메모리를 사용하기 위하여 베리어를 정해 보자
VkBufferMemoryBarrier bufMemBarrier{};
bufMemBarrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER;
bufMemBarrier.pNext = 0;
// 처음 시작할 때에는 값을 변동이 없었으므로 억세스 마스크 필터가 없다.
bufMemBarrier.srcAccessMask = 0;
// 이후 Transfer용 메모리에 자신의 값을 쓸 예정이므로 다음과 같이 지정하자
bufMemBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
bufMemBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
bufMemBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
bufMemBarrier.buffer = texelUniformBuffer;
bufMemBarrier.offset = 0;
bufMemBarrier.size = texelDeviceSize;
vkCmdPipelineBarrier( recordBuffer,
// 스테이지가 Top에서 Transfer까지 가기 전에 위의 명세를 마쳐야지만 STAGE_TRANSFER_BIT에 접근 할 수 있도록 방어막을
친다.
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT,
0,
0, 0,
1, &bufMemBarrier,
0, 0);
// 이제 준비가 되었다! stagingBuffer -> texelBuffer를 실행해보자
VkBufferCopy bufferCopy{};
bufferCopy.srcOffset = 0;
bufferCopy.size = texelDeviceSize;
bufferCopy.dstOffset = 0;
vkCmdCopyBuffer( recordBuffer,
stagingBuffer,
texelUniformBuffer,
1,
&bufferCopy);
// 이제 레코딩을 끝내자
vkEndCommandBuffer(recordBuffer);
// 다음 해당 커멘드 버퍼를 큐에 보내자
VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.pNext = 0;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &recordBuffer;
vkQueueSubmit(graphicsQueue, 1, &submitInfo, nullptr);
vkQueueWaitIdle(graphicsQueue);
vkFreeCommandBuffers(device, commandPool, 1, &recordBuffer);
vkFreeMemory(device, stagingMemory, 0);
vkDestroyBuffer(device, stagingBuffer, 0);
DescriptorSetLayout
// 위에서 언급했듯이 나는 컴퓨트 파이프라인에서 계산돌리고 저장된 값을 그래픽스 파이프라인에서 Uniform_BUffer로 사용할 것이다.
// 그러므로 이 둘은 같은 버퍼를 공유한다.
VkDescriptorSetLayoutBinding texelBufferBinding{};
texelBufferBinding.binding = 2;
texelBufferBinding.descriptorCount = 1;
texelBufferBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER;
texelBufferBinding.pImmutableSamplers = nullptr;
texelBufferBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
VkDescriptorSetLayoutBinding texelVertexBufferBinding{};
texelVertexBufferBinding.binding = 3;
texelVertexBufferBinding.descriptorCount = 1;
texelVertexBufferBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
texelVertexBufferBinding.pImmutableSamplers = nullptr;
texelVertexBufferBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
DescriptorPool
poolSizes[2].type = VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER;
poolSizes[2].descriptorCount = static_cast<uint32_t>(swapChainImages.size());
poolSizes[3].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
poolSizes[3].descriptorCount = static_cast<uint32_t>(swapChainImages.size());
DescriptorSets
descriptorWrites[2].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrites[2].dstSet = descriptorSets[i];
descriptorWrites[2].dstBinding = 2;
descriptorWrites[2].dstArrayElement = 0;
descriptorWrites[2].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER;
descriptorWrites[2].descriptorCount = 1;
descriptorWrites[2].pBufferInfo = &texelBufferInfo;
// 텍셀 버퍼로 사용하려면 버퍼 뷰를 지정해야 한다더라. 왜인지는 나도 나중에 찾아봐야 알것같다.
descriptorWrites[2].pTexelBufferView = &texelUniformBuffersView;
descriptorWrites[3].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrites[3].dstSet = descriptorSets[i];
descriptorWrites[3].dstBinding = 3;
descriptorWrites[3].dstArrayElement = 0;
descriptorWrites[3].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
descriptorWrites[3].descriptorCount = 1;
descriptorWrites[3].pBufferInfo = &texelBufferInfo;
이제 그려주기만 하면 된다. drawFrame에서 "vkCmdBeginRenderPass전에" 다음과 같이 미리 컴퓨팅 파이프라인을 실행시키자.
// Descriptor Bind 2 CommmandBuffer
vkCmdBindDescriptorSets( commandBuffer,
VK_PIPELINE_BIND_POINT_COMPUTE,
obj->computePipelineLayout,
0,
1,
&obj->descriptorSets[currentFrame],
0,
nullptr);
// compute pipeline Bind 2 CommmandBuffer
vkCmdBindPipeline( commandBuffer,
VK_PIPELINE_BIND_POINT_COMPUTE,
obj->computesPipeline);
// constant Bind 2 CommandBuffer
vkCmdPushConstants(commandBuffer, obj->computePipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(float), &EXAMPLE);
// 해당 버퍼에 담긴 값을 어떻게 Dispatch할 것인가에 대한 Helper(descriptor)
vkCmdDispatch( commandBuffer, 2, 1, 1);
여기에서 vkCmdDispatch의 역할이 굉장히 중요하다.
얘 역할은 해당 버퍼에 들어있는 값이 어떠한 형태 (배열)로 정렬이 되어있는가 그 말이다.
값 하나의 크기는 위에 버퍼 생성할 때 format(VK_FORMAT_R32G32B32A32_SFLOAT) 이 크기에 맞추어 져서 배열이 할당 되는 것이다.
예를 들어 위에서는 디스패치가 2, 1, 1로 지정되어있고, 버퍼의 값이 {1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f} 이면
값의 포멧이 VK_FORMAT_R32G32B32A32_SFLOAT이니까 sizeof(float) * 4가 값(원소) 하나의 사이즈가 되고, 2개로 나뉘어져 있으므로
{ { 1.0f, 0.0f, 0.0f, 1.0f }, { 0.0f, 1.0f, 0.0f, 1.0f } } 다음과 같이 나뉘어져 Compute Pipeline으로 디스패치 되어진다.
#version 430 core
// glsl에서 printf 가능하게 해주는 확장팩
#extension GL_EXT_debug_printf : enable
// 이건 형식상 쓰던데 나도 찾아봐야 앎
layout(local_size_x = 32, local_size_y = 32) in;
// 위에서 만든 텍셀 버퍼를 디스크립터를 통해 가져옴// 위에서 봤 듯이 디스크립터_셋_0 에서(set = 0) 2번 째에 바인딩한 (binding = 2) VK_FORMAT
// R32G32B32A32_SFLOAT 형태의 (rgba32f) UniformBuffer를 여기로 가져오는 코드
layout( set = 0, binding = 2, rgba32f ) uniform imageBuffer StorageTexelBuffer;
// 상수 버퍼도 가져옴
layout( push_constant ) uniform constants{
float DeltaTime;
} PushConstant;
// 그냥 전역 변수 (사용 안함)const uint PARTICLES_COUNT = 1;
void main() {
// 위에서 말한대로 { { 1.0f, 0.0f, 0.0f, 1.0f }, { 0.0f, 1.0f, 0.0f, 1.0f } } 로 배열화 되어 들어온
// StorageTexelBuffer 에서 0 번째 (int (gl_GlobalInfocateionID.x * 2)) 인덱스의 원소를 불러온다
// 즉 0번째 원소니까 {1.0f, 0.0f, 0.0f, 1.0f}를 position벡터에 집어 넣는다.
vec4 position = imageLoad( StorageTexelBuffer, int(gl_GlobalInvocationID.x * 2) );
// {1.0f, 0.0f, 0.0f, 1.0f}에 다음 값을 더하여 저장한다.
position += vec4(-0.0001f, 0.0001f, 0.0001f, 0.0f);
// StorageTexelBuffer 에서 0 번째 (int (gl_GlobalInfocateionID.x * 2)) 인덱스의 원소에 position을 저장한다.
// 즉 0번째 원소니까 위에서 계산을 마친 position을 0 번째 원소의 메모리에 집어 넣는다.
imageStore( StorageTexelBuffer, int(gl_GlobalInvocationID.x * 2), position );
}
// gl_GlobalInvocationID는 위에서 본 함수
// vkCmdDispatch(commandBuffer, x, y, z) -> gl_GlobalInvocation.xyz
// 이렇게 변환 된다고 생각하셈.
그리고 이거는 opengl하고 똑같이 값을 가져올 수 있고 저장할 수 있고..
끝
요즘 벌컨 배우려고 하는 사람 많더라.
이 글 한번 보시고 다시 한번 더 생각 해 보시길..
호에엥 벌컨 쓰기쉽게 추상화좀 해줘 - dc App
이미 Opaque 형식의 구조체로 추상화 기능이 제공 되는 중이라고 하더라고.. c언어의 한계인거 같아.. ㅋㅋ
그냥 OpenGL 쓸게..
굳굳 취업할 때에도 아직 한국은 벌컨보다는 gl쪽이 수요가 많아서 벌컨 배워도 별로 의미 없어
읔... 이게 뭐죠
마지막에 셰이더는 뭐하는애임?
컴퓨트셰이더에서 해야할 일 정의
vertex fragment 셰이더만 봐서 저건 읽을줄 모르는데 대충해석좀
위에 코드 부분에 주석 첨부했다. 그냥 대충 썼는데 진짜 진지하게 읽을 사람이 있을 줄은 몰랐다 ㅋㅋ
공부중인데 아직은 봐도 모르겠음 설명은 감사