Войти
ПрограммированиеСтатьиГрафика

Vulkan API «Hello Triangle» (6 стр)

Автор:

Буферы и память

Вывод геометрии требует наличие данных о ней (вершин) в памяти устройства, а точнее - в буфере.

Буфер

VkResult vkCreateBuffer(
    VkDevice                                    device,
    const VkBufferCreateInfo*                   pCreateInfo,
    const VkAllocationCallbacks*                pAllocator,
    VkBuffer*                                   pBuffer);

typedef struct VkBufferCreateInfo {
    VkStructureType        sType;
    const void*            pNext;
    VkBufferCreateFlags    flags;
    VkDeviceSize           size;
    VkBufferUsageFlags     usage;
    VkSharingMode          sharingMode;
    uint32_t               queueFamilyIndexCount;
    const uint32_t*        pQueueFamilyIndices;
} VkBufferCreateInfo;

Как и в OpenGL, буферы Vulkan API различаются по назначению. Необходимый нам буфер вершин создается с параметром usage = VkBufferUsageFlagBits::VK_BUFFER_USAGE_VERTEX_BUFFER_BIT.

Сами буферы не являются форматированными (в отличии от изображений), что позволяет нам заполнять их как только нам вздумается.
Важным пунктом является то, что сам буфер не является памятью, но определяет области доступные для чтения/записи.

Память

Память в Vulkan API является отдельной сущностью, независимой от буферов и изображений. Как и буферы команд, мы можем только выделить ее из физического устройства:

VkResult vkAllocateMemory(
    VkDevice                                    device,
    const VkMemoryAllocateInfo*                 pAllocateInfo,
    const VkAllocationCallbacks*                pAllocator,
    VkDeviceMemory*                             pMemory);

typedef struct VkMemoryAllocateInfo {
    VkStructureType    sType;
    const void*        pNext;
    VkDeviceSize       allocationSize;
    uint32_t           memoryTypeIndex;
} VkMemoryAllocateInfo;

Тут есть один трудный момент, который скрывался от большинства разработчиков до этого времени. На самом деле даже одна видеокарта может располагать разными участками память с разным предназначением. Так, например, устройство может иметь кучу памяти видимой внутри устройства, но невидимой снаружи и совсем мало для доступа извне.

Такие ограничения хотя и понятны, но тем не менее вносят долю сложности в разработку: нам придется выбирать из доступных типов памяти пригодный для наших нужд.
Сами буферы тоже имеют ограничения на используемую память. Мы можем узнать о них, вызвав:

void vkGetBufferMemoryRequirements(
    VkDevice                                    device,
    VkBuffer                                    buffer,
    VkMemoryRequirements*                       pMemoryRequirements);

Уже выделенную память, видимую со стороны приложения, можно мапить:

VkResult vkMapMemory(
    VkDevice                                    device,
    VkDeviceMemory                              memory,
    VkDeviceSize                                offset,
    VkDeviceSize                                size,
    VkMemoryMapFlags                            flags,
    void**                                      ppData);

void vkUnmapMemory(
    VkDevice                                    device,
    VkDeviceMemory                              memory);

и связывать с устройством:

VkResult vkBindBufferMemory(
    VkDevice                                    device,
    VkBuffer                                    buffer,
    VkDeviceMemory                              memory,
    VkDeviceSize                                memoryOffset);

Передача информации о вершинах на GPU

Мы выделим буфер, свяжем с ним область памяти и скопируем в нее заранее подготовленные данные о вершинах:

