벌컨에서 compute pipeline을 처음 사용해 봤는데, 성공하기까지 4일 정도 밤 새웠다.

혹시라도 벌린이가 있다면 이 글을 보고 나 처럼 몇 날 몇일을 밤 새우지 않았으면 좋겠다.


실행 영상


viewimage.php?id=2ab4c42ef0d0&no=24b0d769e1d32ca73cec81fa11d02831ce3cef1b9542c00ceb084720f8a1823a783de9b24ad875e81425f22ea566843f12d236cf5e842c68ccc7f46743



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하고 똑같이 값을 가져올 수 있고 저장할 수 있고..



요즘 벌컨 배우려고 하는 사람 많더라.

이 글 한번 보시고 다시 한번 더 생각 해 보시길..