본문 바로가기

Vulkan

Staging buffer

이전 글 : Vertex buffer creation

다음 글 : Index buffer



Staging buffer

스테이징 버퍼,준비 버퍼


소개


지금 우리가 가지고있는 버텍스 버퍼는 제대로 작동 하지만 우리가 CPU에서 액세스 할 수 있는 메모리 타입으로 읽을 수 있는 메모리는 그래픽 카드 자체에 대한 최적의 메모리 타입이 아닐 수 있습니다.


가장 최적의 메모리는  VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT 플래그를 갖는 메모리로, 일반적으로 전용 그래픽 카드에 적합하며  CPU에 의해 액세스 할 수 없습니다.


이 장에서 우리는 두 개의 버텍스 버퍼를 만들 것입니다.

정점 어레이로부터 데이터를 업로드 하기 위한 CPU가 액세스 가능한 메모리인 staging buffer 이고 다른 하나는 디바이스의 로컬 메모리인 정점 버퍼 입니다.


우리는 staging 버퍼 로부터 데이터를 실제 정점버퍼로 이동하는 버퍼 복사 명령을 사용할 수 있습니다.



Transfer queue,전송 큐


버퍼 복사 명령에는 전송 작업을 지원하는 대기열 패밀리가 필요하며 VK_QUEUE_TRANSFER_BIT 를 사용하여 표시 됩니다.

좋은 소식은 VK_QUEUE_GRAPHICS_BIT 또는 VK_QUEUE_COMPUTE_BIT 기능이 있는 대기열 패밀리가 이미 VK_QUEUE_TRANSFER_BIT 작업을 암시적으로 지원한다는 것입니다. 이 경우 구현시 queueFlags에 명시적으로 나열 할 필요가 없습니다.


당힌이 도전을 좋아하면  전송 작업을 위해 특별히 다른 대기열을 사용하려고 할 수 있습니다. 그러면 프로그램을 다음과 같이 수정해야합니다.


  • QueueFamilyIndices 및 findQueueFamilies를 수정하여 VK_QUEUE_TRANSFER 비트가 있지만 VK_QUEUE_GRAPHICS_BIT가 아닌 대기열 패밀리를 명시적으로 찾습니다.

  • Modify : 전송 큐에 대한 핸들을 요청하기 위한 createLogicalDevice

  • 전송 대기열에 제출 된 명령 버퍼에 대한 두 번째 명령 풀 만들기

  • 리소스의 sharingMode를 VK_SHARING_MODE_CONCURRENT로 변경하고 그래픽 및 전송 대기열 패밀리를 모두 지정하십시오

  • vkCmdCopyBuffer (이 장에서 사용할 것임)와 같은 전송 명령을 그래픽 대기열 대신 전송 대기열에 제출합니다.


이것은 약간의 작업이지만 큐 (queue) 계열간에 리소스가 공유되는 방법에 대해 많은 것을 가르쳐 줄 것입니다.



Abstractiong buffer creation

추상화 버퍼 작성


이 장에서는 여러 개의 버퍼를 생성하기 때문에 버퍼 생성을 도우미 함수로 두는 것이 좋습니다. 새 함수 createBuffer를 만들고 createVertexBuffer 의 코드 (매핑 제외)를 옮깁니다.


void createBuffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, VkBuffer& buffer, VkDeviceMemory& bufferMemory) {
   VkBufferCreateInfo bufferInfo = {};
   bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
   bufferInfo.size = size;
   bufferInfo.usage = usage;
   bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

   if (vkCreateBuffer(device, &bufferInfo, nullptr, &buffer) != VK_SUCCESS) {
       throw std::runtime_error("failed to create buffer!");
   }

   VkMemoryRequirements memRequirements;
   vkGetBufferMemoryRequirements(device, buffer, &memRequirements);

   VkMemoryAllocateInfo allocInfo = {};
   allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
   allocInfo.allocationSize = memRequirements.size;
   allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties);

   if (vkAllocateMemory(device, &allocInfo, nullptr, &bufferMemory) != VK_SUCCESS) {
       throw std::runtime_error("failed to allocate buffer memory!");
   }

   vkBindBufferMemory(device, buffer, bufferMemory, 0);
}