VkBuffer vk_buffer;
{
  std::vector<float> source = {
    -0.5f, +0.5f,
    +0.5f, +0.5f,
    +0.0f, -0.5f
  };

  VkBufferCreateInfo vk_bufferCreateInfo;
  {
    vk_bufferCreateInfo.sType = VkStructureType::VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
    vk_bufferCreateInfo.pNext = nullptr;
    vk_bufferCreateInfo.flags = 0;
    vk_bufferCreateInfo.size = sizeof(float)* source.size();
    vk_bufferCreateInfo.usage = VkBufferUsageFlagBits::VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
    vk_bufferCreateInfo.sharingMode = VkSharingMode::VK_SHARING_MODE_EXCLUSIVE;
    vk_bufferCreateInfo.queueFamilyIndexCount = 0;
    vk_bufferCreateInfo.pQueueFamilyIndices = nullptr;
  }

  if(vkCreateBuffer(vk_device, &vk_bufferCreateInfo, nullptr, &vk_buffer) != VkResult::VK_SUCCESS)
  {
    throw std::exception("failed to create buffer");
  }

  VkDeviceMemory vk_deviceMemory;
  {
    VkMemoryRequirements vk_memoryRequirements;
    vkGetBufferMemoryRequirements(vk_device, vk_buffer, &vk_memoryRequirements);

    VkMemoryAllocateInfo vk_memoryAllocateInfo;
    {
      vk_memoryAllocateInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
      vk_memoryAllocateInfo.pNext = nullptr;
      vk_memoryAllocateInfo.allocationSize = vk_memoryRequirements.size;
      vk_memoryAllocateInfo.memoryTypeIndex = [&]()
      {
        for(size_t i = 0; i < vk_physicalDeviceMemoryProperties.memoryTypeCount; ++i)
        {
          auto bit = ((uint32_t)1 << i);
          if((vk_memoryRequirements.memoryTypeBits & bit) != 0)
          {
            if((vk_physicalDeviceMemoryProperties.memoryTypes[i].propertyFlags & VkMemoryPropertyFlagBits::VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0)
            {
              return i;
            }
          }
        }

        throw std::exception("failed to get correct memory type");
      }();
    }

    if(vkAllocateMemory(vk_device, &vk_memoryAllocateInfo, nullptr, &vk_deviceMemory) != VkResult::VK_SUCCESS)
    {
      throw std::exception("failed to allocate device memory");
    }

    void* data;
    if(vkMapMemory(vk_device, vk_deviceMemory, 0, sizeof(float)* source.size(), 0, &data) != VkResult::VK_SUCCESS)
    {
      throw std::exception("failed to map device memory");
    }

    memcpy(data, source.data(), sizeof(float)* source.size());

    vkUnmapMemory(vk_device, vk_deviceMemory);
  }

  if(vkBindBufferMemory(vk_device, vk_buffer, vk_deviceMemory, 0) != VkResult::VK_SUCCESS)
  {
    throw std::exception("failed to bind buffer memory");
  }
}

Шейдеры

Процессом вывода примитивов занимаются шейдерные модули. Группа таких модулей объединяется в конвейер, который определяет все этапы и тонкости рисования, но об этом чуть позже.

Создаются шейдеры, по заранее подготовленному исходному коду на шейдерном языке SPIR-V. Последний является промежуточным языком, а потому, в отличии от OpenGL, трансляция теперь становится заботой разработчика. Тем не менее, в состав «LunarG Vulkan SDK» также входит транслятор GLSL->SPIR-V, с помощью которого мы и будем создавать шейдеры для Vulkan.

План:
•  Создать вершинный и фрагментный шейдеры на GLSL
•  Перевести их в SPIR-V
•  Создать из исходного кода шейдерные модули

Исходный код

Каждый шейдер отвечает за отдельную стадию графического конвейера: вершинную, контроля тесселляции, оценки тесселляции, геометрическую или фрагментную. Нам потребуются лишь две стадии: вершинная и фрагментная. Для хранения исходного кода создадим два файла в папке с проектом: 1.vert и 1.frag, и наполним их содержимым:
1.vert:

#version 450

layout(location = 0) in vec2 vPos;

void main()
{
  gl_Position = vec4(vPos.x, vPos.y, 0.0f, 1.0f);
}

1.frag:

#version 450

layout(location = 0) out vec4 oColor;

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

Как и всегда, упустим объяснение касающиеся GLSL, т.к. это предмет отдельной статьи.
Для перевода исходного кода из GLSL в SPIR-V мы будем использовать утилиту glslangValidator.exe, поставляемую вместе с «LunarG Vulkan SDK» и находящуюся в папке Bin/Bin32. Но есть один нюанс...

Т.к. во время дебага нам придется менять код шейдеров довольно часто, мы создадим batник, который будет транслировать все файлы в папке на SPIR-V:

@echo off

cd %CD%

for %%i in (*.vert) do glslangValidator.exe %%i -V -o %%i.spv
for %%i in (*.frag) do glslangValidator.exe %%i -V -o %%i.spv

pause

И опять таки обойдемся без объяснений вышеперечисленных команд. За нас это уже сделала сеть.
После запуска нам будут предоставлены файлы 1.vert.spv и 1.frag.spv, которые и являются нашим скомпилированными шейдерами.
Последний момент, слегка отдаленный непосредственно от Vulkan API - загрузка кода шейдера:

auto loadShader = [](const std::string& filename)
{
  FILE* file = nullptr;
  if(fopen_s(&file, filename.c_str(), "rb") != 0)
  {
    throw std::exception("failed to load file");
  }
  fseek(file, 0, FILE_END);
  auto size = ftell(file);
  if(size % 4 != 0) throw std::exception("");

  rewind(file);

  std::vector<uint32_t> result(size);
  fread((void*)result.data(), 1, size, file);

  fclose(file);

  return std::move(result);
};

Шейдерные модули

По аналогии с OpenGL, шейдеры в Vulkan API являются отдельными звеньями, которые могут использоваться в разных конфигурациях в зависимости от задачи.
Создается шейдерный модуль с помощью:

VkResult vkCreateShaderModule(
    VkDevice                                    device,
    const VkShaderModuleCreateInfo*             pCreateInfo,
    const VkAllocationCallbacks*                pAllocator,
    VkShaderModule*                             pShaderModule);

typedef struct VkShaderModuleCreateInfo {
    VkStructureType              sType;
    const void*                  pNext;
    VkShaderModuleCreateFlags    flags;
    size_t                       codeSize;
    const uint32_t*              pCode;
} VkShaderModuleCreateInfo;

Информационная структура тривиальна: достаточно указать начало данных и их длину в формате uint32_t (4 байта).

Таким образом создаем два модуля:
вершинный:

VkShaderModule vk_shaderModuleVertex;
{
  auto source = loadShader("1.vert.spv");

  VkShaderModuleCreateInfo vk_shaderModuleCreateInfo;
  {
    vk_shaderModuleCreateInfo.sType = VkStructureType::VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
    vk_shaderModuleCreateInfo.pNext = nullptr;
    vk_shaderModuleCreateInfo.flags = 0;
    vk_shaderModuleCreateInfo.codeSize = source.size();
    vk_shaderModuleCreateInfo.pCode = source.data();
  }

  if(vkCreateShaderModule(vk_device, &vk_shaderModuleCreateInfo, nullptr, &vk_shaderModuleVertex) != VkResult::VK_SUCCESS)
  {
    throw std::exception("failed to create vertex shader");
  }
}

и фрагментный:

VkShaderModule vk_shaderModuleFragment;
{
  auto source = loadShader("1.frag.spv");

  VkShaderModuleCreateInfo vk_shaderModuleCreateInfo;
  {
    vk_shaderModuleCreateInfo.sType = VkStructureType::VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
    vk_shaderModuleCreateInfo.pNext = nullptr;
    vk_shaderModuleCreateInfo.flags = 0;
    vk_shaderModuleCreateInfo.codeSize = source.size();
    vk_shaderModuleCreateInfo.pCode = source.data();
  }

  if(vkCreateShaderModule(vk_device, &vk_shaderModuleCreateInfo, nullptr, &vk_shaderModuleFragment) != VkResult::VK_SUCCESS)
  {
    throw std::exception("failed to create fragment shader");
  }
}
Страницы: 1 2 3 4 5 6 7 Следующая »

#Vulkan, #основы

15 сентября 2016

Комментарии [69]