이 함수를 사용하여 다양한 유형의 버퍼를 만들 수 있도록 버퍼 크기, 메모리 속성 및 사용법에 대한 매개 변수를 추가해야 합니다. 마지막 두 매개 변수는 핸들을 쓰는 출력 변수입니다.


이제 createVertexBuffer에서 버퍼 생성 및 메모리 할당 코드를 제거하고 대신 createBuffer를 호출 할 수 있습니다.


void createVertexBuffer() {
   VkDeviceSize bufferSize = sizeof(vertices[0]) * vertices.size();
   createBuffer(bufferSize, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, vertexBuffer, vertexBufferMemory);

   void* data;
   vkMapMemory(device, vertexBufferMemory, 0, bufferSize, 0, &data);
       memcpy(data, vertices.data(), (size_t) bufferSize);
   vkUnmapMemory(device, vertexBufferMemory);
}


프로그램을 실행하여 정점 버퍼가 여전히 올바르게 작동하는지 확인하십시오.




Using a staging buffer


이제는 호스트 버퍼를 임시 버퍼로 사용하고 장치 로컬 버퍼를 실제 버텍스 버퍼로 사용하기 위해 createVertexBuffer를 변경하려고 합니다.


void createVertexBuffer() {
   VkDeviceSize bufferSize = sizeof(vertices[0]) * vertices.size();

   VkBuffer stagingBuffer;
   VkDeviceMemory stagingBufferMemory;
   createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory);

   void* data;
   vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, &data);
       memcpy(data, vertices.data(), (size_t) bufferSize);
   vkUnmapMemory(device, stagingBufferMemory);

   createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, vertexBuffer, vertexBufferMemory);
}


정점 데이터를 매핑하고 복사하기 위해 stagingBufferMemory 와 함께 새로운 stagingBuffer 를 사용하고 있습니다. 이 장에서는 두 개의 새로운 버퍼 사용 플래그를 사용할 것입니다.


  • VK_BUFFER_USAGE_TRANSFER_SRC_BIT : 메모리 전송 동작에서 버퍼를 소스로 사용할 수 있습니다.

  • VK_BUFFER_USAGE_TRANSFER_DST_BIT : 버퍼는 메모리 전송 작업에서 대상으로 사용할 수 있습니다.


이제 vertexBuffer는 장치의 로컬 메모리 유형으로 할당됩니다. 이는 일반적으로 vkMapMemory 를 사용할 수 없음을 의미합니다. 그러나 stagingBuffer 에서 vertexBuffer 로 데이터를 복사 할 수 있습니다. stagingBuffer에 대한 전송 소스 플래그와 vertexBuffer 에 대한 전송 대상 플래그를 정점 버퍼 사용 플래그와 함께 지정하여 이를 수행 할 예정임을 표시해야 합니다.


우리는 이제 한 버퍼에서 다른 버퍼로 내용을 복사하는 copyBuffer라는 함수를 작성하려고 합니다.


void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) {

}


메모리 전송 작업은 그리기 명령처럼 명령 버퍼를 사용하여 실행 됩니다. 따라서 임시 명령 버퍼를 먼저 할당해야 합니다. 구현시 메모리 할당 최적화를 적용 할 수 있기 때문에 이러한 종류의 짧은 수명의 버퍼에 대해 별도의 명령 풀을 만들 수 있습니다. 이 경우 명령 풀 생성 중에 VK_COMMAND_POOL_CREATE_TRANSIENT_BIT 플래그를 사용해야 합니다.


void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) {
   VkCommandBufferAllocateInfo allocInfo = {};
   allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
   allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
   allocInfo.commandPool = commandPool;
   allocInfo.commandBufferCount = 1;

   VkCommandBuffer commandBuffer;
   vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer);
}


이어서 명령 버퍼 기록을 시작하게 합니다.


VkCommandBufferBeginInfo beginInfo = {};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;

vkBeginCommandBuffer(commandBuffer, &beginInfo);


그리기 명령 버퍼에 사용했던 VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT 플래그는 여기서는 필요하지 않습니다. 명령 버퍼를 한 번만 사용하고 복사 작업이 실행을 마칠 때까지 함수에서 돌아 오기를 기다릴 것이기 때문입니다. VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT를 사용하여 그래픽 드라이버에게  우리의 의도를 알려주는 것이 좋습니다.


VkBufferCopy copyRegion = {};
copyRegion.srcOffset = 0; // Optional
copyRegion.dstOffset = 0; // Optional
copyRegion.size = size;
vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, &copyRegion);


버퍼 내용은 vkCmdCopyBuffer 명령을 사용하여 전송됩니다. 소스 버퍼와 대상 버퍼를 인수로 사용하면 복사 할 영역의 배열을 복사합니다. 영역은 VkBufferCopy 구조체에 정의되며 소스 버퍼 오프셋, 대상 버퍼 오프셋 및 크기로 구성됩니다. vkMapMemory 명령과 달리 여기에 VK_WHOLE_SIZE 를 지정할 수 없습니다.


vkEndCommandBuffer(commandBuffer);


이 명령 버퍼에는 copy 명령 만 포함되어 있으므로 그 후에 바로 기록을 중지 할 수 있습니다. 이제 명령 버퍼를 실행하여 전송을 완료하십시오.

VkSubmitInfo submitInfo = {};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffer;

vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE);
vkQueueWaitIdle(graphicsQueue);


드로우 명령과 달리 이번에는 기다려야 할 이벤트가 없습니다. 우리는 버퍼에서 즉시 전송을 실행하려고합니다. 이 전송이 완료 될 때까지 대기 할 수있는 두 가지 방법이 있습니다. 울타리를 사용하여 vkWaitForFences 로 대기하거나 vkQueueWaitIdle 을 사용하여 전송 대기열이 유휴 상태가 될 때까지 기다릴 수 있습니다. 차단 장치를 사용하면 동시에 여러 번 전송을 예약하고 한 번에 하나씩 실행하는 대신 완료된 모든 전송을 기다릴 수 있습니다. 그것은 운전자에게 더 많은 기회를 최적화시킬 수 있습니다.


vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);


전송 작업에 사용되는 명령 버퍼를 정리하는 것을 잊지 마십시오.


이제 createVertexBuffer 함수에서 copyBuffer를 호출하여 정점 데이터를 장치 로컬 버퍼로 이동할 수 있습니다.


createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, vertexBuffer, vertexBufferMemory);

copyBuffer(stagingBuffer, vertexBuffer, bufferSize);


스테이징 버퍼에서 장치 버퍼로 데이터를 복사 한 후 정리해야합니다.

  ...

   copyBuffer(stagingBuffer, vertexBuffer, bufferSize);

   vkDestroyBuffer(device, stagingBuffer, nullptr);
   vkFreeMemory(device, stagingBufferMemory, nullptr);
}


프로그램을 실행하여 익숙한 삼각형이 다시 나타나는지 확인하십시오. 현재 개선되지는 않았지만 정점 데이터가 고성능 메모리에서 로드되고 있습니다. 더 복잡한 형상을 렌더링 할 때 이 점이 중요합니다.



이번장의 결론 정리


실제 응용 프로그램에서는 모든 개별 버퍼에 대해 실제로 vkAllocateMemory 를 호출하지 않아야합니다. 동시 메모리 할당의 최대 수는 maxMemoryAllocationCount 물리적 장치 제한에 의해 제한됩니다. NVIDIA GTX 1080과 같은 하이 엔드 하드웨어에서도 4096 으로 낮을 수 있습니다. 많은 수의 개체에 대하여 동시에 메모리를 할당하는 올바른 방법은 우리가 많은 함수에서 보았던 오프셋 매개 변수를 사용하여 여러 객체간에 단일 할당을 분할하는 사용자 정의 할당자를 만드는 것입니다.


이러한 할당자를 직접 구현하거나 GPUOpen 이니셔티브에서 제공하는 VulkanMemoryAllocator 라이브러리를 사용할 수 있습니다. 그러나 이 튜토리얼에서는 모든 리소스에 대해 별도의 할당을 사용하는 것이 좋습니다. 왜냐하면 지금은 이러한 제약에  도달하지 않을 것이기 때문입니다.


C++ code / Vertex shader / Fragment shader


이전 글 : Vertex buffer creation

다음 글 : Index buffer


'Vulkan' 카테고리의 다른 글

행렬  (0) 2018.02.05
Index buffer  (0) 2018.01.25
Vertex buffer creation  (0) 2018.01.25
Vertex input description  (0) 2018.01.25
Swap chain recreation  (0) 2018.01.25